更多内容请访问 rubyonrails.org:

1 指南假设

本指南是为想要从头开始创建 Rails 应用程序的初学者设计的。它不假定您有任何使用 Rails 的经验。

Rails 是一个在 Ruby 编程语言上运行的 Web 应用程序框架。如果您以前没有使用过 Ruby,那么直接深入学习 Rails 会发现学习曲线非常陡峭。有几个精心策划的在线资源列表可供学习 Ruby

请注意,一些资源虽然仍然很棒,但涵盖了旧版本的 Ruby,可能不包括您在使用 Rails 的日常开发中会看到的某些语法。

2 什么是 Rails?

Rails 是用 Ruby 编程语言编写的 Web 应用程序开发框架。它旨在通过对每个开发人员入门所需的假设来简化 Web 应用程序的编程。它允许您编写更少的代码,同时实现比许多其他语言和框架更多的功能。经验丰富的 Rails 开发人员还报告说,它使 Web 应用程序开发更有趣。

Rails 是有主见的软件。它假设有一种“最佳”方式来做事,它旨在鼓励这种方式——在某些情况下,甚至会阻止使用替代方法。如果您学习“Rails 之道”,您可能会发现生产力大幅提高。如果您坚持将来自其他语言的旧习惯带到 Rails 开发中,并尝试使用在其他地方学到的模式,您可能不会有那么愉快的体验。

Rails 的理念包括两个主要指导原则

  • **不要重复自己:** DRY 是软件开发的一项原则,它指出“系统中每条知识都必须有一个单一、明确、权威的表示”。通过避免一遍又一遍地写相同的信息,我们的代码更易于维护、更具可扩展性,并且错误更少。
  • **惯例优于配置:** Rails 对 Web 应用程序中许多事物的最佳做法有自己的想法,并且默认为此惯例集,而不是要求您通过无数配置文件来指定细枝末节。

3 创建新的 Rails 项目

您可以使用预配置的 Dev Container 开发环境创建新的 Rails 应用程序。这是开始使用 Rails 的最快方法。有关说明,请参阅 使用 Dev Container 入门

阅读本指南的最佳方式是按步骤进行。所有步骤对于运行此示例应用程序都是必不可少的,不需要任何其他代码或步骤。

通过按照本指南进行操作,您将创建一个名为 blog 的 Rails 项目,一个(非常)简单的网络日志。在开始构建应用程序之前,您需要确保已安装 Rails 本身。

以下示例使用 $ 来表示类 UNIX 操作系统中的终端提示符,尽管它可能已被自定义为以不同的方式显示。如果您使用的是 Windows,您的提示符将类似于 C:\source_code>

3.1 安装 Rails

在安装 Rails 之前,您应该检查以确保您的系统已安装适当的先决条件。这些包括

  • Ruby
  • SQLite3

3.1.1 安装 Ruby

打开命令行提示符。在 macOS 上,打开 Terminal.app;在 Windows 上,从“开始”菜单中选择“运行”,然后键入 cmd.exe。以美元符号 $ 开头的任何命令都应该在命令行中运行。验证您是否安装了当前版本的 Ruby

$ ruby --version
ruby 3.2.0

Rails 需要 Ruby 3.2.0 或更高版本。最好使用最新版本的 Ruby。如果返回的版本号小于该数字(例如 2.3.7 或 1.8.7),则需要安装新的 Ruby 版本。

要在 Windows 上安装 Rails,您首先需要安装 Ruby Installer

有关大多数操作系统的更多安装方法,请查看 ruby-lang.org

3.1.2 安装 SQLite3

您还需要安装 SQLite3 数据库。许多流行的类 UNIX 操作系统都附带了可接受的 SQLite3 版本。其他用户可以在 SQLite3 网站 上找到安装说明。

验证它是否已正确安装并位于您的 PATH 加载中

$ sqlite3 --version

该程序应报告其版本。

3.1.3 安装 Rails

要安装 Rails,请使用 RubyGems 提供的 gem install 命令

$ gem install rails

要验证您是否已正确安装所有内容,您应该能够在新的终端中运行以下命令

$ rails --version
Rails 8.0.0

如果它显示类似“Rails 8.0.0”的内容,则您已准备好继续。

3.2 创建博客应用程序

Rails 附带了许多称为生成器的脚本,这些脚本旨在通过创建开始特定任务所需的一切来简化您的开发工作。其中之一是新的应用程序生成器,它将为您提供一个全新的 Rails 应用程序的基础,这样您就不必自己编写它。

要使用此生成器,请打开终端,导航到您有权创建文件的目录,然后运行

$ rails new blog

这将在blog目录中创建一个名为Blog的Rails应用程序,并使用bundle install安装Gemfile中已提及的gem依赖项。

您可以通过运行rails new --help查看Rails应用程序生成器接受的所有命令行选项。

创建博客应用程序后,切换到其文件夹

$ cd blog

blog目录将包含许多生成的 文件和文件夹,它们构成了Rails应用程序的结构。本教程中的大部分工作将在app文件夹中进行,但以下是对Rails默认创建的每个文件和文件夹功能的基本概述

文件/文件夹 目的
app/ 包含应用程序的控制器、模型、视图、助手、邮件程序、作业和资产。您将在本指南的其余部分中专注于此文件夹。
bin/ 包含启动应用程序的rails脚本,并且可以包含您用来设置、更新、部署或运行应用程序的其他脚本。
config/ 包含应用程序的路由、数据库等的配置。这将在配置Rails应用程序中详细介绍。
config.ru 用于启动应用程序的基于Rack的服务器的Rack配置。有关Rack的更多信息,请参阅Rack网站
db/ 包含您的当前数据库模式,以及数据库迁移。
Dockerfile Docker的配置文件。
Gemfile
Gemfile.lock
这些文件允许您指定Rails应用程序所需的gem依赖项。这些文件由Bundler gem使用。有关Bundler的更多信息,请参阅Bundler网站
lib/ 应用程序的扩展模块。
log/ 应用程序日志文件。
public/ 包含静态文件和编译的资产。当您的应用程序运行时,此目录将按原样公开。
Rakefile 此文件定位并加载可以从命令行运行的任务。任务定义是在Rails的各个组件中定义的。您应该通过将文件添加到应用程序的lib/tasks目录中来添加自己的任务,而不是更改Rakefile
README.md 这是一个有关应用程序的简短说明手册。您应该编辑此文件以告诉其他人您的应用程序的功能、如何设置它等。
script/ 包含一次性或通用脚本基准测试
storage/ 磁盘服务的Active Storage文件。这将在Active Storage概述中介绍。
test/ 单元测试、夹具和其他测试装置。这些将在测试Rails应用程序中介绍。
tmp/ 临时文件(如缓存和pid文件)。
vendor/ 所有第三方代码的位置。在典型的Rails应用程序中,这包括供应商的gem。
.dockerignore 此文件告诉Docker哪些文件不应复制到容器中。
.gitattributes 此文件为git存储库中的特定路径定义元数据。此元数据可供git和其他工具使用,以增强其行为。有关更多信息,请参阅gitattributes文档
.github/ 包含特定于GitHub的文件。
.gitignore 此文件告诉git哪些文件(或模式)应该忽略。有关忽略文件的更多信息,请参阅GitHub - 忽略文件
.rubocop.yml 此文件包含RuboCop的配置。
.ruby-version 此文件包含默认的Ruby版本。

