☰
Rails Girls 指南:为你的 Rails 应用添加评论功能(Commenting)
2026/10/7 2:42:20 网站建设 项目流程
  • 教程
  • 后端

【免费下载链接】guides.railsgirls.com

Rails Girls Guides

项目地址:https://gitcode.com/gh_mirrors/gu/guides.railsgirls.com
点击查看免费下载

本篇指南(Rails Girls Guides 系列的第 12 篇,源文档位于 _pages/commenting.md)手把手教你基于已经建好的railsgirls应用(Idea 应用),从零添加"评论"功能:让访问者可以在每条 idea 下留言、查看留言并删除留言。与之前大量依赖rails generate scaffold的步骤不同,本篇将更多地手写 Ruby 代码,让你深入理解 Rails 的路由嵌套、模型关联、控制器与视图之间的协作方式。读完本篇,你将掌握嵌套资源路由的写法、一对多模型关联的建立、以及用form_with快速实现表单提交的完整链路。

在动手之前,请确保你已经按顺序完成了前面的主指南(从 _pages/start.md 到 _pages/design.md),特别是已经创建好名为railsgirls的应用、生成了 Idea 的 scaffold 并完成了数据库迁移。该指南的定位信息可以在 主指南索引 中看到——它在指南列表中位列第 12 项,紧跟在样式设计指南之后、图片缩略图与 RSpec 测试指南之前,是"应用功能完善"阶段的重要一环。

理解目标:评论到底是什么

评论(Comment)是访客在网站上留下的短消息。在本应用中,我们要实现的目标非常具体:

  • 每条idea可以拥有多条comment;
  • 每条comment必须属于某一条特定的idea;
  • 评论显示在对应 idea 的详情页上,不会串页——不同 idea 的评论互不干扰;
  • 访客可以提交新评论,也可以删除评论。

关键点在于:评论与 idea 之间是典型的"一对多"(one-to-many)关系。一个 Idea 可以有很多评论,但一条评论只能属于一个 Idea。整个实现围绕这条关系展开,从数据库结构到路由、模型、控制器、视图,层层递进。

第一步:为评论添加嵌套路由

路由(routes)是 Rails 应用收到 HTTP 请求后分发的第一站。要让系统知道"评论"这个资源的存在,我们需要修改config/routes.rb。

打开该文件,找到当前只有一行资源声明的部分:

resources :ideas

将其替换为嵌套资源的写法:

resources :ideas do resources :comments end

为什么使用嵌套路由?

把resources :comments放进resources :ideas的块里,Rails 会生成一组嵌套在 idea 之下的评论路由。这样,URL 本身就携带了归属信息,例如:

  • GET /ideas/1/comments—— 列出 idea 1 的所有评论;
  • POST /ideas/1/comments—— 为 idea 1 创建一条评论;
  • DELETE /ideas/1/comments/5—— 删除 idea 1 下 id 为 5 的评论。

从路由中我们就能直接推导出这条评论属于哪条 idea,而不需要额外的查询参数。这也让控制器代码可以干净地通过params[:idea_id]拿到父资源的主键。

第二步:生成 Comment 模型与数据库迁移

接下来创建评论的数据模型。注意这次不使用 scaffold——scaffold 会连带生成控制器、视图和一大堆样板代码,而本指南的目标是减少对生成代码的依赖、亲手写 Ruby 代码。

在终端执行:

rails generate model comment user_name:string body:text idea:references

这条命令做了三件事:

  1. 创建一个名为Comment的模型(对应app/models/comment.rb);
  2. 声明两个内容字段:user_name(字符串类型,记录留言者姓名)和body(文本类型,存放留言正文);
  3. 通过idea:references在评论表中加入idea_id外键字段,告诉数据库"这条评论属于哪条 idea"——这正是评论不会显示在错误页面上的根基。

执行迁移

生成器同时会创建一个迁移文件(位于db/migrate/目录下),它的作用是让数据库"知道"新增了一张 comments 表。执行迁移命令使数据库结构真正生效:

rails db:migrate

迁移完成后,comments 表就真实存在于数据库中了,包含user_name、body、idea_id以及 Rails 自动添加的created_at/updated_at时间戳字段。

第三步:打通模型之间的关联

数据库层面有了idea_id,但应用代码还需要显式声明两个模型之间的关系,Rails 才知道如何帮你查询"某条 idea 下的所有评论"。

Idea 端:一条 idea 拥有很多评论

打开app/models/idea.rb,在class Idea < ApplicationRecord这一行下面添加:

class Idea < ApplicationRecord has_many :comments end

