1. 简介
欢迎来到 Ruby on Rails!在本指南中,我们将逐步讲解使用 Rails 构建 Web 应用程序的核心概念。您不需要任何 Rails 经验即可跟随本指南。
Rails 是一个为 Ruby 编程语言构建的 Web 框架。Rails 利用了 Ruby 的许多特性,因此我们强烈建议您学习 Ruby 的基础知识,以便您理解本教程中将看到的一些基本术语和词汇。
2. Rails 哲学
Rails 是一个用 Ruby 编程语言编写的 Web 应用程序开发框架。它通过假设每个开发人员入门所需的内容,旨在使 Web 应用程序编程变得更容易。它允许您编写更少的代码,同时比许多其他语言和框架完成更多的工作。经验丰富的 Rails 开发人员也报告说,它使 Web 应用程序开发更有趣。
Rails 是一个有主见的软件。它假设存在一种“最佳”做事方式,并旨在鼓励这种方式——在某些情况下,甚至不鼓励替代方案。如果您学习“Rails 方式”,您可能会发现生产力会大大提高。如果您坚持将其他语言的旧习惯带到您的 Rails 开发中,并试图使用您在其他地方学到的模式,您可能会有不那么愉快的经历。
Rails 哲学包括两个主要的指导原则
- 不要重复自己 (DRY): DRY 是一项软件开发原则,它指出“系统中的每一条知识都必须有一个单一、明确、权威的表示”。通过不一遍又一遍地编写相同的信息,我们的代码更易于维护、更具可扩展性且错误更少。
- 约定优于配置: Rails 对 Web 应用程序中许多事情的最佳方式有自己的看法,并默认采用这套约定,而不是要求您通过无休止的配置文件来定义它们。
3. 创建新的 Rails 应用程序
我们将构建一个名为 store 的项目——一个简单的电子商务应用程序,它展示了 Rails 的几个内置功能。
任何以美元符号 $ 开头的命令都应在终端中运行。
3.1. 先决条件
对于此项目,您需要
- Ruby 3.2 或更高版本
- Rails 8.1.0 或更高版本
- 一个代码编辑器
如果您需要安装 Ruby 和/或 Rails,请按照安装 Ruby on Rails 指南进行操作。
让我们验证是否安装了正确版本的 Rails。要显示当前版本,请打开终端并运行以下命令。您应该会看到一个版本号被打印出来
$ rails --version
Rails 8.1.0
显示的版本应该是 Rails 8.1.0 或更高版本。
3.2. 创建您的第一个 Rails 应用程序
Rails 附带了几个命令,可以简化生活。运行 rails --help 查看所有命令。
rails new 为您生成了一个全新的 Rails 应用程序的基础,所以让我们从这里开始。
要创建我们的 store 应用程序,请在您的终端中运行以下命令
$ rails new store
您可以使用标志自定义 Rails 生成的应用程序。要查看这些选项,请运行 rails new --help。
创建新应用程序后,切换到其目录
$ cd store
3.3. 目录结构
让我们快速浏览一下新 Rails 应用程序中包含的文件和目录。您可以在代码编辑器中打开此文件夹或在终端中运行 ls -la 查看文件和目录。
| 文件/文件夹 | 用途 |
|---|---|
| app/ | 包含您应用程序的控制器、模型、视图、助手、邮件程序、作业和资产。本指南的其余部分将主要关注此文件夹。 |
| bin/ | 包含启动应用程序的 rails 脚本,并且可以包含您用于设置、更新、部署或运行应用程序的其他脚本。 |
| config/ | 包含应用程序路由、数据库等的配置。这在配置 Rails 应用程序中有更详细的介绍。 |
| config.ru | 用于启动应用程序的基于 Rack 的服务器的 Rack 配置。 |
| db/ | 包含您当前的数据库模式以及数据库迁移。 |
| Dockerfile | Docker 的配置文件。 |
| Gemfile Gemfile.lock |
这些文件允许您指定 Rails 应用程序所需的 gem 依赖项。这些文件由 Bundler gem 使用。 |
| lib/ | 应用程序的扩展模块。 |
| log/ | 应用程序日志文件。 |
| public/ | 包含静态文件和编译后的资产。当您的应用程序运行时,此目录将按原样公开。 |
| Rakefile | 此文件查找并加载可从命令行运行的任务。任务定义在 Rails 的各个组件中。您应该通过将文件添加到应用程序的 lib/tasks 目录来添加自己的任务,而不是更改 Rakefile。 |
| README.md | 这是您应用程序的简要说明手册。您应该编辑此文件以告诉其他人您的应用程序做什么、如何设置等等。 |
| script/ | 包含一次性或通用 脚本和基准测试。 |
| storage/ | 包含 SQLite 数据库和用于磁盘服务的 Active Storage 文件。这在Active Storage 概述中有介绍。 |
| test/ | 单元测试、夹具和其他测试设备。这在测试 Rails 应用程序中有介绍。 |
| tmp/ | 临时文件(如缓存和 pid 文件)。 |
| vendor/ | 所有第三方代码的存放位置。在典型的 Rails 应用程序中,这包括供应商 gems。 |
| .dockerignore | 此文件告诉 Docker 哪些文件不应复制到容器中。 |
| .gitattributes | 此文件定义了 Git 仓库中特定路径的元数据。此元数据可由 Git 和其他工具用于增强其行为。有关更多信息,请参阅 gitattributes 文档。 |
| .git/ | 包含 Git 仓库文件。 |
| .github/ | 包含 GitHub 特定文件。 |
| .gitignore | 此文件告诉 Git 应该忽略哪些文件(或模式)。有关忽略文件的更多信息,请参阅 GitHub - 忽略文件。 |
| .kamal/ | 包含 Kamal 密钥和部署钩子。 |
| .rubocop.yml | 此文件包含 RuboCop 的配置。 |
| .ruby-version | 此文件包含默认的 Ruby 版本。 |
3.4. 模型-视图-控制器 基础
Rails 代码使用模型-视图-控制器 (MVC) 架构进行组织。通过 MVC,我们有三个主要概念,其中大部分代码都位于
- 模型 - 管理应用程序中的数据。通常是您的数据库表。
- 视图 - 处理以不同格式(如 HTML、JSON、XML 等)渲染响应。
- 控制器 - 处理用户交互和每个请求的逻辑。
现在我们对 MVC 有了基本的了解,让我们看看它在 Rails 中是如何使用的。
4. 你好,Rails!
让我们从简单的开始,创建应用程序的数据库并首次启动 Rails 服务器。
在您的终端中,在 store 目录中运行以下命令
$ bin/rails db:create
这将首先创建应用程序的数据库。
$ bin/rails server
当我们在应用程序目录中运行命令时,我们应该使用 bin/rails。这确保使用了应用程序版本的 Rails。
这将启动一个名为 Puma 的 Web 服务器,它将提供静态文件和您的 Rails 应用程序
=> Booting Puma
=> Rails 8.1.0 application starting in development
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Puma version: 6.4.3 (ruby 3.3.5-p100) ("The Eagle of Durango")
* Min threads: 3
* Max threads: 3
* Environment: development
* PID: 12345
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop
要查看您的 Rails 应用程序,请在浏览器中打开 https://:3000。您将看到默认的 Rails 欢迎页面