4 您好,Rails!

首先,让我们快速地在屏幕上显示一些文本。为此,您需要使Rails应用程序服务器运行。

4.1 启动Web服务器

实际上,您已经拥有一个可运行的Rails应用程序。要查看它,您需要在开发机器上启动一个Web服务器。您可以在blog目录中运行以下命令来实现这一点

$ bin/rails server

如果您使用的是Windows,则必须将bin文件夹下的脚本直接传递给Ruby解释器,例如ruby bin\rails server

JavaScript 资产压缩需要您在系统上拥有一个JavaScript运行时,在没有运行时的情况下,您将在资产压缩期间看到execjs错误。通常macOS和Windows都安装了JavaScript运行时。therubyrhino是JRuby用户的推荐运行时,默认情况下,它会添加到JRuby下生成的应用程序中的Gemfile中。您可以在ExecJS中调查所有受支持的运行时。

这将启动Puma,一个默认情况下与Rails一起分发的Web服务器。要查看您的应用程序,请打开浏览器窗口并导航到https://127.0.0.1:3000。您应该看到Rails默认信息页面

Rails startup page screenshot

当您想要停止Web服务器时,请在运行它的终端窗口中按下Ctrl+C。在开发环境中,Rails通常不需要您重新启动服务器;您在文件中所做的更改将由服务器自动拾取。

Rails启动页面是新Rails应用程序的冒烟测试:它确保您的软件配置正确,足以提供页面。

4.2 说“您好”,Rails

要让Rails说“您好”,您至少需要创建一个路由、一个带有操作控制器和一个视图。路由将请求映射到控制器操作。控制器操作执行处理请求所需的必要工作,并为视图准备任何数据。视图以所需的格式显示数据。

在实现方面:路由是在Ruby DSL(领域特定语言)中编写的规则。控制器是Ruby类,它们的方法是操作。视图是模板,通常以HTML和Ruby的混合形式编写。

让我们从在路由文件中添加路由开始,config/routes.rb,在Rails.application.routes.draw块的顶部

Rails.application.routes.draw do
  get "/articles", to: "articles#index"

  # For details on the DSL available within this file, see https://guides.rubyonrails.net.cn/routing.html
end

上面的路由声明GET /articles请求映射到ArticlesControllerindex操作。

要创建ArticlesController及其index操作,我们将运行控制器生成器(使用--skip-routes选项,因为我们已经有一个合适的路由)

$ bin/rails generate controller Articles index --skip-routes

Rails将为您创建几个文件

create  app/controllers/articles_controller.rb
invoke  erb
create    app/views/articles
create    app/views/articles/index.html.erb
invoke  test_unit
create    test/controllers/articles_controller_test.rb
invoke  helper
create    app/helpers/articles_helper.rb
invoke    test_unit

其中最重要的文件是控制器文件,app/controllers/articles_controller.rb。让我们来看看它

class ArticlesController < ApplicationController
  def index
  end
end

index操作是空的。当操作没有显式地呈现视图(或以其他方式触发HTTP响应)时,Rails将自动呈现一个视图,该视图与控制器和操作的名称匹配。惯例优于配置!视图位于app/views目录中。因此,index操作将默认呈现app/views/articles/index.html.erb

让我们打开app/views/articles/index.html.erb,并将内容替换为

<h1>Hello, Rails!</h1>

如果您之前停止了Web服务器以运行控制器生成器,请使用bin/rails server重新启动它。现在访问https://127.0.0.1:3000/articles,您将看到我们的文本显示出来!

4.3 设置应用程序主页

目前,https://127.0.0.1:3000仍然显示带有Ruby on Rails徽标的页面。让我们也显示我们的“您好,Rails!”文本在https://127.0.0.1:3000上。为此,我们将添加一个路由,将应用程序的根路径映射到相应的控制器和操作。

让我们打开config/routes.rb,并将以下root路由添加到Rails.application.routes.draw块的顶部

Rails.application.routes.draw do
  root "articles#index"

  get "/articles", to: "articles#index"
end

现在,当我们访问https://127.0.0.1:3000时,我们可以看到我们的“您好,Rails!”文本,这确认了root路由也映射到ArticlesControllerindex操作。

要了解有关路由的更多信息,请参阅Rails Routing from the Outside In

5 自动加载

Rails应用程序使用require来加载应用程序代码。

您可能已经注意到,ArticlesController继承自ApplicationController,但app/controllers/articles_controller.rb没有类似于

require "application_controller" # DON'T DO THIS.

应用程序类和模块在任何地方都可用,您不需要也不应该使用require加载app下的任何内容。此功能称为自动加载,您可以在自动加载和重新加载常量中了解更多信息。

您只需要require调用来处理两种情况

  • 加载lib目录下的文件。
  • 加载Gemfilerequire: false的gem依赖项。

6 MVC 和您

到目前为止,我们已经讨论了路由、控制器、操作和视图。所有这些都是遵循MVC(模型-视图-控制器)模式的Web应用程序的典型组成部分。MVC是一种设计模式,它将应用程序的职责划分为不同的部分,以便更容易理解。Rails按照惯例遵循这种设计模式。

既然我们有了控制器和视图,让我们生成下一个部分:模型。

6.1 生成模型

模型是一个Ruby类,用于表示数据。此外,模型可以通过Rails称为Active Record的功能与应用程序的数据库进行交互。

要定义模型,我们将使用模型生成器

$ bin/rails generate model Article title:string body:text

模型名称是单数,因为一个实例化的模型代表一个单一的数据记录。为了帮助记住这种约定,请考虑如何调用模型的构造函数:我们希望编写Article.new(...)而不是Articles.new(...)

这将创建几个文件

invoke  active_record
create    db/migrate/<timestamp>_create_articles.rb
create    app/models/article.rb
invoke    test_unit
create      test/models/article_test.rb
create      test/fixtures/articles.yml

我们将重点关注的两个文件是迁移文件(db/migrate/<timestamp>_create_articles.rb)和模型文件(app/models/article.rb)。

6.2 数据库迁移

迁移用于更改应用程序数据库的结构。在Rails应用程序中,迁移是用Ruby编写的,以便它们可以与数据库无关。

让我们看看新迁移文件的内容

class CreateArticles < ActiveRecord::Migration[8.0]
  def change
    create_table :articles do |t|
      t.string :title
      t.text :body

      t.timestamps
    end
  end
end

create_table的调用指定了如何构建articles表。默认情况下,create_table方法添加一个id列作为自动递增的主键。因此,表中的第一条记录的id为1,下一条记录的id为2,依此类推。

create_table的块内,定义了两个列:titlebody。这些是通过生成器添加的,因为我们将其包含在生成命令中(bin/rails generate model Article title:string body:text)。