has_many :comments告诉 Rails:一个 Idea 对象可以关联多条 Comment 记录。加上这一行之后,你就可以直接在代码里写@idea.comments来取出属于这条 idea 的全部评论——Rails 会自动生成对应的数据库查询。

Comment 端:一条评论属于一条 idea

打开app/models/comment.rb,你会看到生成器已经自动写好了归属关系:

class Comment < ApplicationRecord belongs_to :idea end

belongs_to :idea引用了Idea模型,它是我们之前生成模型时idea:references这一参数自动产出的结果。这一行与has_many :comments互为镜像,共同构成了 Rails 中一对多关联的标准写法。

第四步:从数据库加载评论到详情页

评论数据要被页面展示,第一步是把它从数据库"捞"出来。在 Rails 的 MVC 架构中,这个职责由控制器承担。

打开app/controllers/ideas_controller.rb,找到show方法。这是一个 Ruby 方法,它的职责是从数据库加载数据、供视图(我们之前编辑过的 HTML 文件)使用。把原来的show方法修改为:

def show @comments = @idea.comments end

注意:这里我们直接调用了@idea.comments,正是上一步has_many :comments声明带来的魔法——Rails 会根据关联关系,自动查询出所有idea_id等于当前 idea 主键的评论记录,并存入@comments实例变量。之后视图就能通过@comments访问这些数据了。

第五步:编写 CommentsController

要把评论存进数据库,之后还要能删除它们,我们需要一个专门的控制器。和IdeasController一样,它负责执行针对评论表的数据库操作。

在app/controllers/目录下新建一个名为comments_controller.rb的文件:

macOS / Linux 使用:

touch app/controllers/comments_controller.rb

Windows 使用:

ni app/controllers/comments_controller.rb

打开这个空文件,粘贴以下代码:

class CommentsController < ApplicationController before_action :set_idea, only: %i[create destroy] before_action :set_comment, only: %i[destroy] def create @comment = @idea.comments.new(comment_params) if @comment.save redirect_to idea_path(@idea), notice: "Comment was successfully created." else render :new, status: :unprocessable_entity end end def destroy @comment.destroy redirect_to idea_path(@idea), notice: "Comment was successfully destroyed." end private def set_idea @idea = Idea.find(params[:idea_id]) end def set_comment @comment = @idea.comments.find(params[:id]) end def comment_params params.require(:comment).permit(:user_name, :body) end end

逐段解读控制器

  • before_action :set_idea, only: %i[create destroy]:在create和destroy动作执行之前,先从params[:idea_id]找到对应的 Idea 对象存入@idea。由于路由是嵌套的,请求 URL 中天然带有idea_id参数。
  • before_action :set_comment, only: %i[destroy]:在删除动作前,通过@idea.comments.find(params[:id])精确取出属于该 idea 的评论——注意是通过关联查找而非直接Comment.find,这样即使有人篡改 URL 中的 id,也无法删除不属于该 idea 的评论。
  • create:用@idea.comments.new(comment_params)基于当前 idea 构建一条新评论(自动填好idea_id),保存成功则带着绿色提示信息重定向回 idea 详情页;保存失败(例如字段校验未通过)则返回422 Unprocessable Entity状态码并重新渲染表单。
  • destroy:删除评论后重定向回 idea 详情页,并显示删除成功提示。
  • comment_params:这是 Rails 的**强参数(Strong Parameters)**机制,只允许:user_name和:body两个字段通过表单提交进入模型,防止恶意用户注入其他字段。

整个控制器只监听两类请求:创建评论和删除评论。收到请求后它告诉数据库存什么、删什么,然后把浏览器重定向回来源页面。

教练小贴士

这一部分非常适合由现场教练(coach)展开讲解:控制器如何与 HTTP 请求、模型、视图三者交互,一次完整的"浏览器发请求 → 路由分发 → 控制器处理 → 模型读写数据库 → 视图渲染 HTML → 浏览器显示"链路是怎样的。Rails Girls 的指南在网页端通过{% coach %}标签渲染为醒目的教练提示框,该标签的实现可以在 _plugins/coach.rb 中看到。

第六步:在 idea 详情页展示评论

现在数据已经能加载到@comments,接下来把它们显示在页面上。

打开app/views/ideas/show.html.erb,在文件最底部追加以下代码:

<h2>Comments</h2> <% if @comments.any? %> <% @comments.each do |comment| %> <div> <p><strong><%= comment.user_name %></strong></p> <p><%= comment.body %></p> <%= button_to "Destroy this comment", idea_comment_path(@idea, comment), method: :delete, class: "btn btn-danger", form: { data: { turbo_confirm: "Are you sure?" } } %> </div> <% end %> <% else %> <p>No comments found.</p> <% end %> <h2>Add a new comment</h2> <%= render partial: "comments/form", locals: { idea: @idea } %>

