- 教程
- 后端
【免费下载链接】guides.railsgirls.com
Rails Girls Guides
本篇指南(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这条命令做了三件事:
- 创建一个名为
Comment的模型(对应app/models/comment.rb); - 声明两个内容字段:
user_name(字符串类型,记录留言者姓名)和body(文本类型,存放留言正文); - 通过
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 endhas_many :comments告诉 Rails:一个 Idea 对象可以关联多条 Comment 记录。加上这一行之后,你就可以直接在代码里写@idea.comments来取出属于这条 idea 的全部评论——Rails 会自动生成对应的数据库查询。
Comment 端:一条评论属于一条 idea
打开app/models/comment.rb,你会看到生成器已经自动写好了归属关系:
class Comment < ApplicationRecord belongs_to :idea endbelongs_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.rbWindows 使用:
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 代码做了四件事:
- 用
@comments.any?判断是否存在评论,存在则循环渲染每一条; - 每条评论显示留言者姓名(
comment.user_name,加粗)和正文(comment.body); - 用
button_to生成一个删除按钮,指向idea_comment_path(@idea, comment)(即嵌套路由中的删除 URL),以DELETE方法提交,并带上 Bootstrap 的危险按钮样式与 Turbo 确认弹窗(turbo_confirm); - 最后两行渲染一个评论提交表单——这是我们下一步要创建的内容。
第七步:创建评论提交表单
要让访客能提交评论,我们需要一个表单。在app/views/下新建comments/目录,并在其中创建名为_form.html.erb的局部模板(partial):
macOS / Linux 使用:
mkdir -p app/views/comments/ touch app/views/comments/_form.html.erbWindows 使用:
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.rb | has_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
相关推荐
Arya在线Markdown编辑器:揭秘免费专业写作工具的六大核心功能
Arya在线Markdown编辑器:揭秘免费专业写作工具的六大核心功能 在数字化写作日益普及的今天,Arya作为一款基于Vue2和Vditor构建的免费在线Ma
前端富文本Agent Starter Pack 安装指南:四种安装方式、升级卸载与故障排查全解
Agent Starter Pack 安装指南:四种安装方式、升级卸载与故障排查全解 Agent Starter Pack 是一套用于在 Google Clou
教程后端Ransack快速入门:5个步骤为你的Rails应用添加搜索功能
Ransack快速入门:5个步骤为你的Rails应用添加搜索功能 想要为你的Ruby on Rails应用添加强大的搜索功能吗?Ransack正是你需要的终极解
后端搜索引擎
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考