块的最后一行是对t.timestamps的调用。此方法定义了两个额外的列,名为created_atupdated_at。正如我们将在后面看到的那样,Rails将为我们管理这些,并在我们创建或更新模型对象时设置它们的值。

让我们使用以下命令运行迁移

$ bin/rails db:migrate

该命令将显示输出,指示表已创建

==  CreateArticles: migrating ===================================
-- create_table(:articles)
   -> 0.0018s
==  CreateArticles: migrated (0.0018s) ==========================

要了解有关迁移的更多信息,请参阅Active Record Migrations

现在,我们可以使用模型与表进行交互。

6.3 使用模型与数据库进行交互

为了更好地使用我们的模型,我们将使用 Rails 中的一个名为 **控制台** 的功能。控制台是一个交互式编码环境,就像 `irb` 一样,但它还会自动加载 Rails 和我们的应用程序代码。

让我们使用以下命令启动控制台

$ bin/rails console

你应该看到一个类似于以下的 Rails 控制台提示

Loading development environment (Rails 8.0.0)
blog(dev)>

在这个提示符下,我们可以初始化一个新的 `Article` 对象

blog(dev)> article = Article.new(title: "Hello Rails", body: "I am on Rails!")

需要注意的是,我们仅仅 **初始化** 了这个对象。这个对象并没有保存到数据库中,它目前只在控制台中可用。要将对象保存到数据库,我们需要调用 save 方法。

blog(dev)> article.save
(0.1ms)  begin transaction
Article Create (0.4ms)  INSERT INTO "articles" ("title", "body", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["title", "Hello Rails"], ["body", "I am on Rails!"], ["created_at", "2020-01-18 23:47:30.734416"], ["updated_at", "2020-01-18 23:47:30.734416"]]
(0.9ms)  commit transaction
=> true

上面的输出显示了一个 `INSERT INTO "articles" ...` 数据库查询。这表明文章已经插入到我们的表中。如果我们再看一下 `article` 对象,我们会发现一些有趣的事情发生了。

blog(dev)> article
=> #<Article id: 1, title: "Hello Rails", body: "I am on Rails!", created_at: "2020-01-18 23:47:30", updated_at: "2020-01-18 23:47:30">

对象的 `id`、`created_at` 和 `updated_at` 属性现在被设置了。当我们保存对象时,Rails 为我们做了这件事。

当我们想从数据库中获取这篇文章时,我们可以在模型上调用 find 方法,并传递 `id` 作为参数。

blog(dev)> Article.find(1)
=> #<Article id: 1, title: "Hello Rails", body: "I am on Rails!", created_at: "2020-01-18 23:47:30", updated_at: "2020-01-18 23:47:30">

当我们想从数据库中获取所有文章时,我们可以在模型上调用 all 方法。

blog(dev)> Article.all
=> #<ActiveRecord::Relation [#<Article id: 1, title: "Hello Rails", body: "I am on Rails!", created_at: "2020-01-18 23:47:30", updated_at: "2020-01-18 23:47:30">]>

这个方法返回一个 ActiveRecord::Relation 对象,你可以把它理解为一个超级强大的数组。

要了解更多关于模型的信息,请参阅 Active Record 基础Active Record 查询接口

模型是 MVC 拼图的最后一块。接下来,我们将把所有这些部分连接在一起。

6.4 显示文章列表

让我们回到 `app/controllers/articles_controller.rb` 中的控制器,并将 `index` 动作修改为从数据库中获取所有文章。

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end
end

控制器的实例变量可以被视图访问。这意味着我们可以在 `app/views/articles/index.html.erb` 中引用 `@articles`。让我们打开这个文件,并将其内容替换为以下内容。

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <%= article.title %>
    </li>
  <% end %>
</ul>

上面的代码是 HTML 和 **ERB** 的混合。ERB 是 **嵌入式 Ruby** 的缩写,它是一个模板系统,用于评估嵌入在文档中的 Ruby 代码。在这里,我们可以看到两种类型的 ERB 标签:`<% %>` 和 `<%= %>`。`<% %>` 标签表示 “评估包含的 Ruby 代码”。`<%= %>` 标签表示 “评估包含的 Ruby 代码,并输出它返回的值”。你可以在这些 ERB 标签中写入任何你在常规 Ruby 程序中可以写的内容,尽管为了可读性,最好保持 ERB 标签的内容简短。

由于我们不希望输出 `@articles.each` 返回的值,所以我们将这段代码放在了 `<% %>` 标签中。但是,由于我们 **确实** 想要输出 `article.title` 返回的值(对于每篇文章),所以我们将这段代码放在了 `<%= %>` 标签中。

我们可以通过访问 https://127.0.0.1:3000 来查看最终结果。(请记住,`bin/rails server` 必须在运行!)。以下是我们这样做时会发生的事情。

  1. 浏览器发出一个请求:`GET https://127.0.0.1:3000`。
  2. 我们的 Rails 应用程序接收到这个请求。
  3. Rails 路由器将根路由映射到 `ArticlesController` 的 `index` 动作。
  4. `index` 动作使用 `Article` 模型来获取数据库中所有的文章。
  5. Rails 自动渲染 `app/views/articles/index.html.erb` 视图。
  6. 视图中的 ERB 代码被评估以输出 HTML。
  7. 服务器发送一个包含 HTML 的响应给浏览器。

我们已经将所有 MVC 部分连接在一起,并且我们有了第一个控制器动作!接下来,我们将继续进行第二个动作。

7 CRUDit 在哪里 CRUDit 就该在哪里

几乎所有 web 应用程序都涉及 CRUD(创建、读取、更新和删除) 操作。你甚至会发现你的应用程序做的工作大部分是 CRUD。Rails 认识到这一点,并提供了许多功能来帮助简化执行 CRUD 的代码。

让我们通过向我们的应用程序添加更多功能来开始探索这些功能。

7.1 显示单篇文章

我们目前有一个视图,它列出了数据库中的所有文章。让我们添加一个新的视图,它显示单个文章的标题和正文。

我们首先添加一个新的路由,它映射到一个新的控制器动作(我们将在下面添加它)。打开 `config/routes.rb`,并插入这里显示的最后一个路由。

Rails.application.routes.draw do
  root "articles#index"

  get "/articles", to: "articles#index"
  get "/articles/:id", to: "articles#show"
end

新的路由是另一个 `get` 路由,但是它的路径中有一些额外的内容:`:id`。这指定了一个路由 **参数**。路由参数捕获请求路径的一部分,并将该值放入 `params` 哈希表中,该哈希表可以被控制器动作访问。例如,当处理一个像 `GET https://127.0.0.1:3000/articles/1` 这样的请求时,`1` 将被捕获为 `:id` 的值,然后在 `ArticlesController` 的 `show` 动作中可以访问 `params[:id]`。

让我们现在添加 `show` 动作,将其放在 `app/controllers/articles_controller.rb` 中的 `index` 动作下面。

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end
end

`show` 动作调用 `Article.find`(之前提到过)并传递路由参数捕获的 ID。返回的文章存储在 `@article` 实例变量中,因此它可以被视图访问。默认情况下,`show` 动作将渲染 `app/views/articles/show.html.erb`。