这段 ERB 代码做了四件事:

  1. 用@comments.any?判断是否存在评论,存在则循环渲染每一条;
  2. 每条评论显示留言者姓名(comment.user_name,加粗)和正文(comment.body);
  3. 用button_to生成一个删除按钮,指向idea_comment_path(@idea, comment)(即嵌套路由中的删除 URL),以DELETE方法提交,并带上 Bootstrap 的危险按钮样式与 Turbo 确认弹窗(turbo_confirm);
  4. 最后两行渲染一个评论提交表单——这是我们下一步要创建的内容。

第七步:创建评论提交表单

要让访客能提交评论,我们需要一个表单。在app/views/下新建comments/目录,并在其中创建名为_form.html.erb的局部模板(partial):

macOS / Linux 使用:

mkdir -p app/views/comments/ touch app/views/comments/_form.html.erb

Windows 使用:

md app/views/comments/ ni app/views/comments/_form.html.erb

在新文件中粘贴以下代码:

<%= form_with(model: [idea, idea.comments.build]) do |form| %> <div class="mb-3"> <%= form.label :user_name, "Your name", class: "form-label" %> <%= form.text_field :user_name, class: "form-control" %> </div> <div class="mb-3"> <%= form.label :body, "Comment message", class: "form-label" %> <%= form.text_area :body, class: "form-control" %> </div> <%= form.submit class: "btn btn-primary" %> <% end %>

表单的要点

  • form_with(model: [idea, idea.comments.build]):这是 Rails 6+ 时代的表单构造方式。传入的数组[idea, idea.comments.build]让 Rails 自动推导出提交地址——因为 idea 是已存在资源、comment 是新建对象,它会自动指向嵌套路由POST /ideas/:idea_id/comments。idea.comments.build会基于当前 idea 构建一个内存中的空评论对象,用于表单字段的绑定。
  • form.text_field :user_name与form.text_area :body:分别渲染姓名输入框和正文多行文本框,并带有 Bootstrap 的form-label/form-control样式类。
  • form.submit:渲染提交按钮,按钮文字默认显示为 "Create comment"。

验证成果

保存所有文件后,刷新浏览器打开任意 idea 的详情页,你应该能看到:

  • 页面底部出现 "Add a new comment" 区域和完整的表单;
  • 填入姓名和留言内容,点击 "Create comment" 按钮;
  • 页面顶部以绿色文字提示 "Comment was successfully created.",新评论出现在 "Comments" 区块中。

再访问另一条不同 idea 的详情页,你会发现刚才的评论不会出现在那里——这正是嵌套路由、idea_id外键和has_many/belongs_to关联共同作用的结果。

回顾:我们亲手写出了什么

至此,评论功能全部完成。回顾整个实现,我们通过手写代码而非 scaffold 完成了三层工作:

层次文件职责
路由config/routes.rb声明resources :comments嵌套于resources :ideas之下,让 URL 携带归属信息
模型app/models/idea.rb/app/models/comment.rbhas_many :comments与belongs_to :idea构成一对多关联
数据库db/migrate/迁移文件通过idea:references建立idea_id外键
控制器app/controllers/comments_controller.rb处理评论的创建与删除,配合强参数过滤
视图app/views/ideas/show.html.erb+app/views/comments/_form.html.erb展示评论列表、删除按钮与提交表单

与之前使用rails generate scaffold idea ...的指南(见 _pages/app.md)相比,本篇刻意减少了生成器的使用:模型由你亲自生成并理解字段含义,控制器完全手写,视图手动拼接。这种"少脚手架、多手写"的方式,能让你更清晰地看到 MVC 各个部件的真实协作方式——这恰恰是 Rails Girls 系列指南希望传递的核心价值:理解比魔法更重要。

如果你还想继续深入,指南列表中接下来的两篇非常值得学习:_pages/thumbnails.md(为图片上传生成缩略图)和 _pages/testing-rspec.md(用 RSpec 为应用编写测试)。

  • 教程
  • 后端

【免费下载链接】guides.railsgirls.com

Rails Girls Guides

项目地址:https://gitcode.com/gh_mirrors/gu/guides.railsgirls.com
点击查看免费下载
上一篇:FerretDB v1.8.0 发布解析:`$group` 表达式、`$expr` 查询操作符与 SQLite 后端改进
下一篇:EMQX MQTT 桥接 `$queue/` 订阅消息丢失修复:Subscription-Identifier 在 MQ 投递链路中的完整解析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询