它奏效了!
此页面是新 Rails 应用程序的冒烟测试,确保一切都在幕后正常工作以提供页面。
要随时停止 Rails 服务器,请在终端中按 Ctrl-C。
4.1. 开发环境中的自动加载
开发人员的幸福是 Rails 的基石哲学,实现这一点的一种方法是在开发环境中自动重新加载代码。
一旦您启动 Rails 服务器,新文件或对现有文件的更改就会被检测到并根据需要自动加载或重新加载。这使您可以专注于构建,而无需在每次更改后重新启动 Rails 服务器。
您可能还会注意到,Rails 应用程序很少使用您在其他编程语言中可能看到过的 require 语句。Rails 使用命名约定自动 require 文件,因此您可以专注于编写应用程序代码。
有关更多详细信息,请参阅自动加载和重新加载常量。
5. 创建数据库模型
Active Record 是 Rails 的一项功能,它将关系数据库映射到 Ruby 代码。它有助于生成用于与数据库交互的结构化查询语言 (SQL),例如创建、更新和删除表和记录。我们的应用程序使用的是 SQLite,它是 Rails 的默认数据库。
让我们首先向我们的 Rails 应用程序添加一个数据库表,以便向我们的简单电子商务商店添加产品。
$ bin/rails generate model Product name:string
此命令告诉 Rails 生成一个名为 Product 的模型,该模型在数据库中具有一个 name 列和 string 类型。稍后,您将学习如何添加其他列类型。
您将在终端中看到以下内容
invoke active_record
create db/migrate/20240426151900_create_products.rb
create app/models/product.rb
invoke test_unit
create test/models/product_test.rb
create test/fixtures/products.yml
此命令执行多项操作。它创建...
db/migrate文件夹中的一个迁移文件。app/models/product.rb中的一个 Active Record 模型。- 此模型的测试和测试夹具。
模型名称是单数,因为一个实例化的模型表示数据库中的一条记录(即,您正在创建要添加到数据库的产品)。
5.1. 数据库迁移
迁移是我们想要对数据库进行的一组更改。
通过定义迁移,我们告诉 Rails 如何更改数据库以添加、更改或删除表、列或数据库的其他属性。这有助于跟踪我们在开发中(仅在我们的计算机上)所做的更改,以便它们可以安全地部署到生产环境(在线!)中。
在您的代码编辑器中,打开 Rails 为我们创建的迁移文件,以便我们可以查看迁移所做的工作。它位于 db/migrate/<timestamp>_create_products.rb
class CreateProducts < ActiveRecord::Migration[8.1]
def change
create_table :products do |t|
t.string :name
t.timestamps
end
end
end
此迁移告诉 Rails 创建一个名为 products 的新数据库表。
与上面的模型相反,Rails 将数据库表名设置为复数,因为数据库保存了每个模型的所有实例(即,您正在创建一个产品数据库)。
create_table 块然后定义了此数据库表中应该定义的列和类型。
t.string :name 告诉 Rails 在 products 表中创建一个名为 name 的列,并将类型设置为 string。
t.timestamps 是定义模型上两个列的快捷方式:created_at:datetime 和 updated_at:datetime。您将在 Rails 的大多数 Active Record 模型上看到这些列,它们在创建或更新记录时由 Active Record 自动设置。
5.2. 运行迁移
现在您已经定义了要对数据库进行的更改,使用以下命令运行迁移
$ bin/rails db:migrate
此命令检查任何新的迁移并将其应用到您的数据库。其输出如下所示
== 20240426151900 CreateProducts: migrating ===================================
-- create_table(:products)
-> 0.0030s
== 20240426151900 CreateProducts: migrated (0.0031s) ==========================
如果您犯了错误,可以运行 bin/rails db:rollback 来撤消上次迁移。
6. Rails 控制台
现在我们已经创建了产品表,我们可以在 Rails 中与其交互。让我们试一试。
为此,我们将使用 Rails 的一个功能,称为控制台。控制台是一个有用的交互式工具,用于在 Rails 应用程序中测试我们的代码。
$ bin/rails console
您将看到如下提示
Loading development environment (Rails 8.1.0)
store(dev)>
在这里我们可以键入代码,当按下 Enter 时将执行该代码。让我们尝试打印出 Rails 版本
store(dev)> Rails.version
=> "8.1.0"
它奏效了!
7. Active Record 模型基础
当我们运行 Rails 模型生成器创建 Product 模型时,它在 app/models/product.rb 创建了一个文件。此文件创建了一个使用 Active Record 与我们的 products 数据库表交互的类。
class Product < ApplicationRecord
end
您可能会惊讶于这个类中没有代码。Rails 如何知道定义这个模型的内容?
当使用 Product 模型时,Rails 将查询数据库表以获取列名和类型,并自动为这些属性生成代码。Rails 省去了我们编写这些样板代码,而是为我们在幕后处理,这样我们就可以专注于应用程序逻辑。
让我们使用 Rails 控制台查看 Rails 为 Product 模型检测到哪些列。
运行
store(dev)> Product.column_names
您应该会看到
=> ["id", "name", "created_at", "updated_at"]
Rails 向数据库请求了上述列信息,并使用该信息在 Product 类上动态定义属性,因此您无需手动定义每个属性。这是 Rails 如何使开发变得轻而易举的一个示例。
7.1. 创建记录
我们可以使用以下代码实例化一个新的产品记录
store(dev)> product = Product.new(name: "T-Shirt")
=> #<Product:0x000000012e616c30 id: nil, name: "T-Shirt", created_at: nil, updated_at: nil>
product 变量是 Product 的一个实例。它尚未保存到数据库中,因此没有 ID、created_at 或 updated_at 时间戳。
我们可以调用 save 将记录写入数据库。
store(dev)> product.save
TRANSACTION (0.1ms) BEGIN immediate TRANSACTION /*application='Store'*/
Product Create (0.9ms) INSERT INTO "products" ("name", "created_at", "updated_at") VALUES ('T-Shirt', '2024-11-09 16:35:01.117836', '2024-11-09 16:35:01.117836') RETURNING "id" /*application='Store'*/
TRANSACTION (0.9ms) COMMIT TRANSACTION /*application='Store'*/
=> true
当调用 save 时,Rails 会获取内存中的属性并生成 INSERT SQL 查询,将此记录插入到数据库中。
Rails 还会使用数据库记录 id 以及 created_at 和 updated_at 时间戳更新内存中的对象。我们可以通过打印出 product 变量来看到这一点。
store(dev)> product
=> #<Product:0x00000001221f6260 id: 1, name: "T-Shirt", created_at: "2024-11-09 16:35:01.117836000 +0000", updated_at: "2024-11-09 16:35:01.117836000 +0000">
与 save 类似,我们可以使用 create 在一次调用中实例化并保存 Active Record 对象。
store(dev)> Product.create(name: "Pants")
TRANSACTION (0.1ms) BEGIN immediate TRANSACTION /*application='Store'*/
Product Create (0.4ms) INSERT INTO "products" ("name", "created_at", "updated_at") VALUES ('Pants', '2024-11-09 16:36:01.856751', '2024-11-09 16:36:01.856751') RETURNING "id" /*application='Store'*/
TRANSACTION (0.1ms) COMMIT TRANSACTION /*application='Store'*/
=> #<Product:0x0000000120485c80 id: 2, name: "Pants", created_at: "2024-11-09 16:36:01.856751000 +0000", updated_at: "2024-11-09 16:36:01.856751000 +0000">
7.2. 查询记录
我们还可以使用 Active Record 模型从数据库中查找记录。
要查找数据库中的所有 Product 记录,我们可以使用 all 方法。这是一个类方法,这就是为什么我们可以在 Product 上使用它(与我们在产品实例上调用的实例方法(如上面的 save)不同)。
store(dev)> Product.all
Product Load (0.1ms) SELECT "products".* FROM "products" /* loading for pp */ LIMIT 11 /*application='Store'*/
=> [#<Product:0x0000000121845158 id: 1, name: "T-Shirt", created_at: "2024-11-09 16:35:01.117836000 +0000", updated_at: "2024-11-09 16:35:01.117836000 +0000">,
#<Product:0x0000000121845018 id: 2, name: "Pants", created_at: "2024-11-09 16:36:01.856751000 +0000", updated_at: "2024-11-09 16:36:01.856751000 +0000">]
这会生成一个 SELECT SQL 查询,从 products 表中加载所有记录。每条记录都会自动转换为我们的 Product Active Record 模型的一个实例,因此我们可以轻松地使用 Ruby 处理它们。
all 方法返回一个 ActiveRecord::Relation 对象,它是一个类似数组的数据库记录集合,具有过滤、排序和执行其他数据库操作的功能。
7.3. 过滤和排序记录
如果我们想过滤数据库中的结果怎么办?我们可以使用 where 按列过滤记录。
store(dev)> Product.where(name: "Pants")
Product Load (1.5ms) SELECT "products".* FROM "products" WHERE "products"."name" = 'Pants' /* loading for pp */ LIMIT 11 /*application='Store'*/
=> [#<Product:0x000000012184d858 id: 2, name: "Pants", created_at: "2024-11-09 16:36:01.856751000 +0000", updated_at: "2024-11-09 16:36:01.856751000 +0000">]
这会生成一个 SELECT SQL 查询,但也会添加一个 WHERE 子句来过滤名称与 "Pants" 匹配的记录。这也会返回一个 ActiveRecord::Relation,因为多个记录可能具有相同的名称。
我们可以使用 order(name: :asc) 按名称升序字母顺序对记录进行排序。
store(dev)> Product.order(name: :asc)
Product Load (0.3ms) SELECT "products".* FROM "products" /* loading for pp */ ORDER BY "products"."name" ASC LIMIT 11 /*application='Store'*/
=> [#<Product:0x0000000120e02a88 id: 2, name: "Pants", created_at: "2024-11-09 16:36:01.856751000 +0000", updated_at: "2024-11-09 16:36:01.856751000 +0000">,
#<Product:0x0000000120e02948 id: 1, name: "T-Shirt", created_at: "2024-11-09 16:35:01.117836000 +0000", updated_at: "2024-11-09 16:35:01.117836000 +0000">]
7.4. 查找记录
如果我们想查找一条特定的记录怎么办?
我们可以通过使用 find 类方法按 ID 查找单个记录来实现。调用该方法并使用以下代码传入特定 ID
store(dev)> Product.find(1)
Product Load (0.2ms) SELECT "products".* FROM "products" WHERE "products"."id" = 1 LIMIT 1 /*application='Store'*/
=> #<Product:0x000000012054af08 id: 1, name: "T-Shirt", created_at: "2024-11-09 16:35:01.117836000 +0000", updated_at: "2024-11-09 16:35:01.117836000 +0000">
这会生成一个 SELECT 查询,但为 id 列指定了一个 WHERE,使其与传入的 ID 1 匹配。它还添加了一个 LIMIT 以仅返回一条记录。
这次,我们获得了一个 Product 实例,而不是 ActiveRecord::Relation,因为我们只从数据库中检索一条记录。
7.5. 更新记录
记录可以通过两种方式更新:使用 update 或分配属性并调用 save。
我们可以在 Product 实例上调用 update 并传入一个新的属性哈希以保存到数据库。这将在一个方法调用中分配属性、运行验证并将更改保存到数据库。
store(dev)> product = Product.find(1)
store(dev)> product.update(name: "Shoes")
TRANSACTION (0.1ms) BEGIN immediate TRANSACTION /*application='Store'*/
Product Update (0.3ms) UPDATE "products" SET "name" = 'Shoes', "updated_at" = '2024-11-09 22:38:19.638912' WHERE "products"."id" = 1 /*application='Store'*/
TRANSACTION (0.4ms) COMMIT TRANSACTION /*application='Store'*/
=> true
这将“T恤”产品的名称更新为数据库中的“鞋子”。通过再次运行 Product.all 确认这一点。
store(dev)> Product.all
您将看到两种产品:鞋子和裤子。
Product Load (0.3ms) SELECT "products".* FROM "products" /* loading for pp */ LIMIT 11 /*application='Store'*/
=>
[#<Product:0x000000012c0f7300
id: 1,
name: "Shoes",
created_at: "2024-12-02 20:29:56.303546000 +0000",
updated_at: "2024-12-02 20:30:14.127456000 +0000">,
#<Product:0x000000012c0f71c0
id: 2,
name: "Pants",
created_at: "2024-12-02 20:30:02.997261000 +0000",
updated_at: "2024-12-02 20:30:02.997261000 +0000">]
或者,我们可以分配属性并在准备好验证并将更改保存到数据库时调用 save。
让我们将名称“鞋子”改回“T恤”。
store(dev)> product = Product.find(1)
store(dev)> product.name = "T-Shirt"
=> "T-Shirt"
store(dev)> product.save
TRANSACTION (0.1ms) BEGIN immediate TRANSACTION /*application='Store'*/
Product Update (0.2ms) UPDATE "products" SET "name" = 'T-Shirt', "updated_at" = '2024-11-09 22:39:09.693548' WHERE "products"."id" = 1 /*application='Store'*/
TRANSACTION (0.0ms) COMMIT TRANSACTION /*application='Store'*/
=> true
7.6. 删除记录
destroy 方法可用于从数据库中删除记录。
store(dev)> product.destroy
TRANSACTION (0.1ms) BEGIN immediate TRANSACTION /*application='Store'*/
Product Destroy (0.4ms) DELETE FROM "products" WHERE "products"."id" = 1 /*application='Store'*/
TRANSACTION (0.1ms) COMMIT TRANSACTION /*application='Store'*/
=> #<Product:0x0000000125813d48 id: 1, name: "T-Shirt", created_at: "2024-11-09 22:39:38.498730000 +0000", updated_at: "2024-11-09 22:39:38.498730000 +0000">
这从我们的数据库中删除了 T 恤产品。我们可以通过 Product.all 确认这一点,看看它只返回裤子。
store(dev)> Product.all
Product Load (1.9ms) SELECT "products".* FROM "products" /* loading for pp */ LIMIT 11 /*application='Store'*/
=>
[#<Product:0x000000012abde4c8
id: 2,
name: "Pants",
created_at: "2024-11-09 22:33:19.638912000 +0000",
updated_at: "2024-11-09 22:33:19.638912000 +0000">]
7.7. 验证
Active Record 提供验证,允许您确保插入到数据库中的数据符合某些规则。
让我们向 Product 模型添加一个 presence 验证,以确保所有产品都必须有一个 name。
class Product < ApplicationRecord
validates :name, presence: true
end
您可能还记得 Rails 在开发过程中会自动重新加载更改。但是,如果在您更新代码时控制台正在运行,您需要手动刷新它。所以让我们现在通过运行“reload!”来做到这一点。
store(dev)> reload!
Reloading...
让我们尝试在 Rails 控制台中创建一个没有名称的产品。
store(dev)> product = Product.new
store(dev)> product.save
=> false
这次 save 返回 false,因为没有指定 name 属性。
Rails 在创建、更新和保存操作期间自动运行验证,以确保有效输入。要查看验证生成的错误列表,我们可以在实例上调用 errors。
store(dev)> product.errors
=> #<ActiveModel::Errors [#<ActiveModel::Error attribute=name, type=blank, options={}>]>
这会返回一个 ActiveModel::Errors 对象,它可以准确地告诉我们存在哪些错误。
它还可以为我们生成友好的错误消息,我们可以在用户界面中使用。
store(dev)> product.errors.full_messages
=> ["Name can't be blank"]
现在让我们为我们的产品构建一个 Web 界面。
我们暂时完成了控制台,因此您可以运行 exit 退出。
8. 请求在 Rails 中的旅程
要让 Rails 说“Hello”,您至少需要创建一个路由、一个带有动作的控制器和一个视图。路由将请求映射到控制器动作。控制器动作执行处理请求所需的必要工作,并为视图准备任何数据。视图以所需的格式显示数据。
在实现方面:路由是用 Ruby DSL(领域特定语言)编写的规则。控制器是 Ruby 类,它们的公共方法是动作。视图是模板,通常用 HTML 和 Ruby 的混合编写。
简而言之,但接下来我们将更详细地讲解这些步骤。
9. 路由
在 Rails 中,路由是 URL 的一部分,它决定了如何将传入的 HTTP 请求定向到适当的控制器和操作进行处理。首先,让我们快速回顾一下 URL 和 HTTP 请求方法。
9.1. URL 的组成部分
让我们检查 URL 的不同部分
https://example.org/products?sale=true&sort=asc
在此 URL 中,每个部分都有一个名称
https是 协议example.org是 主机/products是 路径?sale=true&sort=asc是 查询参数
9.2. HTTP 方法及其用途
HTTP 请求使用方法来告诉服务器它应该为给定的 URL 执行什么操作。以下是最常见的方法
GET请求告诉服务器检索给定 URL 的数据(例如,加载页面或获取记录)。POST请求会将数据提交到 URL 进行处理(通常是创建新记录)。PUT或PATCH请求将数据提交到 URL 以更新现有记录。- 对 URL 的
DELETE请求告诉服务器删除记录。
9.3. Rails 路由
Rails 中的 route 指的是将 HTTP 方法和 URL 路径配对的代码行。路由还会告诉 Rails 哪个 controller 和 action 应该响应请求。
要在 Rails 中定义路由,让我们回到代码编辑器,并将以下路由添加到 config/routes.rb
Rails.application.routes.draw do
get "/products", to: "products#index"
end
此路由告诉 Rails 查找对 /products 路径的 GET 请求。在此示例中,我们指定 "products#index" 作为请求的路由位置。
当 Rails 看到匹配的请求时,它会将请求发送到 ProductsController 和该控制器内的 index 动作。这就是 Rails 处理请求并将响应返回给浏览器的方式。
您会注意到,我们不需要在路由中指定协议、域或查询参数。这基本上是因为协议和域确保请求到达您的服务器。从那里,Rails 接收请求并知道根据定义的路由使用哪个路径来响应请求。查询参数就像 Rails 可以用于应用于请求的选项,因此它们通常用于控制器中以过滤数据。
让我们看另一个例子。在前一个路由之后添加这一行
post "/products", to: "products#create"
在这里,我们告诉 Rails 接收对“/products”的 POST 请求,并使用 ProductsController 的 create 动作处理它们。
路由可能还需要匹配具有特定模式的 URL。那它是如何工作的呢?
get "/products/:id", to: "products#show"
此路由包含 :id。这称为 parameter,它捕获 URL 的一部分以供稍后处理请求时使用。
如果用户访问 /products/1,:id 参数将设置为 1,并且可以在控制器动作中使用它来查找并显示 ID 为 1 的产品记录。/products/2 将显示 ID 为 2 的产品,依此类推。
路由参数也不必是整数。
例如,您可以有一个带有文章的博客,并将 /blog/hello-world 与以下路由匹配
get "/blog/:title", to: "blog#show"
Rails 将从 /blog/hello-world 中捕获 hello-world,这可以用来查找具有匹配标题的博客文章。
9.3.1. CRUD 路由
资源通常需要 4 种常见操作:创建、读取、更新、删除 (CRUD)。这转化为 8 个典型路由
- 索引 - 显示所有记录
- 新建 - 渲染用于创建新记录的表单
- 创建 - 处理新表单提交,处理错误并创建记录
- 显示 - 渲染特定记录以供查看
- 编辑 - 渲染用于更新特定记录的表单
- 更新(完整) - 处理编辑表单提交,处理错误并更新整个记录,通常由 PUT 请求触发。
- 更新(部分) - 处理编辑表单提交,处理错误并更新记录的特定属性,通常由 PATCH 请求触发。
- 销毁 - 处理删除特定记录
我们可以使用以下方法添加这些 CRUD 操作的路由
get "/products", to: "products#index"
get "/products/new", to: "products#new"
post "/products", to: "products#create"
get "/products/:id", to: "products#show"
get "/products/:id/edit", to: "products#edit"
patch "/products/:id", to: "products#update"
put "/products/:id", to: "products#update"
delete "/products/:id", to: "products#destroy"
9.3.2. 资源路由
每次都输入这些路由是多余的,因此 Rails 提供了一个快捷方式来定义它们。要创建所有相同的 CRUD 路由,请将上述路由替换为这一行
resources :products
如果您不需要所有这些 CRUD 操作,您可以准确指定您需要的内容。有关详细信息,请查看路由指南。
9.4. 路由命令
Rails 提供了一个命令,可以显示您的应用程序响应的所有路由。
在您的终端中,运行以下命令。
$ bin/rails routes
您将在输出中看到这些,它们是 resources :products 生成的路由
Prefix Verb URI Pattern Controller#Action
products GET /products(.:format) products#index
POST /products(.:format) products#create
new_product GET /products/new(.:format) products#new
edit_product GET /products/:id/edit(.:format) products#edit
product GET /products/:id(.:format) products#show
PATCH /products/:id(.:format) products#update
PUT /products/:id(.:format) products#update
DELETE /products/:id(.:format) products#destroy
您还将看到其他内置 Rails 功能(如健康检查)的路由。
10. 控制器和动作
现在我们已经为产品定义了路由,让我们实现控制器和动作来处理对这些 URL 的请求。
此命令将生成一个带有 index 动作的 ProductsController。由于我们已经设置了路由,因此我们可以使用标志跳过生成器的该部分。
$ bin/rails generate controller Products index --skip-routes
create app/controllers/products_controller.rb
invoke erb
create app/views/products
create app/views/products/index.html.erb
invoke test_unit
create test/controllers/products_controller_test.rb
invoke helper
create app/helpers/products_helper.rb
invoke test_unit
此命令为我们的控制器生成了少数文件
- 控制器本身
- 我们生成的控制器的视图文件夹
- 我们生成控制器时指定的动作的视图文件
- 此控制器的测试文件
- 一个用于提取视图中逻辑的助手文件
让我们看一下 app/controllers/products_controller.rb 中定义的 ProductsController。它看起来像这样
class ProductsController < ApplicationController
def index
end
end
您可能会注意到文件名 products_controller.rb 是此文件定义的类 ProductsController 的下划线版本。这种模式有助于 Rails 自动加载代码,而无需像您在其他语言中可能看到的那样使用 require。
这里的 index 方法是一个动作。尽管它是一个空方法,Rails 仍将默认渲染具有匹配名称的模板。
index 动作将渲染 app/views/products/index.html.erb。如果我们用代码编辑器打开该文件,我们将看到它渲染的 HTML。
<h1>Products#index</h1>
<p>Find me in app/views/products/index.html.erb</p>
10.1. 发出请求
让我们在浏览器中看看这个。首先,在终端中运行 bin/rails server 启动 Rails 服务器。然后打开 https://:3000,您将看到 Rails 欢迎页面。
如果我们在浏览器中打开 https://:3000/products,Rails 将渲染产品索引 HTML。
我们的浏览器请求了 /products,Rails 将此路由匹配到 products#index。Rails 将请求发送到 ProductsController 并调用了 index 动作。由于此动作为空,Rails 渲染了 app/views/products/index.html.erb 中的匹配模板并将其返回给我们的浏览器。非常酷!
如果打开 config/routes.rb,我们可以通过添加以下行告诉 Rails 根路由应该渲染 Products index 动作
root "products#index"
现在,当您访问 https://:3000 时,Rails 将渲染 Products#index。
10.2. 实例变量
让我们更进一步,渲染数据库中的一些记录。
在 index 动作中,让我们添加一个数据库查询并将其分配给一个实例变量。Rails 使用实例变量(以 @ 开头的变量)与视图共享数据。
class ProductsController < ApplicationController
def index
@products = Product.all
end
end
在 app/views/products/index.html.erb 中,我们可以用这个 ERB 替换 HTML
<%= debug @products %>
ERB 是 嵌入式 Ruby 的缩写,它允许我们执行 Ruby 代码以使用 Rails 动态生成 HTML。<%= %> 标签告诉 ERB 执行内部的 Ruby 代码并输出返回值。在我们的例子中,这会获取 @products,将其转换为 YAML,并输出 YAML。
现在刷新浏览器中的 https://:3000/,您会看到输出已更改。您看到的是以 YAML 格式显示的数据库中的记录。
debug 助手以 YAML 格式打印出变量,以帮助调试。例如,如果您没有注意并输入了单数 @product 而不是复数 @products,调试助手可以帮助您识别该变量在控制器中未正确设置。
查看Action View 助手指南,了解更多可用的助手。
让我们更新 app/views/products/index.html.erb 以渲染我们所有产品名称。
<h1>Products</h1>
<div id="products">
<% @products.each do |product| %>
<div>
<%= product.name %>
</div>
<% end %>
</div>
使用 ERB,此代码遍历 @products ActiveRecord::Relation 对象中的每个产品,并渲染一个包含产品名称的 <div> 标签。
这次我们也使用了新的 ERB 标签。<% %> 评估 Ruby 代码但不输出返回值。这会忽略 @products.each 的输出,它会输出一个我们不希望出现在 HTML 中的数组。
10.3. CRUD 动作
我们需要能够访问单个产品。这是 CRUD 中的 R,用于读取资源。
我们已经通过 resources :products 路由定义了单个产品的路由。这会将 /products/:id 生成为指向 products#show 的路由。
现在我们需要将该操作添加到 ProductsController 并定义调用它时会发生什么。
10.4. 显示单个产品
打开 Products 控制器并添加 show 动作,如下所示
class ProductsController < ApplicationController
def index
@products = Product.all
end
def show
@product = Product.find(params[:id])
end
end
这里的 show 动作定义了单数 @product,因为它正在从数据库中加载一条记录,换句话说:显示这一个产品。我们在 index 中使用复数 @products,因为我们正在加载多个产品。
为了查询数据库,我们使用 params 来访问请求参数。在这种情况下,我们正在使用路由 /products/:id 中的 :id。当我们访问 /products/1 时,params 哈希包含 {id: 1},这导致我们的 show 动作调用 Product.find(1) 来从数据库中加载 ID 为 1 的产品。
接下来我们需要一个用于 show 动作的视图。遵循 Rails 命名约定,ProductsController 期望视图位于 app/views 中的一个名为 products 的子文件夹中。
show 动作期望 app/views/products/show.html.erb 中有一个文件。让我们在编辑器中创建该文件并添加以下内容
<h1><%= @product.name %></h1>
<%= link_to "Back", products_path %>
索引页面链接到每个产品的显示页面会很有帮助,这样我们就可以点击它们进行导航。我们可以更新 app/views/products/index.html.erb 视图以链接到此新页面,以使用锚标记指向 show 动作的路径。
<h1>Products</h1>
<div id="products">
<% @products.each do |product| %>
<div>
<a href="/products/<%= product.id %>">
<%= product.name %>
</a>
</div>
<% end %>
</div>
在浏览器中刷新此页面,您会看到它有效,但我们可以做得更好。
Rails 提供用于生成路径和 URL 的助手方法。当您运行 bin/rails routes 时,您会看到 Prefix 列。此前缀与您可用于使用 Ruby 代码生成 URL 的助手相匹配。
Prefix Verb URI Pattern Controller#Action
products GET /products(.:format) products#index
product GET /products/:id(.:format) products#show
这些路由前缀为我们提供了以下助手
products_path生成"/products"products_url生成"https://:3000/products"product_path(1)生成"/products/1"product_url(1)生成"https://:3000/products/1"
_path 返回一个相对路径,浏览器理解该路径用于当前域。
_url 返回一个完整的 URL,包括协议、主机和端口。
URL 助手对于渲染将在浏览器外部查看的电子邮件很有用。
结合 link_to 助手,我们可以生成锚标签并使用 URL 助手在 Ruby 中干净地完成此操作。link_to 接受链接的显示内容 (product.name) 以及 href 属性的路径或 URL (product)。
让我们重构它以使用这些助手
<h1>Products</h1>
<div id="products">
<% @products.each do |product| %>
<div>
<%= link_to product.name, product_path(product.id) %>
</div>
<% end %>
</div>
10.5. 创建产品
到目前为止,我们必须在 Rails 控制台中创建产品,但让我们使其在浏览器中工作。
我们需要为创建创建两个动作
- 新产品表单以收集产品信息
- 控制器中的创建动作以保存产品并检查错误
让我们从控制器动作开始。
class ProductsController < ApplicationController
def index
@products = Product.all
end
def show
@product = Product.find(params[:id])
end
def new
@product = Product.new
end
end
new 动作实例化一个新的 Product,我们将使用它来显示表单字段。
我们可以更新 app/views/products/index.html.erb 以链接到新动作。
<h1>Products</h1>
<%= link_to "New product", new_product_path %>
<div id="products">
<% @products.each do |product| %>
<div>
<%= link_to product.name, product_path(product.id) %>
</div>
<% end %>
</div>
让我们创建 app/views/products/new.html.erb 来渲染此新 Product 的表单。
<h1>New product</h1>
<%= form_with model: @product do |form| %>
<div>
<%= form.label :name %>
<%= form.text_field :name %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>
<%= link_to "Cancel", products_path %>
在此视图中,我们使用 Rails form_with 助手生成一个 HTML 表单来创建产品。此助手使用表单构建器来处理 CSRF 令牌、根据提供的 model: 生成 URL,甚至根据模型定制提交按钮文本等事务。
如果您在浏览器中打开此页面并查看源代码,表单的 HTML 将如下所示
<form action="/products" accept-charset="UTF-8" method="post">
<input type="hidden" name="authenticity_token" value="UHQSKXCaFqy_aoK760zpSMUPy6TMnsLNgbPMABwN1zpW-Jx6k-2mISiF0ulZOINmfxPdg5xMyZqdxSW1UK-H-Q" autocomplete="off">
<div>
<label for="product_name">Name</label>
<input type="text" name="product[name]" id="product_name">
</div>
<div>
<input type="submit" name="commit" value="Create Product" data-disable-with="Create Product">
</div>
</form>
表单构建器已包含用于安全的 CSRF 令牌,配置了用于 UTF-8 支持的表单,设置了输入字段名称,甚至为提交按钮添加了禁用状态。
因为我们将一个新的 Product 实例传递给表单构建器,所以它自动生成了一个表单,配置为向 /products 发送 POST 请求,这是创建新记录的默认路由。
为了处理这个问题,我们首先需要在控制器中实现 create 动作。
class ProductsController < ApplicationController
def index
@products = Product.all
end
def show
@product = Product.find(params[:id])
end
def new
@product = Product.new
end
def create
@product = Product.new(product_params)
if @product.save
redirect_to @product
else
render :new, status: :unprocessable_entity
end
end
private
def product_params
params.expect(product: [ :name ])
end
end
10.5.1. 强参数
create 动作处理表单提交的数据,但需要进行安全过滤。这就是 product_params 方法发挥作用的地方。
在 product_params 中,我们告诉 Rails 检查 params 并确保存在一个名为 :product 的键,其值为参数数组。产品的唯一允许参数是 :name,Rails 将忽略任何其他参数。这保护了我们的应用程序免受可能试图攻击我们应用程序的恶意用户的影响。
10.5.2. 处理错误
将这些参数分配给新的 Product 后,我们可以尝试将其保存到数据库中。@product.save 告诉 Active Record 运行验证并将记录保存到数据库中。
如果 save 成功,我们希望重定向到新产品。当 redirect_to 被赋予一个 Active Record 对象时,Rails 会为该记录的 show 动作生成一个路径。
redirect_to @product
由于 @product 是一个 Product 实例,Rails 会将模型名称复数化,并在路径中包含对象的 ID,从而产生 "/products/2" 进行重定向。
当 save 不成功且记录无效时,我们希望重新渲染表单,以便用户可以修复无效数据。在 else 子句中,我们告诉 Rails render :new。Rails 知道我们在 Products 控制器中,因此它应该渲染 app/views/products/new.html.erb。由于我们已经在 create 中设置了 @product 变量,我们可以渲染该模板,并且即使它无法保存到数据库中,表单也将使用我们的 Product 数据填充。
我们还将 HTTP 状态设置为 422 Unprocessable Entity,以告诉浏览器此 POST 请求失败并相应地处理它。
10.6. 编辑产品
编辑记录的过程与创建记录非常相似。我们将有 edit 和 update 动作,而不是 new 和 create 动作。
让我们在控制器中实现它们,如下所示
class ProductsController < ApplicationController
def index
@products = Product.all
end
def show
@product = Product.find(params[:id])
end
def new
@product = Product.new
end
def create
@product = Product.new(product_params)
if @product.save
redirect_to @product
else
render :new, status: :unprocessable_entity
end
end
def edit
@product = Product.find(params[:id])
end
def update
@product = Product.find(params[:id])
if @product.update(product_params)
redirect_to @product
else
render :edit, status: :unprocessable_entity
end
end
private
def product_params
params.expect(product: [ :name ])
end
end
10.6.1. 提取局部变量
我们已经为创建新产品编写了一个表单。如果我们能将其用于编辑和更新,岂不美哉?我们可以,使用一个名为“局部变量”的功能,它允许您在多个地方重用视图。
我们可以将表单移动到名为 app/views/products/_form.html.erb 的文件中。文件名以下划线开头表示这是一个局部变量。
我们还希望将任何实例变量替换为局部变量,我们可以在渲染局部变量时定义它。我们将通过将 @product 替换为 product 来实现这一点。
我们还希望在表单中显示任何表单提交错误。
<%= form_with model: product do |form| %>
<% if form.object.errors.any? %>
<p class="error"><%= form.object.errors.full_messages.first %></p>
<% end %>
<div>
<%= form.label :name %>
<%= form.text_field :name %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>
使用局部变量允许局部变量在同一页面上多次使用不同的值。这对于渲染项目列表(如索引页)非常有用。
要在我们的 app/views/products/new.html.erb 视图中使用此局部变量,我们可以用渲染调用替换表单
<h1>New product</h1>
<%= render "form", product: @product %>
<%= link_to "Cancel", products_path %>
由于表单局部变量,编辑视图几乎与此完全相同。让我们创建 app/views/products/edit.html.erb,内容如下
<h1>Edit product</h1>
<%= render "form", product: @product %>
<%= link_to "Cancel", @product %>
要了解有关视图局部变量的更多信息,请查看Action View 指南。
现在我们可以向 app/views/products/show.html.erb 添加一个编辑链接
<h1><%= @product.name %></h1>
<%= link_to "Back", products_path %>
<%= link_to "Edit", edit_product_path(@product) %>
10.6.2. 前置动作
由于 edit 和 update 需要像 show 一样现有的数据库记录,我们可以将其去重到 before_action 中。
before_action 允许您提取动作之间共享的代码并在动作之前运行它。在上面的控制器代码中,@product = Product.find(params[:id]) 在三个不同的方法中定义。将此查询提取到名为 set_product 的 before_action 中可以清理每个动作的代码。
这是 DRY(不要重复自己)哲学的一个很好的实践示例。
class ProductsController < ApplicationController
before_action :set_product, only: %i[ show edit update ]
def index
@products = Product.all
end
def show
end
def new
@product = Product.new
end
def create
@product = Product.new(product_params)
if @product.save
redirect_to @product
else
render :new, status: :unprocessable_entity
end
end
def edit
end
def update
if @product.update(product_params)
redirect_to @product
else
render :edit, status: :unprocessable_entity
end
end
private
def set_product
@product = Product.find(params[:id])
end
def product_params
params.expect(product: [ :name ])
end
end
10.7. 删除产品
我们需要实现的最后一个功能是删除产品。我们将向 ProductsController 添加一个 destroy 动作来处理 DELETE /products/:id 请求。
将 destroy 添加到 before_action :set_product 允许我们以与其他动作相同的方式设置 @product 实例变量。
class ProductsController < ApplicationController
before_action :set_product, only: %i[ show edit update destroy ]
def index
@products = Product.all
end
def show
end
def new
@product = Product.new
end
def create
@product = Product.new(product_params)
if @product.save
redirect_to @product
else
render :new, status: :unprocessable_entity
end
end
def edit
end
def update
if @product.update(product_params)
redirect_to @product
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@product.destroy
redirect_to products_path
end
private
def set_product
@product = Product.find(params[:id])
end
def product_params
params.expect(product: [ :name ])
end
end
为了实现这一点,我们需要在 app/views/products/show.html.erb 中添加一个删除按钮
<h1><%= @product.name %></h1>
<%= link_to "Back", products_path %>
<%= link_to "Edit", edit_product_path(@product) %>
<%= button_to "Delete", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
button_to 生成一个表单,其中包含一个带有“删除”文本的按钮。单击此按钮后,它将提交表单,该表单将向 /products/:id 发出 DELETE 请求,从而触发控制器中的 destroy 动作。
turbo_confirm 数据属性告诉 Turbo JavaScript 库在提交表单之前要求用户确认。我们很快会深入探讨这一点。
11. 添加身份验证
任何人都可以编辑或删除产品,这不安全。让我们添加一些安全性,要求用户经过身份验证才能管理产品。
Rails 附带了一个身份验证生成器,我们可以使用它。它创建 User 和 Session 模型以及登录应用程序所需的控制器和视图。
回到终端,运行以下命令
$ bin/rails generate authentication
然后迁移数据库以添加 User 和 Session 表。
$ bin/rails db:migrate
打开 Rails 控制台以创建一个用户。
$ bin/rails console
使用 User.create! 方法在 Rails 控制台中创建一个用户。随意使用您自己的电子邮件和密码而不是示例。
store(dev)> User.create! email_address: "you@example.org", password: "s3cr3t", password_confirmation: "s3cr3t"
重启您的 Rails 服务器,以便它拾取生成器添加的 bcrypt gem。BCrypt 用于安全地哈希密码以进行身份验证。
$ bin/rails server
当您访问任何页面时,Rails 将提示输入用户名和密码。输入您在创建用户记录时使用的电子邮件和密码。
通过访问 https://:3000/products/new 试一试
如果您输入正确的用户名和密码,它将允许您通过。您的浏览器还将存储这些凭据以供将来请求使用,这样您就不必在每个页面视图中输入它。
11.1. 添加注销功能
要退出应用程序,我们可以在 app/views/layouts/application.html.erb 的顶部添加一个按钮。此布局是您放置要在每个页面中包含的 HTML 的地方,例如页眉或页脚。
在 <body> 内部添加一个小 <nav> 部分,其中包含指向 Home 的链接和注销按钮,并将 yield 包装在 <main> 标签中。
<!DOCTYPE html>
<html>
<!-- ... -->
<body>
<nav>
<%= link_to "Home", root_path %>
<%= button_to "Log out", session_path, method: :delete if authenticated? %>
</nav>
<main>
<%= yield %>
</main>
</body>
</html>
这只会在用户通过身份验证时显示注销按钮。点击后,它将向会话路径发送 DELETE 请求,这将注销用户。
11.2. 允许未经验证的访问
但是,我们商店的产品索引和显示页面应该对所有人开放。默认情况下,Rails 身份验证生成器将所有页面限制为仅限已验证用户访问。
为了允许访客查看产品,我们可以在控制器中允许未经身份验证的访问。
class ProductsController < ApplicationController
allow_unauthenticated_access only: %i[ index show ]
# ...
end
注销并访问产品索引和显示页面,查看它们在未经身份验证的情况下是否可访问。
11.3. 仅向已验证用户显示链接
由于只有登录用户才能创建产品,我们可以修改 app/views/products/index.html.erb 视图,使其仅在用户通过身份验证时显示新产品链接。
<%= link_to "New product", new_product_path if authenticated? %>
点击“注销”按钮,您会看到“新建”链接已隐藏。在 https://:3000/session/new 登录,您会在索引页上看到“新建”链接。
或者,您可以在导航栏中包含指向此路由的链接,以便在未通过身份验证时添加“登录”链接。
<%= link_to "Login", new_session_path unless authenticated? %>
您还可以更新 app/views/products/show.html.erb 视图上的编辑和删除链接,使其仅在经过身份验证时显示。
<h1><%= @product.name %></h1>
<%= link_to "Back", products_path %>
<% if authenticated? %>
<%= link_to "Edit", edit_product_path(@product) %>
<%= button_to "Delete", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
<% end %>
12. 缓存产品
有时缓存页面的特定部分可以提高性能。Rails 通过 Solid Cache 简化了这一过程,Solid Cache 是一个默认包含的基于数据库的缓存存储。
使用 cache 方法,我们可以将 HTML 存储在缓存中。让我们将 app/views/products/show.html.erb 中的标题缓存起来。
<% cache @product do %>
<h1><%= @product.name %></h1>
<% end %>
通过将 @product 传递给 cache,Rails 会为产品生成一个唯一的缓存键。Active Record 对象有一个 cache_key 方法,它返回一个类似 "products/1" 的字符串。视图中的 cache 助手将其与模板摘要结合起来,为该 HTML 创建一个唯一的键。
要在开发中启用缓存,请在终端中运行以下命令。
$ bin/rails dev:cache
当您访问产品的 show 动作(例如 /products/2)时,您会在 Rails 服务器日志中看到新的缓存行
Read fragment views/products/show:a5a585f985894cd27c8b3d49bb81de3a/products/1-20240918154439539125 (1.6ms)
Write fragment views/products/show:a5a585f985894cd27c8b3d49bb81de3a/products/1-20240918154439539125 (4.0ms)
我们第一次打开此页面时,Rails 将生成一个缓存键并询问缓存存储它是否存在。这就是 Read fragment 行。
由于这是第一个页面视图,缓存不存在,因此 HTML 被生成并写入缓存。我们可以在日志中看到这是 Write fragment 行。
刷新页面,您将看到日志中不再包含 Write fragment。
Read fragment views/products/show:a5a585f985894cd27c8b3d49bb81de3a/products/1-20240918154439539125 (1.3ms)
缓存条目是由上一个请求写入的,因此 Rails 在第二个请求时找到缓存条目。Rails 还会更改记录更新时的缓存键,以确保它永远不会渲染陈旧的缓存数据。
在使用 Rails 进行缓存指南中了解更多信息。
13. 使用 Action Text 的富文本字段
许多应用程序需要包含嵌入内容(即多媒体元素)的富文本,Rails 通过 Action Text 开箱即用地提供了此功能。
要使用 Action Text,您首先需要运行安装程序
$ bin/rails action_text:install
$ bundle install
$ bin/rails db:migrate
重启您的 Rails 服务器,以确保加载了所有新功能。
现在,让我们为产品添加一个富文本描述字段。
首先,将以下内容添加到 Product 模型中
class Product < ApplicationRecord
has_rich_text :description
validates :name, presence: true
end
现在可以更新表单,使其在提交按钮之前在 app/views/products/_form.html.erb 中包含一个用于编辑描述的富文本字段。
<%= form_with model: product do |form| %>
<%# ... %>
<div>
<%= form.label :description, style: "display: block" %>
<%= form.rich_textarea :description %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>
我们的控制器还需要在表单提交时允许这个新参数,所以我们将在 app/controllers/products_controller.rb 中更新允许的参数以包含 description
# Only allow a list of trusted parameters through.
def product_params
params.expect(product: [ :name, :description ])
end
我们还需要更新 show 视图以在 app/views/products/show.html.erb 中显示描述
<% cache @product do %>
<h1><%= @product.name %></h1>
<%= @product.description %>
<% end %>
Rails 生成的缓存键在视图修改时也会更改。这确保缓存与视图模板的最新版本保持同步。
创建一个新产品并添加粗体和斜体文本的描述。您会看到显示页面显示了格式化的文本,并且编辑产品会保留文本区域中的富文本。
查看Action Text 概述以了解更多信息。
14. 使用 Active Storage 上传文件
Action Text 建立在 Rails 的另一个功能 Active Storage 之上,该功能可以轻松上传文件。
尝试编辑产品并将图像拖到富文本编辑器中,然后更新记录。您会看到 Rails 上传此图像并将其渲染在富文本编辑器中。很酷,对吧?!
我们也可以直接使用 Active Storage。让我们为 Product 模型添加一个特色图片。
class Product < ApplicationRecord
has_one_attached :featured_image
has_rich_text :description
validates :name, presence: true
end
然后我们可以在提交按钮之前向我们的产品表单添加一个文件上传字段
<%= form_with model: product do |form| %>
<%# ... %>
<div>
<%= form.label :featured_image, style: "display: block" %>
<%= form.file_field :featured_image, accept: "image/*" %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>
在 app/controllers/products_controller.rb 中将 :featured_image 添加为允许的参数
# Only allow a list of trusted parameters through.
def product_params
params.expect(product: [ :name, :description, :featured_image ])
end
最后,我们希望在 app/views/products/show.html.erb 中显示产品的特色图片。将其添加到顶部。
<%= image_tag @product.featured_image if @product.featured_image.attached? %>
尝试为产品上传一张图片,保存后您会在显示页面上看到该图片。
有关更多详细信息,请查看Active Storage 概述。
15. 国际化 (I18n)
Rails 让您的应用程序轻松翻译成其他语言。
我们视图中的 translate 或 t 助手通过名称查找翻译并返回当前语言环境的文本。
在 app/views/products/index.html.erb 中,让我们更新标题标签以使用翻译。
<h1><%= t "hello" %></h1>
刷新页面,我们看到 Hello world 现在是标题文本。这是从哪里来的?
由于默认语言是英语,Rails 会在 config/locales/en.yml(在 rails new 期间创建)中查找语言环境下的匹配键。
en:
hello: "Hello world"
让我们在编辑器中创建一个新的西班牙语语言环境文件,并在 config/locales/es.yml 中添加一个翻译。
es:
hello: "Hola mundo"
我们需要告诉 Rails 使用哪个语言环境。最简单的选择是查找 URL 中的语言环境参数。我们可以在 app/controllers/application_controller.rb 中使用以下代码来实现这一点
class ApplicationController < ActionController::Base
# ...
around_action :switch_locale
def switch_locale(&action)
locale = params[:locale] || I18n.default_locale
I18n.with_locale(locale, &action)
end
end
这将运行每个请求,并在 params 中查找 locale,如果找不到则回退到默认 locale。它设置请求的 locale 并在请求完成后重置它。
- 访问 https://:3000/products?locale=en,您将看到英文翻译。
- 访问 https://:3000/products?locale=es,您将看到西班牙文翻译。
- 访问 https://:3000/products 时没有 locale 参数,它将回退到英文。
让我们更新索引标题以使用真实的翻译,而不是 "Hello world"。
<h1><%= t ".title" %></h1>
注意到 title 前面的 . 了吗?这告诉 Rails 使用相对语言环境查找。相对查找自动包含控制器和动作在键中,因此您无需每次都输入它们。对于带有英语语言环境的 .title,它将查找 en.products.index.title。
在 config/locales/en.yml 中,我们希望在 products 和 index 下添加 title 键,以匹配我们的控制器、视图和翻译名称。
en:
hello: "Hello world"
products:
index:
title: "Products"
在西班牙语本地化文件中,我们可以做同样的事情
es:
hello: "Hola mundo"
products:
index:
title: "Productos"
现在您将看到在查看英文区域设置时显示“Products”,在查看西班牙文区域设置时显示“Productos”。
了解更多关于 Rails 国际化 (I18n) API 的信息。
16. Action Mailer 和电子邮件通知
电子商务商店的一个常见功能是电子邮件订阅,以便在产品有库存时收到通知。现在我们已经了解了 Rails 的基础知识,让我们将此功能添加到我们的商店中。
16.1. 基本库存跟踪
首先,让我们向 Product 模型添加一个库存计数,以便我们可以跟踪库存。我们可以使用以下命令生成此迁移
$ bin/rails generate migration AddInventoryCountToProducts inventory_count:integer
这将生成一个迁移文件。打开它并添加一个默认值 0 以确保 inventory_count 永远不会是 nil
class AddInventoryCountToProducts < ActiveRecord::Migration[8.1]
def change
add_column :products, :inventory_count, :integer, default: 0
end
end
然后我们运行迁移。
$ bin/rails db:migrate
我们需要将库存计数添加到 app/views/products/_form.html.erb 中的产品表单中。
<%= form_with model: product do |form| %>
<%# ... %>
<div>
<%= form.label :inventory_count, style: "display: block" %>
<%= form.number_field :inventory_count %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>
控制器还需要将 :inventory_count 添加到允许的参数中。
def product_params
params.expect(product: [ :name, :description, :featured_image, :inventory_count ])
end
验证我们的库存计数永远不会是负数也会很有帮助,所以让我们在模型中也添加一个验证。
class Product < ApplicationRecord
has_one_attached :featured_image
has_rich_text :description
validates :name, presence: true
validates :inventory_count, numericality: { greater_than_or_equal_to: 0 }
end
有了这些更改,我们现在可以更新商店中产品的库存计数。
16.2. 为产品添加订阅者
为了通知用户产品已补货,我们需要跟踪这些订阅者。
让我们生成一个名为 Subscriber 的模型来存储这些电子邮件地址并将其与相应的产品关联起来。
这里我们没有为 email 指定类型,因为在迁移中没有给定类型时,rails 会自动默认为 string。
$ bin/rails generate model Subscriber product:belongs_to email
通过在上面包含 product:belongs_to,我们告诉 Rails 订阅者和产品具有一对多关系,这意味着订阅者“属于”一个 Product 实例。
接下来,打开生成的迁移文件(db/migrate/<timestamp>_create_subscribers.rb),就像我们对 Product 所做的那样。
class CreateSubscribers < ActiveRecord::Migration[8.1]
def change
create_table :subscribers do |t|
t.belongs_to :product, null: false, foreign_key: true
t.string :email
t.timestamps
end
end
end
这看起来与 Product 的迁移非常相似,主要的新内容是 belongs_to,它添加了一个 product_id 外键列。
然后运行新的迁移
$ bin/rails db:migrate
然而,一个产品可以有许多订阅者,所以我们然后将 has_many :subscribers, dependent: :destroy 添加到我们的产品模型中,以添加这两个模型之间关联的第二部分。这告诉 Rails 如何连接两个数据库表之间的查询。
class Product < ApplicationRecord
has_many :subscribers, dependent: :destroy
has_one_attached :featured_image
has_rich_text :description
validates :name, presence: true
validates :inventory_count, numericality: { greater_than_or_equal_to: 0 }
end
现在我们需要一个控制器来创建这些订阅者。让我们在 app/controllers/subscribers_controller.rb 中使用以下代码创建它
class SubscribersController < ApplicationController
allow_unauthenticated_access
before_action :set_product
def create
@product.subscribers.where(subscriber_params).first_or_create
redirect_to @product, notice: "You are now subscribed."
end
private
def set_product
@product = Product.find(params[:product_id])
end
def subscriber_params
params.expect(subscriber: [ :email ])
end
end
redirect_to 使用 notice: 参数设置一个“闪现”消息,告诉用户他们已订阅。
Flash 提供了一种在控制器动作之间传递临时数据的方法。您放入 flash 中的任何内容都将可用于下一个动作,然后被清除。flash 通常用于在控制器动作中设置消息(例如通知和警报),然后重定向到向用户显示消息的动作。
要显示闪现消息,让我们将闪现添加到 app/views/layouts/application.html.erb 的 body 内部
<html>
<!-- ... -->
<body>
<div class="notice"><%= flash[:notice] %></div>
<div class="alert"><%= flash[:alert] %></div>
<!-- ... -->
</body>
</html>
在Action Controller 概述中了解有关 Flash 的更多信息
要将用户订阅到特定产品,我们将使用嵌套路由,以便我们知道订阅者属于哪个产品。在 config/routes.rb 中将 resources :products 更改为以下内容
resources :products do
resources :subscribers, only: [ :create ]
end
在产品显示页面上,我们可以检查是否有库存并显示库存数量。否则,我们可以显示缺货消息并提供订阅表单,以便在有库存时收到通知。
在 app/views/products/_inventory.html.erb 中创建一个新的局部视图,并添加以下内容
<% if product.inventory_count.positive? %>
<p><%= product.inventory_count %> in stock</p>
<% else %>
<p>Out of stock</p>
<p>Email me when available.</p>
<%= form_with model: [product, Subscriber.new] do |form| %>
<%= form.email_field :email, placeholder: "you@example.com", required: true %>
<%= form.submit "Submit" %>
<% end %>
<% end %>
然后更新 app/views/products/show.html.erb 以在 cache 块之后渲染此局部视图。
<%= render "inventory", product: @product %>
16.3. 有货电子邮件通知
Action Mailer 是 Rails 的一项功能,允许您发送电子邮件。我们将使用它在产品有库存时通知订阅者。
我们可以使用以下命令生成邮件程序
$ bin/rails g mailer Product in_stock
这在 app/mailers/product_mailer.rb 中生成了一个带有 in_stock 方法的类。
更新此方法以将邮件发送到订阅者的电子邮件地址。
class ProductMailer < ApplicationMailer
# Subject can be set in your I18n file at config/locales/en.yml
# with the following lookup:
#
# en.product_mailer.in_stock.subject
#
def in_stock
@product = params[:product]
mail to: params[:subscriber].email
end
end
邮件程序生成器还在我们的视图文件夹中生成了两个电子邮件模板:一个用于 HTML,一个用于文本。我们可以更新它们以包含消息和指向产品的链接。
将 app/views/product_mailer/in_stock.html.erb 更改为
<h1>Good news!</h1>
<p><%= link_to @product.name, product_url(@product) %> is back in stock.</p>
并将 app/views/product_mailer/in_stock.text.erb 更改为
Good news!
<%= @product.name %> is back in stock.
<%= product_url(@product) %>
我们在邮件程序中使用 product_url 而不是 product_path,因为电子邮件客户端需要知道完整的 URL 才能在点击链接时在浏览器中打开。
我们可以通过打开 Rails 控制台并加载产品和订阅者来测试电子邮件
store(dev)> product = Product.first
store(dev)> subscriber = product.subscribers.find_or_create_by(email: "subscriber@example.org")
store(dev)> ProductMailer.with(product: product, subscriber: subscriber).in_stock.deliver_later
您会看到它在日志中打印出一封电子邮件。
ProductMailer#in_stock: processed outbound mail in 63.0ms
Delivered mail 66a3a9afd5d4a_108b04a4c41443@local.mail (33.1ms)
Date: Fri, 26 Jul 2024 08:50:39 -0500
From: from@example.com
To: subscriber@example.com
Message-ID: <66a3a9afd5d4a_108b04a4c41443@local.mail>
Subject: In stock
Mime-Version: 1.0
Content-Type: multipart/alternative;
boundary="--==_mimepart_66a3a9afd235e_108b04a4c4136f";
charset=UTF-8
Content-Transfer-Encoding: 7bit
----==_mimepart_66a3a9afd235e_108b04a4c4136f
Content-Type: text/plain;
charset=UTF-8
Content-Transfer-Encoding: 7bit
Good news!
T-Shirt is back in stock.
https://:3000/products/1
----==_mimepart_66a3a9afd235e_108b04a4c4136f
Content-Type: text/html;
charset=UTF-8
Content-Transfer-Encoding: 7bit
<!-- BEGIN app/views/layouts/mailer.html.erb --><!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style>
/* Email styles need to be inline */
</style>
</head>
<body>
<!-- BEGIN app/views/product_mailer/in_stock.html.erb --><h1>Good news!</h1>
<p><a href="https://:3000/products/1">T-Shirt</a> is back in stock.</p>
<!-- END app/views/product_mailer/in_stock.html.erb -->
</body>
</html>
<!-- END app/views/layouts/mailer.html.erb -->
----==_mimepart_66a3a9afd235e_108b04a4c4136f--
Performed ActionMailer::MailDeliveryJob (Job ID: 5e2bd5f2-f54f-4088-ace3-3f6eb15aaf46) from Async(default) in 111.34ms
为了触发这些电子邮件,我们可以在 Product 模型中使用回调,以便在库存数量从 0 变为正数时随时发送电子邮件。
class Product < ApplicationRecord
has_many :subscribers, dependent: :destroy
has_one_attached :featured_image
has_rich_text :description
validates :name, presence: true
validates :inventory_count, numericality: { greater_than_or_equal_to: 0 }
after_update_commit :notify_subscribers, if: :back_in_stock?
def back_in_stock?
inventory_count_previously_was.zero? && inventory_count.positive?
end
def notify_subscribers
subscribers.each do |subscriber|
ProductMailer.with(product: self, subscriber: subscriber).in_stock.deliver_later
end
end
end
after_update_commit 是一个 Active Record 回调,它在更改保存到数据库后触发。if: :back_in_stock? 告诉回调仅在 back_in_stock? 方法返回 true 时运行。
Active Record 会跟踪属性的更改,因此 back_in_stock? 使用 inventory_count_previously_was 检查 inventory_count 的先前值。然后我们可以将其与当前库存数量进行比较,以确定产品是否已补货。
notify_subscribers 使用 Active Record 关联查询 subscribers 表以获取此特定产品的所有订阅者,然后将 in_stock 电子邮件排队发送给他们每个人。
16.4. 提取一个关注点
Product 模型现在有相当多的代码用于处理通知。为了更好地组织我们的代码,我们可以将其提取到 ActiveSupport::Concern 中。Concern 是一个 Ruby 模块,带有一些语法糖,使其使用起来更简单。
首先让我们创建 Notifications 模块。
在 app/models/product/notifications.rb 创建一个文件,内容如下
module Product::Notifications
extend ActiveSupport::Concern
included do
has_many :subscribers, dependent: :destroy
after_update_commit :notify_subscribers, if: :back_in_stock?
end
def back_in_stock?
inventory_count_previously_was.zero? && inventory_count.positive?
end
def notify_subscribers
subscribers.each do |subscriber|
ProductMailer.with(product: self, subscriber: subscriber).in_stock.deliver_later
end
end
end
当您在一个类中包含一个模块时,included 块中的任何代码都会像它是该类的一部分一样运行。同时,模块中定义的方法会成为您可以调用该类对象(实例)的常规方法。
现在触发通知的代码已提取到 Notifications 模块中,Product 模型可以简化为包含 Notifications 模块。
class Product < ApplicationRecord
include Notifications
has_one_attached :featured_image
has_rich_text :description
validates :name, presence: true
validates :inventory_count, numericality: { greater_than_or_equal_to: 0 }
end
关注点是组织 Rails 应用程序功能的好方法。随着您向 Product 添加更多功能,该类将变得混乱。相反,我们可以使用关注点将每个功能提取到自包含的模块中,例如 Product::Notifications,它包含处理订阅者和发送通知的所有功能。
将代码提取到关注点也有助于使功能可重用。例如,我们可以引入一个也需要订阅者通知的新模型。此模块可以在多个模型中使用以提供相同的功能。
16.5. 取消订阅链接
订阅者可能希望在某个时候取消订阅,所以接下来让我们构建它。
首先,我们需要一个用于取消订阅的路由,它将是我们在电子邮件中包含的 URL。
Rails.application.routes.draw do
# ...
resources :products do
resources :subscribers, only: [ :create ]
end
resource :unsubscribe, only: [ :show ]
取消订阅路由在顶层添加,并使用单数 resource 来处理像 /unsubscribe?token=xyz 这样的路由。
Active Record 有一个名为 generates_token_for 的功能,可以生成唯一的令牌以查找用于不同目的的数据库记录。我们可以使用它来生成一个唯一的取消订阅令牌,用于电子邮件的取消订阅 URL。
class Subscriber < ApplicationRecord
belongs_to :product
generates_token_for :unsubscribe
end
我们的控制器将首先从 URL 中的令牌查找 Subscriber 记录。一旦找到订阅者,它将销毁记录并重定向到主页。创建 app/controllers/unsubscribes_controller.rb 并添加以下代码
class UnsubscribesController < ApplicationController
allow_unauthenticated_access
before_action :set_subscriber
def show
@subscriber&.destroy
redirect_to root_path, notice: "Unsubscribed successfully."
end
private
def set_subscriber
@subscriber = Subscriber.find_by_token_for(:unsubscribe, params[:token])
end
end
最后但同样重要的是,让我们将取消订阅链接添加到我们的电子邮件模板中。
在 app/views/product_mailer/in_stock.html.erb 中,添加一个 link_to
<h1>Good news!</h1>
<p><%= link_to @product.name, product_url(@product) %> is back in stock.</p>
<%= link_to "Unsubscribe", unsubscribe_url(token: params[:subscriber].generate_token_for(:unsubscribe)) %>
在 app/views/product_mailer/in_stock.text.erb 中,以纯文本形式添加 URL
Good news!
<%= @product.name %> is back in stock.
<%= product_url(@product) %>
Unsubscribe: <%= unsubscribe_url(token: params[:subscriber].generate_token_for(:unsubscribe)) %>
当点击取消订阅链接时,订阅者记录将从数据库中删除。控制器还会安全地处理无效或过期的令牌,而不会引发任何错误。
使用 Rails 控制台发送另一封电子邮件并测试日志中的取消订阅链接。
17. 添加 CSS 和 JavaScript
CSS 和 JavaScript 是构建 Web 应用程序的核心,所以让我们学习如何在 Rails 中使用它们。
17.1. Propshaft
Rails 的资产管道称为 Propshaft。它获取您的 CSS、JavaScript、图像和其他资产并将其提供给您的浏览器。在生产环境中,Propshaft 会跟踪您资产的每个版本,以便可以缓存它们以加快您的页面速度。查看资产管道指南以了解有关其工作原理的更多信息。
让我们修改 app/assets/stylesheets/application.css 并将字体更改为 sans-serif。
body {
font-family: Arial, Helvetica, sans-serif;
padding: 1rem;
}
nav {
justify-content: flex-end;
display: flex;
font-size: 0.875em;
gap: 0.5rem;
max-width: 1024px;
margin: 0 auto;
padding: 1rem;
}
nav a {
display: inline-block;
}
main {
max-width: 1024px;
margin: 0 auto;
}
.alert,
.error {
color: red;
}
.notice {
color: green;
}
section.product {
display: flex;
gap: 1rem;
flex-direction: row;
}
section.product img {
border-radius: 8px;
flex-basis: 50%;
max-width: 50%;
}
然后我们将更新 app/views/products/show.html.erb 以使用这些新样式。
<p><%= link_to "Back", products_path %></p>
<section class="product">
<%= image_tag @product.featured_image if @product.featured_image.attached? %>
<section class="product-info">
<% cache @product do %>
<h1><%= @product.name %></h1>
<%= @product.description %>
<% end %>
<%= render "inventory", product: @product %>
<% if authenticated? %>
<%= link_to "Edit", edit_product_path(@product) %>
<%= button_to "Delete", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
<% end %>
</section>
</section>
刷新页面,您会看到 CSS 已应用。
17.2. 导入映射
Rails 默认使用导入映射来处理 JavaScript。这允许您编写现代 JavaScript 模块而无需构建步骤。
您可以在 config/importmap.rb 中找到 JavaScript 别名。此文件将 JavaScript 包名称与源文件映射,用于在浏览器中生成 importmap 标签。
# Pin npm packages by running ./bin/importmap
pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
pin "trix"
pin "@rails/actiontext", to: "actiontext.esm.js"
每个 pin 都将 JavaScript 包名(例如 "@hotwired/turbo-rails")映射到特定的文件或 URL(例如 "turbo.min.js")。pin_all_from 将目录中的所有文件(例如 app/javascript/controllers)映射到命名空间(例如 "controllers")。
导入映射保持设置干净且最小,同时仍支持现代 JavaScript 功能。
这些 JavaScript 文件已经在我们的导入映射中了吗?它们是 Rails 默认使用的一个前端框架,称为 Hotwire。
17.3. Hotwire
Hotwire 是一个 JavaScript 框架,旨在充分利用服务器端生成的 HTML。它由 3 个核心组件组成
- Turbo 处理导航、表单提交、页面组件和更新,而无需编写任何自定义 JavaScript。
- Stimulus 提供了一个框架,当您需要自定义 JavaScript 来向页面添加功能时。
- Native 允许您通过嵌入您的 Web 应用程序并逐步使用原生移动功能来增强它,从而创建混合移动应用程序。
我们还没有编写任何 JavaScript,但我们一直在前端使用 Hotwire。例如,您创建的用于添加和编辑产品的表单是由 Turbo 提供支持的。
在资产管道和在 Rails 中使用 JavaScript指南中了解更多信息。
18. 测试
Rails 附带了一个强大的测试套件。让我们编写一个测试,以确保产品补货时发送了正确数量的电子邮件。
18.1. 测试夹具
当您使用 Rails 生成模型时,它会自动在 test/fixtures 目录中创建一个相应的 fixture 文件。
夹具是预定义的数据集,它们在运行测试之前填充您的测试数据库。它们允许您定义具有易于记忆名称的记录,从而简化在测试中访问它们。
此文件默认为空 - 您需要用夹具填充它以进行测试。
让我们用以下内容更新 test/fixtures/products.yml 中的产品夹具文件
tshirt:
name: T-Shirt
inventory_count: 15
对于订阅者,让我们将这两个夹具添加到 test/fixtures/subscribers.yml
david:
product: tshirt
email: david@example.org
chris:
product: tshirt
email: chris@example.org
您会注意到我们可以在这里按名称引用 Product 夹具。Rails 会自动在数据库中为我们关联它,这样我们就不必在测试中管理记录 ID 和关联。
这些夹具将在我们运行测试套件时自动插入到数据库中。
18.2. 测试电子邮件
在 test/models/product_test.rb 中,让我们添加一个测试
require "test_helper"
class ProductTest < ActiveSupport::TestCase
include ActionMailer::TestHelper
test "sends email notifications when back in stock" do
product = products(:tshirt)
# Set product out of stock
product.update(inventory_count: 0)
assert_emails 2 do
product.update(inventory_count: 99)
end
end
end
让我们分析一下这个测试正在做什么。
首先,我们包含 Action Mailer 测试助手,以便我们可以在测试期间监控发送的电子邮件。
tshirt 夹具使用 products() 夹具助手加载,并返回该记录的 Active Record 对象。每个夹具在测试套件中生成一个助手,以便轻松按名称引用夹具,因为它们的数据库 ID 可能每次运行都不同。
然后我们通过将其库存更新为 0 来确保 T恤缺货。
接下来,我们使用 assert_emails 确保块内的代码生成了 2 封电子邮件。为了触发电子邮件,我们在块内更新产品的库存计数。这会触发 Product 模型中的 notify_subscribers 回调以发送电子邮件。一旦执行完成,assert_emails 会计算电子邮件并确保它与预期计数匹配。
我们可以使用 bin/rails test 运行测试套件,或者通过传递文件名来运行单个测试文件。
$ bin/rails test test/models/product_test.rb
Running 1 tests in a single process (parallelization threshold is 50)
Run options: --seed 3556
# Running:
.
Finished in 0.343842s, 2.9083 runs/s, 5.8166 assertions/s.
1 runs, 2 assertions, 0 failures, 0 errors, 0 skips
我们的测试通过了!
Rails 还在 test/mailers/product_mailer_test.rb 生成了一个 ProductMailer 的示例测试。让我们更新它,使其也通过。
require "test_helper"
class ProductMailerTest < ActionMailer::TestCase
test "in_stock" do
mail = ProductMailer.with(product: products(:tshirt), subscriber: subscribers(:david)).in_stock
assert_equal "In stock", mail.subject
assert_equal [ "david@example.org" ], mail.to
assert_equal [ "from@example.com" ], mail.from
assert_match "Good news!", mail.body.encoded
end
end
现在让我们运行整个测试套件,并确保所有测试都通过。
$ bin/rails test
Running 2 tests in a single process (parallelization threshold is 50)
Run options: --seed 16302
# Running:
..
Finished in 0.665856s, 3.0037 runs/s, 10.5128 assertions/s.
2 runs, 7 assertions, 0 failures, 0 errors, 0 skips
您可以将其作为起点,继续构建具有应用程序功能完整覆盖的测试套件。
了解有关测试 Rails 应用程序的更多信息
19. 使用 RuboCop 保持代码格式一致
编写代码时,我们有时会使用不一致的格式。Rails 带有一个名为 RuboCop 的 linter,它有助于保持我们的代码格式一致。
我们可以通过运行以下命令检查代码的一致性:
$ bin/rubocop
这将打印出任何违规行为并告诉你它们是什么。
Inspecting 53 files
.....................................................
53 files inspected, no offenses detected
RuboCop 可以使用 --autocorrect 标志(或其短版本 -a)自动修复违规行为。
$ bin/rubocop -a
20. 安全
Rails 包含 Brakeman gem,用于检查应用程序的安全问题——可能导致会话劫持、会话固定或重定向等攻击的漏洞。
运行 bin/brakeman,它将分析您的应用程序并输出报告。
$ bin/brakeman
Loading scanner...
...
== Overview ==
Controllers: 6
Models: 6
Templates: 15
Errors: 0
Security Warnings: 0
== Warning Types ==
No warnings found
了解更多关于 保护 Rails 应用程序 的信息
21. 使用 GitHub Actions 进行持续集成
Rails 应用程序会生成一个 .github 文件夹,其中包含预先编写的 GitHub Actions 配置,该配置会运行 rubocop、brakeman 和我们的测试套件。
当我们启用 GitHub Actions 并将代码推送到 GitHub 仓库时,它会自动运行这些步骤并报告每个步骤的成功或失败。这使我们能够监控代码更改中的缺陷和问题,并确保我们的工作质量始终如一。
22. 部署到生产环境
现在是有趣的部分:让我们部署您的应用程序。
Rails 带有一个名为 Kamal 的部署工具,我们可以使用它将应用程序直接部署到服务器。Kamal 使用 Docker 容器来运行您的应用程序并实现零停机部署。
默认情况下,Rails 附带了一个生产就绪的 Dockerfile,Kamal 将使用它来构建 Docker 镜像,创建一个包含所有依赖项和配置的容器化应用程序版本。这个 Dockerfile 使用 Thruster 在生产环境中高效地压缩和提供资产。
要使用 Kamal 进行部署,我们需要:
- 一台运行 Ubuntu LTS 且内存为 1GB 或更高的服务器。服务器应运行具有长期支持(LTS)版本的 Ubuntu 操作系统,以便接收定期的安全和错误修复。Hetzner、DigitalOcean 和其他托管服务提供商提供服务器以供入门。
- 一个 Docker Hub 帐户和访问令牌。Docker Hub 存储应用程序的镜像,以便可以在服务器上下载和运行。
在 Docker Hub 上,为您的应用程序镜像创建一个仓库。使用“store”作为仓库名称。
打开 config/deploy.yml 并将 192.168.0.1 替换为您的服务器 IP 地址,将 your-user 替换为您的 Docker Hub 用户名。
# Name of your application. Used to uniquely configure containers.
service: store
# Name of the container image.
image: your-user/store
# Deploy to these servers.
servers:
web:
- 192.168.0.1
# Credentials for your image host.
registry:
# Specify the registry server, if you're not using Docker Hub
# server: registry.digitalocean.com / ghcr.io / ...
username: your-user
在 proxy: 部分下,您还可以添加一个域以启用应用程序的 SSL。确保您的 DNS 记录指向服务器,Kamal 将使用 LetsEncrypt 为该域颁发 SSL 证书。
proxy:
ssl: true
host: app.example.com
在 Docker 网站上创建一个访问令牌,并赋予读写权限,以便 Kamal 可以推送您的应用程序的 Docker 镜像。
然后,在终端中导出访问令牌,以便 Kamal 可以找到它。
export KAMAL_REGISTRY_PASSWORD=your-access-token
运行以下命令以设置您的服务器并首次部署您的应用程序。
$ bin/kamal setup
恭喜!您的新 Rails 应用程序已上线并投入生产!
要查看您的新 Rails 应用程序的实际运行情况,请打开浏览器并输入您服务器的 IP 地址。您应该会看到您的商店正在运行。
此后,当您更改应用程序并想将其推送到生产环境时,您可以运行以下命令:
$ bin/kamal deploy
22.1. 添加用户到生产环境
要在生产环境中创建和编辑产品,我们需要在生产数据库中有一个用户记录。
您可以使用 Kamal 打开生产 Rails 控制台。
$ bin/kamal console
store(prod)> User.create!(email_address: "you@example.org", password: "s3cr3t", password_confirmation: "s3cr3t")
现在您可以使用此电子邮件和密码登录生产环境并管理产品。
22.2. 使用 Solid Queue 进行后台作业
后台作业允许您在单独的进程中异步地在幕后运行任务,防止它们中断用户体验。想象一下向 10,000 名收件人发送库存电子邮件。这可能需要一段时间,因此我们可以将该任务卸载到后台作业以保持 Rails 应用程序的响应性。
在开发环境中,Rails 使用 :async 队列适配器通过 ActiveJob 处理后台作业。Async 将待处理作业存储在内存中,但会在重启时丢失待处理作业。这对于开发来说很好,但对于生产来说则不然。
为了使后台作业更健壮,Rails 在生产环境中使用 solid_queue。Solid Queue 将作业存储在数据库中并在单独的进程中执行它们。
Solid Queue 通过将 SOLID_QUEUE_IN_PUMA: true 环境变量添加到 config/deploy.yml 中,为我们的生产 Kamal 部署启用。这会告诉我们的 Web 服务器 Puma 自动启动和停止 Solid Queue 进程。
当使用 Action Mailer 的 deliver_later 发送电子邮件时,这些电子邮件将被发送到 Active Job 进行后台发送,这样它们就不会延迟 HTTP 请求。在生产环境中,使用 Solid Queue,电子邮件将在后台发送,如果发送失败会自动重试,并且在重启期间作业会安全地保存在数据库中。
23. 接下来是什么?
恭喜您构建并部署了您的第一个 Rails 应用程序!
接下来,请按照注册和设置教程继续学习。
我们还建议阅读其他 Ruby on Rails 指南以了解更多信息
祝您构建愉快!