让我们创建 `app/views/articles/show.html.erb`,其内容如下。

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

现在我们可以通过访问 https://127.0.0.1:3000/articles/1 来查看文章!

为了完成,让我们添加一个方便的方法来访问文章页面。我们将在 `app/views/articles/index.html.erb` 中将每篇文章的标题链接到它的页面。

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <a href="/articles/<%= article.id %>">
        <%= article.title %>
      </a>
    </li>
  <% end %>
</ul>

7.2 资源路由

到目前为止,我们已经涵盖了 CRUD 中的 “R”(读取)。我们最终将涵盖 “C”(创建)、“U”(更新)和 “D”(删除)。正如你可能已经猜到的,我们将通过添加新的路由、控制器动作和视图来实现。每当我们有一组路由、控制器动作和视图协同工作来对一个实体执行 CRUD 操作时,我们称这个实体为 **资源**。例如,在我们的应用程序中,我们可以说一篇文章是一个资源。

Rails 提供了一个名为 resources 的路由方法,它映射了资源集合(例如文章)的所有常规路由。所以在我们继续 “C”、 “U” 和 “D” 部分之前,让我们用 `resources` 替换 `config/routes.rb` 中的两个 `get` 路由。

Rails.application.routes.draw do
  root "articles#index"

  resources :articles
end

我们可以通过运行 `bin/rails routes` 命令来检查映射了哪些路由。

$ bin/rails routes
      Prefix Verb   URI Pattern                  Controller#Action
        root GET    /                            articles#index
    articles GET    /articles(.:format)          articles#index
 new_article GET    /articles/new(.:format)      articles#new
     article GET    /articles/:id(.:format)      articles#show
             POST   /articles(.:format)          articles#create
edit_article GET    /articles/:id/edit(.:format) articles#edit
             PATCH  /articles/:id(.:format)      articles#update
             PUT    /articles/:id(.:format)      articles#update
             DELETE /articles/:id(.:format)      articles#destroy

`resources` 方法还设置了 URL 和路径帮助器方法,我们可以使用它们来使我们的代码不依赖于特定的路由配置。上面 “Prefix” 列中的值加上 `_url` 或 `_path` 后缀形成了这些帮助器的名称。例如,当给定一篇文章时,`article_path` 帮助器将返回 `"/articles/#{article.id}"`。我们可以用它来整理我们在 `app/views/articles/index.html.erb` 中的链接。

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <a href="<%= article_path(article) %>">
        <%= article.title %>
      </a>
    </li>
  <% end %>
</ul>

然而,我们将进一步使用 link_to 帮助器。`link_to` 帮助器渲染一个链接,其第一个参数是链接的文本,第二个参数是链接的目标。如果我们传递一个模型对象作为第二个参数,`link_to` 将调用相应的路径帮助器将对象转换为路径。例如,如果我们传递一篇文章,`link_to` 将调用 `article_path`。因此,`app/views/articles/index.html.erb` 变成这样。

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <%= link_to article.title, article %>
    </li>
  <% end %>
</ul>

不错!

要了解有关路由的更多信息,请参阅Rails Routing from the Outside In

7.3 创建新文章

现在我们开始进行 CRUD 中的 “C”(创建)。通常,在 web 应用程序中,创建一个新的资源是一个多步骤的过程。首先,用户请求一个表单来填写。然后,用户提交表单。如果没有错误,则创建资源并显示某种确认信息。否则,表单将重新显示带有错误消息,并重复此过程。

在 Rails 应用程序中,这些步骤通常由控制器的 `new` 和 `create` 动作处理。让我们在 `app/controllers/articles_controller.rb` 中添加一个典型的实现,放在 `show` 动作的下面。

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(title: "...", body: "...")

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end
end

`new` 动作实例化一个新的文章,但不保存它。这个文章将在构建表单时被视图使用。默认情况下,`new` 动作将渲染 `app/views/articles/new.html.erb`,我们将在下面创建它。

`create` 动作实例化一个新的文章,它包含标题和正文的值,并尝试保存它。如果文章保存成功,动作将把浏览器重定向到文章页面,地址为 `“https://127.0.0.1:3000/articles/#{@article.id}”`。否则,动作将通过渲染 `app/views/articles/new.html.erb` 并设置状态码为 422 不可处理的实体 来重新显示表单。这里的标题和正文是虚拟值。在创建表单后,我们将回来修改它们。

redirect_to 将导致浏览器发出新的请求,而 render 则为当前请求渲染指定的视图。在修改数据库或应用程序状态后,使用 `redirect_to` 很重要。否则,如果用户刷新页面,浏览器将发出相同的请求,并且修改将被重复。

7.3.1 使用表单生成器

我们将使用 Rails 的一项名为 **表单生成器** 的功能来创建我们的表单。使用表单生成器,我们可以编写最少的代码来输出一个完全配置并遵循 Rails 惯例的表单。

让我们创建 `app/views/articles/new.html.erb`,其内容如下。

<h1>New Article</h1>

<%= form_with model: @article do |form| %>
  <div>
    <%= form.label :title %><br>
    <%= form.text_field :title %>
  </div>

  <div>
    <%= form.label :body %><br>
    <%= form.textarea :body %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>

form_with 帮助器方法实例化一个表单生成器。在 `form_with` 代码块中,我们调用表单生成器上的 `label` 和 `text_field` 等方法来输出相应的表单元素。

我们从 `form_with` 调用得到的输出将如下所示。

<form action="/articles" accept-charset="UTF-8" method="post">
  <input type="hidden" name="authenticity_token" value="...">

  <div>
    <label for="article_title">Title</label><br>
    <input type="text" name="article[title]" id="article_title">
  </div>

  <div>
    <label for="article_body">Body</label><br>
    <textarea name="article[body]" id="article_body"></textarea>
  </div>

  <div>
    <input type="submit" name="commit" value="Create Article" data-disable-with="Create Article">
  </div>
</form>

要了解更多关于表单生成器的信息,请参阅 Action View 表单帮助器

7.3.2 使用强参数

提交的表单数据被放入 `params` 哈希表中,与捕获的路由参数放在一起。因此,`create` 动作可以通过 `params[:article][:title]` 访问提交的标题,通过 `params[:article][:body]` 访问提交的正文。我们可以将这些值单独传递给 `Article.new`,但这将是冗长且可能容易出错的。而且,随着我们添加更多字段,这种情况会变得更糟。

相反,我们将传递一个包含值的单个哈希。但是,我们仍然必须指定该哈希中允许的值。否则,恶意用户可能会提交额外的表单字段并覆盖私有数据。事实上,如果我们将未过滤的 `params[:article]` 哈希直接传递给 `Article.new`,Rails 会引发 `ForbiddenAttributesError` 来提醒我们这个问题。所以我们将使用 Rails 的一个叫做 *强参数* 的特性来过滤 `params`。把它想象成 `params` 的 强类型

让我们在 `app/controllers/articles_controller.rb` 的底部添加一个私有方法,名为 `article_params`,它过滤 `params`。并且让我们更改 `create` 以使用它

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end

  private
    def article_params
      params.expect(article: [:title, :body])
    end
end

要了解更多关于强参数的信息,请参阅 Action Controller 概述 § 强参数

7.3.3 验证和显示错误消息

正如我们所见,创建资源是一个多步骤的过程。处理无效的用户输入是该过程的另一个步骤。Rails 提供了一个叫做 *验证* 的特性来帮助我们处理无效的用户输入。验证是在保存模型对象之前检查的规则。如果任何检查失败,保存将被中止,并且相应的错误消息将被添加到模型对象的 `errors` 属性中。

让我们在 `app/models/article.rb` 中的模型中添加一些验证

class Article < ApplicationRecord
  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

第一个验证声明 `title` 值必须存在。因为 `title` 是一个字符串,这意味着 `title` 值必须包含至少一个非空格字符。

第二个验证声明 `body` 值也必须存在。此外,它声明 `body` 值的长度必须至少为 10 个字符。

你可能想知道 `title` 和 `body` 属性是在哪里定义的。Active Record 会自动为每个表列定义模型属性,因此你不需要在你的模型文件中声明这些属性。

有了我们的验证,让我们修改 `app/views/articles/new.html.erb` 来显示 `title` 和 `body` 的任何错误消息

<h1>New Article</h1>

<%= form_with model: @article do |form| %>
  <div>
    <%= form.label :title %><br>
    <%= form.text_field :title %>
    <% @article.errors.full_messages_for(:title).each do |message| %>
      <div><%= message %></div>
    <% end %>
  </div>

  <div>
    <%= form.label :body %><br>
    <%= form.textarea :body %><br>
    <% @article.errors.full_messages_for(:body).each do |message| %>
      <div><%= message %></div>
    <% end %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>

full_messages_for 方法返回指定属性的用户友好错误消息数组。如果没有该属性的错误,则数组将为空。

为了理解这一切是如何协同工作的,让我们再看一下 `new` 和 `create` 控制器操作

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end

当我们访问 https://127.0.0.1:3000/articles/new 时,`GET /articles/new` 请求被映射到 `new` 操作。`new` 操作不会尝试保存 `@article`。因此,不会检查验证,并且不会有任何错误消息。

当我们提交表单时,`POST /articles` 请求被映射到 `create` 操作。`create` 操作 *确实* 尝试保存 `@article`。因此,*会* 检查验证。如果任何验证失败,`@article` 不会被保存,并且 `app/views/articles/new.html.erb` 将会渲染出错误消息。

要了解更多关于验证的信息,请参阅 Active Record 验证。要了解更多关于验证错误消息的信息,请参阅 Active Record 验证 § 处理验证错误

7.3.4 完成

我们现在可以通过访问 https://127.0.0.1:3000/articles/new 来创建文章。为了完成,让我们在 `app/views/articles/index.html.erb` 的底部链接到该页面

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <%= link_to article.title, article %>
    </li>
  <% end %>
</ul>

<%= link_to "New Article", new_article_path %>

7.4 更新文章

我们已经涵盖了 CRUD 的 "CR"。现在让我们继续 "U"(更新)。更新资源与创建资源非常相似。它们都是多步骤的过程。首先,用户请求一个表单来编辑数据。然后,用户提交表单。如果没有错误,则资源将被更新。否则,表单将重新显示错误消息,并且重复该过程。

这些步骤通常由控制器的 `edit` 和 `update` 操作处理。让我们在 `app/controllers/articles_controller.rb` 中的 `create` 操作下方添加这些操作的典型实现

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit
    @article = Article.find(params[:id])
  end

  def update
    @article = Article.find(params[:id])

    if @article.update(article_params)
      redirect_to @article
    else
      render :edit, status: :unprocessable_entity
    end
  end

  private
    def article_params
      params.expect(article: [:title, :body])
    end
end

注意 `edit` 和 `update` 操作是如何类似于 `new` 和 `create` 操作的。

`edit` 操作从数据库中获取文章,并将其存储在 `@article` 中,以便在构建表单时使用。默认情况下,`edit` 操作将渲染 `app/views/articles/edit.html.erb`。

`update` 操作 (重新) 从数据库中获取文章,并尝试使用由 `article_params` 过滤的提交的表单数据来更新它。如果没有任何验证失败并且更新成功,则操作将重定向浏览器到文章的页面。否则,操作将通过渲染 `app/views/articles/edit.html.erb` 来重新显示表单——带有错误消息。

7.4.1 使用部分来共享视图代码

我们的 `edit` 表单将看起来与我们的 `new` 表单相同。甚至代码也将相同,这要归功于 Rails 表单构建器和资源路由。表单构建器会自动配置表单,以根据模型对象是否已保存来发出适当类型的请求。

因为代码将相同,我们将把它分解为一个叫做 *部分* 的共享视图。让我们创建 `app/views/articles/_form.html.erb`,内容如下

<%= form_with model: article do |form| %>
  <div>
    <%= form.label :title %><br>
    <%= form.text_field :title %>
    <% article.errors.full_messages_for(:title).each do |message| %>
      <div><%= message %></div>
    <% end %>
  </div>

  <div>
    <%= form.label :body %><br>
    <%= form.textarea :body %><br>
    <% article.errors.full_messages_for(:body).each do |message| %>
      <div><%= message %></div>
    <% end %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>

以上代码与我们 `app/views/articles/new.html.erb` 中的表单相同,除了所有 `@article` 的出现都被替换为 `article`。因为部分是共享代码,所以最好实践是不依赖于控制器操作设置的特定实例变量。相反,我们将把文章作为局部变量传递给部分。

让我们更新 `app/views/articles/new.html.erb` 以使用通过 render 的部分

<h1>New Article</h1>

<%= render "form", article: @article %>

部分的文件名必须以下划线开头,例如 `_form.html.erb`。但是在渲染时,它将被引用为 *没有* 下划线,例如 `render "form"`。

现在,让我们创建一个非常相似的 `app/views/articles/edit.html.erb`

<h1>Edit Article</h1>

<%= render "form", article: @article %>

要了解更多关于部分的信息,请参阅 Rails 中的布局和渲染 § 使用部分

7.4.2 完成

我们现在可以通过访问其编辑页面来更新文章,例如 https://127.0.0.1:3000/articles/1/edit。为了完成,让我们在 `app/views/articles/show.html.erb` 的底部链接到编辑页面

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
</ul>

7.5 删除文章

最后,我们到达了 CRUD 的 "D"(删除)。删除资源比创建或更新更简单。它只需要一条路由和一个控制器操作。而我们的资源路由 (resources :articles) 已经提供了路由,它将 `DELETE /articles/:id` 请求映射到 `ArticlesController` 的 `destroy` 操作。

所以,让我们在 `app/controllers/articles_controller.rb` 中的 `update` 操作下方添加一个典型的 `destroy` 操作

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit
    @article = Article.find(params[:id])
  end

  def update
    @article = Article.find(params[:id])

    if @article.update(article_params)
      redirect_to @article
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def destroy
    @article = Article.find(params[:id])
    @article.destroy

    redirect_to root_path, status: :see_other
  end

  private
    def article_params
      params.expect(article: [:title, :body])
    end
end

`destroy` 操作从数据库中获取文章,并对它调用 destroy。然后,它将浏览器重定向到根路径,状态代码为 303 See Other

我们选择重定向到根路径,因为它是我们访问文章的主要入口。但是,在其他情况下,你可能选择重定向到例如 `articles_path`。

现在让我们在 `app/views/articles/show.html.erb` 的底部添加一个链接,以便我们可以在其自己的页面中删除文章

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
  <li><%= link_to "Destroy", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "Are you sure?"
                  } %></li>
</ul>

在上面的代码中,我们使用 `data` 选项来设置 "Destroy" 链接的 `data-turbo-method` 和 `data-turbo-confirm` HTML 属性。这两个属性都挂钩到 Turbo,它默认包含在新的 Rails 应用程序中。`data-turbo-method="delete"` 将导致链接发出 `DELETE` 请求而不是 `GET` 请求。`data-turbo-confirm="Are you sure?"` 将导致在点击链接时出现一个确认对话框。如果用户取消对话框,则请求将被中止。

就这样!我们现在可以列出、显示、创建、更新和删除文章了!InCRUDable!

8 添加第二个模型

是时候向应用程序添加第二个模型了。第二个模型将处理文章的评论。

8.1 生成模型

我们将看到与之前创建 `Article` 模型时使用的相同生成器。这次我们将创建一个 `Comment` 模型来保存对文章的引用。在你的终端中运行以下命令

$ bin/rails generate model Comment commenter:string body:text article:references

此命令将生成四个文件

文件 目的
db/migrate/<timestamp>_create_comments.rb 迁移到在你的数据库中创建评论表
app/models/comment.rb 评论模型
test/models/comment_test.rb 评论模型的测试工具
test/fixtures/comments.yml 用于测试的示例评论

首先,看一下 `app/models/comment.rb`

class Comment < ApplicationRecord
  belongs_to :article
end

这与你之前看到的 `Article` 模型非常相似。不同之处在于 `belongs_to :article` 行,它设置了一个 Active Record *关联*。你将在本指南的下一部分中了解一些关于关联的信息。

在 shell 命令中使用的 (:references) 关键字是模型的一种特殊数据类型。它在你的数据库表中创建一个新的列,该列用提供的模型名称加上 `_id` 来命名,该列可以保存整数值。为了更好地理解,在运行迁移后分析 `db/schema.rb` 文件。

除了模型之外,Rails 还做了一个迁移来创建相应的数据库表

class CreateComments < ActiveRecord::Migration[8.0]
  def change
    create_table :comments do |t|
      t.string :commenter
      t.text :body
      t.references :article, null: false, foreign_key: true

      t.timestamps
    end
  end
end

t.references 行创建一个名为 `article_id` 的整型列,为它创建一个索引,以及一个指向 `articles` 表的 `id` 列的外键约束。继续运行迁移

$ bin/rails db:migrate

Rails 足够智能,只执行尚未针对当前数据库运行的迁移,所以在这个例子中你只会看到

==  CreateComments: migrating =================================================
-- create_table(:comments)
   -> 0.0115s
==  CreateComments: migrated (0.0119s) ========================================

8.2 关联模型

Active Record 关联允许你轻松地声明两个模型之间的关系。在评论和文章的情况下,你可以这样写出关系

  • 每个评论都属于一篇文章。
  • 一篇文章可以拥有许多评论。

事实上,这非常接近 Rails 用于声明此关联的语法。您已经看到了在 Comment 模型(app/models/comment.rb)中使每个评论属于一篇文章的代码行。

class Comment < ApplicationRecord
  belongs_to :article
end

您需要编辑 app/models/article.rb 来添加关联的另一端。

class Article < ApplicationRecord
  has_many :comments

  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

这两个声明启用了相当多的自动行为。例如,如果您有一个包含文章的实例变量 @article,您可以使用 @article.comments 获取属于该文章的所有评论,以数组的形式。

有关 Active Record 关联的更多信息,请参阅 Active Record 关联 指南。

8.3 添加评论路由

articles 控制器一样,我们需要添加一个路由,以便 Rails 知道我们希望导航到哪里才能看到 comments。再次打开 config/routes.rb 文件并进行如下编辑。

Rails.application.routes.draw do
  root "articles#index"

  resources :articles do
    resources :comments
  end
end

这将 comments 创建为 articles 内的嵌套资源。这是捕获文章和评论之间存在的层次关系的另一个部分。

有关路由的更多信息,请参阅 Rails 路由 指南。

8.4 生成控制器

有了模型,您可以开始关注创建匹配的控制器。同样,我们将使用之前使用的相同生成器。

$ bin/rails generate controller Comments

这将创建三个文件和一个空目录。

文件/目录 目的
app/controllers/comments_controller.rb 评论控制器
app/views/comments/ 控制器的视图存储在此处
test/controllers/comments_controller_test.rb 控制器的测试
app/helpers/comments_helper.rb 视图助手文件

与任何博客一样,我们的读者将在阅读完文章后直接创建他们的评论,并且一旦添加了评论,就会被发送回文章展示页面以查看现在列出的评论。因此,我们的 CommentsController 用于提供一种方法来创建评论,并在评论到达时删除垃圾邮件评论。

所以首先,我们将连接文章展示模板(app/views/articles/show.html.erb)以让我们发表新评论。

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
  <li><%= link_to "Destroy", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "Are you sure?"
                  } %></li>
</ul>

<h2>Add a comment:</h2>
<%= form_with model: [ @article, @article.comments.build ] do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.textarea :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>

这在 Article 展示页面上添加了一个表单,该表单通过调用 CommentsControllercreate 操作来创建新评论。此处的 form_with 调用使用数组,这将构建一个嵌套路由,例如 /articles/1/comments

让我们连接 app/controllers/comments_controller.rb 中的 create

class CommentsController < ApplicationController
  def create
    @article = Article.find(params[:article_id])
    @comment = @article.comments.create(comment_params)
    redirect_to article_path(@article)
  end

  private
    def comment_params
      params.expect(comment: [:commenter, :body])
    end
end

您会看到这里比文章控制器中的代码更复杂。这是您设置的嵌套的副作用。每个评论请求都必须跟踪评论所附加的文章,因此,最初调用 Article 模型的 find 方法以获取相关文章。

此外,代码利用了与关联一起使用的一些方法。我们使用 @article.comments 上的 create 方法来创建并保存评论。这将自动链接评论,使其属于该特定文章。

创建新评论后,我们将使用 article_path(@article) 助手将用户发送回原始文章。正如我们已经看到的那样,这将调用 ArticlesControllershow 操作,该操作又会渲染 show.html.erb 模板。我们希望评论显示在这里,因此让我们将其添加到 app/views/articles/show.html.erb 中。

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
  <li><%= link_to "Destroy", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "Are you sure?"
                  } %></li>
</ul>

<h2>Comments</h2>
<% @article.comments.each do |comment| %>
  <p>
    <strong>Commenter:</strong>
    <%= comment.commenter %>
  </p>

  <p>
    <strong>Comment:</strong>
    <%= comment.body %>
  </p>
<% end %>

<h2>Add a comment:</h2>
<%= form_with model: [ @article, @article.comments.build ] do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.textarea :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>

现在您可以向博客添加文章和评论,并让它们显示在正确的位置。

Article with Comments

9 重构

现在我们已经让文章和评论正常工作,请查看 app/views/articles/show.html.erb 模板。它变得很长而且很笨拙。我们可以使用部分来清理它。

9.1 渲染部分集合

首先,我们将创建一个评论部分来提取显示文章的所有评论。创建文件 app/views/comments/_comment.html.erb 并将以下内容放入其中。

<p>
  <strong>Commenter:</strong>
  <%= comment.commenter %>
</p>

<p>
  <strong>Comment:</strong>
  <%= comment.body %>
</p>

然后,您可以更改 app/views/articles/show.html.erb 以使其如下所示。

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
  <li><%= link_to "Destroy", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "Are you sure?"
                  } %></li>
</ul>

<h2>Comments</h2>
<%= render @article.comments %>

<h2>Add a comment:</h2>
<%= form_with model: [ @article, @article.comments.build ] do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.textarea :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>

这将现在为 @article.comments 集合中的每个评论渲染一次 app/views/comments/_comment.html.erb 中的部分。随着 render 方法迭代 @article.comments 集合,它将每个评论分配给一个与部分同名的局部变量,在本例中为 comment,然后它将在部分中可用,供我们显示。

9.2 渲染部分表单

让我们也将其新评论部分移到它自己的部分。再次,您创建一个包含以下内容的文件 app/views/comments/_form.html.erb

<%= form_with model: [ article, article.comments.build ] do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.textarea :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>

然后,您使 app/views/articles/show.html.erb 看起来如下所示。

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
  <li><%= link_to "Destroy", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "Are you sure?"
                  } %></li>
</ul>

<h2>Comments</h2>
<%= render @article.comments %>

<h2>Add a comment:</h2>
<%= render "comments/form", article: @article %>

第二个渲染只是定义了我们想要渲染的部分模板 comments/form。Rails 足够智能,可以发现字符串中的正斜杠并意识到您想要渲染 app/views/comments 目录中的 _form.html.erb 文件。

9.3 使用关注点

关注点是一种使大型控制器或模型更容易理解和管理的方法。这也具有可重用性的优势,因为多个模型(或控制器)共享相同的关注点。关注点是使用包含代表模型或控制器负责的功能的明确切片的模块来实现的。在其他语言中,模块通常被称为 mixin。

您可以在控制器或模型中使用关注点,就像使用任何模块一样。当您最初使用 rails new blog 创建应用程序时,除了其他内容之外,还在 app/ 中创建了两个文件夹。

app/controllers/concerns
app/models/concerns

在下面的示例中,我们将为我们的博客实现一个新功能,该功能将受益于使用关注点。然后,我们将创建一个关注点,并重构代码以使用它,使代码更 DRY 且更易维护。

一篇博客文章可能具有不同的状态 - 例如,它可能对所有人可见(即 public),或者仅对作者可见(即 private)。它也可能对所有人隐藏,但仍可检索(即 archived)。评论也可能被隐藏或可见。这可以使用每个模型中的 status 列来表示。

首先,让我们运行以下迁移将 status 添加到 ArticlesComments 中。

$ bin/rails generate migration AddStatusToArticles status:string
$ bin/rails generate migration AddStatusToComments status:string

接下来,让我们使用生成的迁移更新数据库。

$ bin/rails db:migrate

要为现有文章和评论选择状态,您可以通过添加 default: "public" 选项来为生成的迁移文件添加默认值,然后再次启动迁移。您也可以在 rails 控制台中调用 Article.update_all(status: "public")Comment.update_all(status: "public")

要了解有关迁移的更多信息,请参阅Active Record Migrations

我们还必须将 :status 键作为强参数的一部分,在 app/controllers/articles_controller.rb 中。


  private
    def article_params
      params.expect(article: [:title, :body, :status])
    end

以及在 app/controllers/comments_controller.rb 中。


  private
    def comment_params
      params.expect(comment: [:commenter, :body, :status])
    end

article 模型中,在使用 bin/rails db:migrate 命令运行迁移以添加 status 列后,您将添加。

class Article < ApplicationRecord
  has_many :comments

  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }

  VALID_STATUSES = [ "public", "private", "archived" ]

  validates :status, inclusion: { in: VALID_STATUSES }

  def archived?
    status == "archived"
  end
end

以及在 Comment 模型中。

class Comment < ApplicationRecord
  belongs_to :article

  VALID_STATUSES = [ "public", "private", "archived" ]

  validates :status, inclusion: { in: VALID_STATUSES }

  def archived?
    status == "archived"
  end
end

然后,在我们的 index 操作模板(app/views/articles/index.html.erb)中,我们将使用 archived? 方法来避免显示任何已存档的文章。

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <% unless article.archived? %>
      <li>
        <%= link_to article.title, article %>
      </li>
    <% end %>
  <% end %>
</ul>

<%= link_to "New Article", new_article_path %>

类似地,在我们的评论部分视图(app/views/comments/_comment.html.erb)中,我们将使用 archived? 方法来避免显示任何已存档的评论。

<% unless comment.archived? %>
  <p>
    <strong>Commenter:</strong>
    <%= comment.commenter %>
  </p>

  <p>
    <strong>Comment:</strong>
    <%= comment.body %>
  </p>
<% end %>

但是,如果您再次查看我们现在的模型,您会发现逻辑是重复的。如果将来我们增强博客的功能 - 例如包含私信 - 我们可能会发现自己再次重复逻辑。这就是关注点派上用场的地方。

关注点只负责模型责任的一个重点子集;我们关注点中的方法都将与模型的可见性相关。让我们将我们的新关注点(模块)称为 Visible。我们可以在 app/models/concerns 中创建一个新文件,名为 visible.rb,并将模型中重复的所有状态方法存储起来。

app/models/concerns/visible.rb

module Visible
  def archived?
    status == "archived"
  end
end

我们可以将状态验证添加到关注点,但这稍微复杂一些,因为验证是在类级别调用的方法。ActiveSupport::Concern (API 指南) 提供了一种更简单的方法来包含它们。

module Visible
  extend ActiveSupport::Concern

  VALID_STATUSES = [ "public", "private", "archived" ]

  included do
    validates :status, inclusion: { in: VALID_STATUSES }
  end

  def archived?
    status == "archived"
  end
end

现在,我们可以从每个模型中删除重复的逻辑,而是包含我们新的 Visible 模块。

app/models/article.rb 中。

class Article < ApplicationRecord
  include Visible

  has_many :comments

  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

以及在 app/models/comment.rb 中。

class Comment < ApplicationRecord
  include Visible

  belongs_to :article
end

类方法也可以添加到关注点。如果我们想在主页上显示公开文章或评论的数量,我们可以向 Visible 添加一个类方法,如下所示。

module Visible
  extend ActiveSupport::Concern

  VALID_STATUSES = [ "public", "private", "archived" ]

  included do
    validates :status, inclusion: { in: VALID_STATUSES }
  end

  class_methods do
    def public_count
      where(status: "public").count
    end
  end

  def archived?
    status == "archived"
  end
end

然后在视图中,您可以像调用任何类方法一样调用它。

<h1>Articles</h1>

Our blog has <%= Article.public_count %> articles and counting!

<ul>
  <% @articles.each do |article| %>
    <% unless article.archived? %>
      <li>
        <%= link_to article.title, article %>
      </li>
    <% end %>
  <% end %>
</ul>

<%= link_to "New Article", new_article_path %>

最后,我们将向表单添加一个选择框,并让用户在创建新文章或发布新评论时选择状态。我们还可以选择对象的狀態,如果尚未设置,则选择 public 的默认值。在 app/views/articles/_form.html.erb 中,我们可以添加。

<div>
  <%= form.label :status %><br>
  <%= form.select :status, Visible::VALID_STATUSES, selected: article.status || 'public' %>
</div>

以及在 app/views/comments/_form.html.erb 中。

<p>
  <%= form.label :status %><br>
  <%= form.select :status, Visible::VALID_STATUSES, selected: 'public' %>
</p>

10 删除评论

博客的另一个重要功能是能够删除垃圾邮件评论。为此,我们需要在视图中实现某种链接,并在 CommentsController 中实现 destroy 操作。

所以首先,让我们在 app/views/comments/_comment.html.erb 部分中添加删除链接。

<% unless comment.archived? %>
  <p>
    <strong>Commenter:</strong>
    <%= comment.commenter %>
  </p>

  <p>
    <strong>Comment:</strong>
    <%= comment.body %>
  </p>

  <p>
    <%= link_to "Destroy Comment", [comment.article, comment], data: {
                  turbo_method: :delete,
                  turbo_confirm: "Are you sure?"
                } %>
  </p>
<% end %>

单击此新的“删除评论”链接将向我们的 CommentsController 发出 DELETE /articles/:article_id/comments/:id 请求,然后可以使用此请求找到我们要删除的评论,因此让我们向控制器(app/controllers/comments_controller.rb)添加 destroy 操作。

class CommentsController < ApplicationController
  def create
    @article = Article.find(params[:article_id])
    @comment = @article.comments.create(comment_params)
    redirect_to article_path(@article)
  end

  def destroy
    @article = Article.find(params[:article_id])
    @comment = @article.comments.find(params[:id])
    @comment.destroy
    redirect_to article_path(@article), status: :see_other
  end

  private
    def comment_params
      params.expect(comment: [:commenter, :body, :status])
    end
end

destroy 操作将找到我们正在查看的文章,在 @article.comments 集合中找到评论,然后将其从数据库中删除,并将其发送回文章的展示操作。

10.1 删除关联对象

如果您删除一篇文章,其关联的评论也需要被删除,否则它们将只是占用数据库空间。Rails 允许您使用关联的 `dependent` 选项来实现这一点。修改 `Article` 模型(`app/models/article.rb`),如下所示:

class Article < ApplicationRecord
  include Visible

  has_many :comments, dependent: :destroy

  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

11 安全性

11.1 基本身份验证

如果您将您的博客发布到网上,任何人都可以添加、编辑和删除文章或删除评论。

Rails 提供了一个 HTTP 身份验证系统,它将在这种情况下很好地工作。

在 `ArticlesController` 中,我们需要有一种方法来阻止未经身份验证的人访问各种操作。在这里,我们可以使用 Rails 的 `http_basic_authenticate_with` 方法,该方法允许在该方法允许的情况下访问请求的操作。

要使用身份验证系统,我们在 `app/controllers/articles_controller.rb` 中的 `ArticlesController` 顶部指定它。在我们的例子中,我们希望用户在除 `index` 和 `show` 之外的所有操作上进行身份验证,因此我们这样写:

class ArticlesController < ApplicationController
  http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show]

  def index
    @articles = Article.all
  end

  # snippet for brevity
end

我们还希望只允许经过身份验证的用户删除评论,因此在 `CommentsController`(`app/controllers/comments_controller.rb`)中,我们这样写:

class CommentsController < ApplicationController
  http_basic_authenticate_with name: "dhh", password: "secret", only: :destroy

  def create
    @article = Article.find(params[:article_id])
    # ...
  end

  # snippet for brevity
end

现在,如果您尝试创建新文章,您将收到一个基本的 HTTP 身份验证挑战。

Basic HTTP Authentication Challenge

输入正确的用户名和密码后,您将保持身份验证状态,直到需要不同的用户名和密码或浏览器关闭。

Rails 应用程序可以使用其他身份验证方法。Rails 的两个流行身份验证插件是 Devise rails 引擎和 Authlogic gem,以及其他许多。

11.2 其他安全注意事项

安全,尤其是在 Web 应用程序中,是一个广泛而详细的领域。在 Ruby on Rails 安全指南 中更深入地介绍了 Rails 应用程序中的安全性。

12 下一步是什么?

既然您已经看到了第一个 Rails 应用程序,您应该可以自由地更新它并在自己身上进行实验。

请记住,您不必在没有帮助的情况下做所有事情。如果您需要帮助启动和运行 Rails,请随时查阅以下支持资源:

13 配置陷阱

使用 Rails 的最简单方法是将所有外部数据存储为 UTF-8。如果您没有,Ruby 库和 Rails 通常能够将您的本地数据转换为 UTF-8,但这并不总是可靠地工作,因此您最好确保所有外部数据都是 UTF-8。

如果您在此方面犯了错误,最常见的症状是浏览器中出现一个带有问号的黑钻。另一个常见的症状是出现 “ü” 等字符,而不是 “ü”。Rails 采取了一些内部步骤来缓解这些问题的常见原因,这些问题可以自动检测和纠正。但是,如果您有未存储为 UTF-8 的外部数据,则可能会偶尔出现这些问题,这些问题无法由 Rails 自动检测和纠正。

两种非常常见的非 UTF-8 数据来源:

  • 您的文本编辑器:大多数文本编辑器(如 TextMate)默认将文件保存为 UTF-8。如果您的文本编辑器没有这样做,则您在模板(例如 é)中输入的特殊字符可能会在浏览器中显示为带有问号的菱形。这也适用于您的 i18n 翻译文件。大多数没有默认使用 UTF-8 的编辑器(例如某些版本的 Dreamweaver)提供了一种方法来更改默认设置为 UTF-8。请这样做。
  • 您的数据库:Rails 默认将数据库中的数据转换为边界处的 UTF-8。但是,如果您的数据库内部没有使用 UTF-8,则可能无法存储用户输入的所有字符。例如,如果您的数据库内部使用 Latin-1,而您的用户输入了俄语、希伯来语或日语字符,则数据在进入数据库后将永远丢失。如果可能,请使用 UTF-8 作为数据库的内部存储。


返回顶部