CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Ruby on Rails 实战入门/课时/表单
RawBlame
课时目录12 节

Ruby on Rails 实战入门

  1. 01Rails 概览与项目结构
  2. 02路由设计
  3. 03控制器
  4. 04Active Record 模型
  5. 05查询与优化
  6. 06数据库适配
  7. 07视图与组件
  8. 08表单
  9. 09认证与授权
  10. 10资产与后台任务
  11. 11测试与调试
  12. 12部署与运维

表单

form_with 与嵌套属性。

30 分钟

表单

这一课你将能做到

  1. 用 form_with 生成绑定模型的表单,新增/编辑复用同一模板
  2. 在视图中渲染字段级错误信息,配合 422 状态码完成「失败回填」体验
  3. 处理文件上传(Active Storage)和一对多嵌套表单

为什么要学

表单是用户向系统输入数据的唯一入口,也是安全问题最密集的地方:CSRF、批量赋值、XSS、文件上传漏洞全在这一课交汇。Rails 的 form_with 把「生成正确字段名、带 CSRF 令牌、回填旧值、显示错误」这些琐事全包了——你要做的是理解它包了什么,才能在出错时知道去哪查。

对比本站:Next.js 里你要手写受控组件、useState 管理每个字段、fetch 提交、处理后端返回的错误映射;Rails 全栈模式下这些由 form_with + Strong Parameters + errors 对象一条流水线完成,前端几乎零代码。

先建立图像

表单提交的完整回路:

GET /articles/new                POST /articles
      │                                │
      ▼                                ▼
@article = Article.new          @article.save ?
      │                            ╱      ╲
      ▼                          成功      失败
渲染 form_with                redirect   render :new (422)
字段名 article[title]         到详情页   errors 回填表单
      │                                │
      └──────── 用户提交 ──────────────┘

form_with 自动判断:模型是新记录(new_record?)→ 表单 POST 到 /articles;是已存在记录 → PATCH 到 /articles/:id。新增和编辑因此能共用同一个 _form.html.erb。

跟着做

步骤 1:基础表单

app/views/articles/_form.html.erb:

<%= form_with model: article do |form| %>
  <div class="field">
    <%= form.label :title, "标题" %>
    <%= form.text_field :title %>
  </div>

  <div class="field">
    <%= form.label :body, "正文" %>
    <%= form.text_area :body, rows: 10 %>
  </div>

  <div class="field">
    <%= form.label :published, "发布" %>
    <%= form.check_box :published %>
  </div>

  <%= form.submit "保存" %>
<% end %>

new 和 edit 视图都只有一行:

<%# app/views/articles/new.html.erb %>
<h1>新文章</h1>
<%= render "form", article: @article %>
<%# app/views/articles/edit.html.erb %>
<h1>编辑文章</h1>
<%= render "form", article: @article %>

生成的 HTML 关键部分:

<form action="/articles" method="post">
  <input type="hidden" name="authenticity_token" value="...">
  <input type="text" name="article[title]" id="article_title">
  <textarea name="article[body]" id="article_body"></textarea>
</form>

注意 article[title] 这种命名——这就是控制器里 params.require(:article).permit(:title, :body) 能工作的前提。编辑模式下 Rails 还会注入 <input type="hidden" name="_method" value="patch"> 模拟 PATCH。

步骤 2:渲染错误信息

<%= form_with model: article do |form| %>
  <% if article.errors.any? %>
    <div class="error-summary">
      <h3><%= pluralize(article.errors.count, "个错误") %>阻止了保存:</h3>
      <ul>
        <% article.errors.full_messages.each do |msg| %>
          <li><%= msg %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :title, "标题" %>
    <%= form.text_field :title %>
    <% article.errors[:title].each do |msg| %>
      <span class="field-error"><%= msg %></span>
    <% end %>
  </div>
  ...
<% end %>

控制器失败分支必须是 render :new, status: :unprocessable_entity——这样 @article.errors 和用户已填的值都在,form_with 会自动回填。Turbo 依赖 422 状态码识别「该替换页面内容」。

步骤 3:下拉、单选、日期等常用控件

<%= form.select :category, ["技术", "生活", "随笔"], { prompt: "请选择分类" } %>

<%= form.collection_select :author_id, Author.all, :id, :name %>

<%= form.radio_button :visibility, "public" %>
<%= form.label :visibility_public, "公开" %>
<%= form.radio_button :visibility, "private" %>
<%= form.label :visibility_private, "私密" %>

<%= form.date_field :published_on %>
<%= form.number_field :reading_minutes, min: 1 %>
<%= form.password_field :access_code %>

collection_select 从模型集合生成 option:参数依次是(值字段, 显示字段)的 id/name——别搞反。

步骤 4:文件上传(Active Storage)

Rails 内置文件上传方案,无需第三方 gem:

bin/rails active_storage:install
bin/rails db:migrate
# app/models/article.rb
class Article < ApplicationRecord
  has_one_attached :cover        # 一张封面图
end
<%= form_with model: article do |form| %>
  <%= form.label :cover, "封面图" %>
  <%= form.file_field :cover, accept: "image/*" %>
  <%= form.submit "保存" %>
<% end %>
# 控制器白名单放行 :cover
def article_params
  params.require(:article).permit(:title, :body, :published, :cover)
end

展示:

<% if article.cover.attached? %>
  <%= image_tag article.cover.variant(resize_to_limit: [800, 400]) %>
<% end %>

开发环境文件存本地 storage/,生产切 S3/七牛只需改 config/storage.yml 一行 service 配置——代码零改动。

步骤 5:嵌套表单(一对多)

文章里直接编辑多条标签:

class Article < ApplicationRecord
  has_many :tags, dependent: :destroy
  accepts_nested_attributes_for :tags, allow_destroy: true, reject_if: :all_blank
end
<%= form.fields_for :tags do |tag_form| %>
  <div class="nested-field">
    <%= tag_form.text_field :name, placeholder: "标签名" %>
    <%= tag_form.check_box :_destroy %> 删除
  </div>
<% end %>

白名单放行嵌套参数:

params.require(:article).permit(:title, :body, tags_attributes: [:id, :name, :_destroy])

注意 :id(已有记录需要)和 :_destroy(勾选删除)必须放行。

讲清楚要点

  1. form_with 的智能源于模型状态:new_record? 为 true 走 POST + create,否则走 PATCH + update,URL 和 method 自动推断。这就是约定优于配置在表单层的体现。
  2. 字段名 article[title] 是参数结构的契约:表单命名、Strong Parameters、模型赋值三者按同一份约定协作,一环脱节就报 ParameterMissing 或静默丢数据。
  3. CSRF 令牌自动注入:authenticity_token 隐藏字段由 form_with 生成,服务端自动校验。AJAX 请求从 csrf_meta_tags 读令牌带上。
  4. 失败回填 = render + 422 + errors 三件套:redirect 会丢失模型状态,render 保留;422 让 Turbo 正确处理;errors 对象驱动错误展示。
  5. Active Storage 抽象了存储后端:本地磁盘、S3、GCS 用同一套 API,切换只改配置。多文件用 has_many_attached。

你可能会懵的地方

  • form_with 和老式的 form_for 区别? form_for 是 Rails 5 之前的写法,必须显式给模型;form_with 是现在的统一入口,默认 data-remote(Turbo 接管)。新代码一律 form_with。

  • checkbox 提交时 Rails 为什么生成两个 input? 一个 hidden(value=0)一个 checkbox(value=1)。checkbox 不勾选时浏览器不提交该字段,hidden 兜底保证服务端收到 0——否则「取消勾选」无法保存。

  • file_field 上传后 @article.cover 是什么? 是 ActiveStorage::Attached::One 代理对象,不是文件本身。.attached? 判断有无、.variant 生成缩略图、.download 拿二进制、.url(配了 CDN)拿地址。

  • 嵌套表单太深(三层以上)怎么办? accepts_nested_attributes_for 只建议一层。深层嵌套用 cocoon gem(动态增删子表单)或拆成独立页面分步操作。

  • 表单对象(Form Object)是什么? 当一个表单涉及多个模型(如注册同时建 User + Profile),把表单逻辑抽成纯 Ruby 类(ActiveModel::Model),控制器只调 form.save。复杂表单的解耦手法,了解了概念,需要时再深入。

错法对照

错法问题正解
手写 <form> + <input name="title">字段名结构错、无 CSRF 令牌form_with model: 生成
校验失败后 redirect_to new错误和已填内容丢失render :new, status: :unprocessable_entity
permit 漏了 :cover文件静默丢失不报错白名单加上附件字段
嵌套表单没 permit :id更新已有子记录失败tags_attributes: [:id, :name, :_destroy]
上传不校验文件类型/大小任意文件上传漏洞accept 属性 + 模型 validate 内容类型和大小

卡点急救

  1. 提交报 ActionController::InvalidAuthenticityToken:CSRF 令牌缺失。检查是否手写了 form 没用 form_with;布局里 csrf_meta_tags 是否被删;清理浏览器 Cookie 重试。
  2. 文件上传后 attached? 是 false:八成是 permit 漏了附件字段名,或表单没写 file_field(form_with 检测到有附件字段会自动加 enctype)。
  3. 编辑表单提交后创建了新记录:form_with 收到的模型是新建对象而非数据库查出的。检查 edit 动作是否用 set_article 回调正确加载了记录。

面试常问

  • CSRF 是什么?Rails 怎么防? 答:跨站请求伪造——恶意网站诱导用户浏览器向目标站发请求(自动带 Cookie)。Rails 防御:每个表单/请求带 authenticity_token(与会话绑定),服务端校验不通过即拒绝。API 模式用 Token 认证则无需 CSRF 保护(不带 Cookie 就不受此威胁)。

  • form_with 如何区分新增和编辑? 答:看模型的 new_record?。新记录 → action 指向 collection 路径(POST /articles);持久化记录 → member 路径(PATCH /articles/:id),并注入 _method=patch 隐藏字段,因为 HTML 表单原生只支持 GET/POST,由 Rack 中间件还原成 PATCH。

  • 文件上传的安全要点? 答:校验内容类型(不轻信扩展名,校验 MIME/魔数)、限制大小、文件名重命名防路径穿越、存到 Web 根目录之外或用对象存储、图片用 variant 处理而不是直接暴露原图 URL。Active Storage 默认存 storage/ 目录(非 public),通过控制器代理访问,天然满足大半要求。

自测三问

  1. 为什么 new 和 edit 能共用同一个 _form.html.erb?
  2. 校验失败的正确响应三要素是什么?缺一不可的理由?
  3. 嵌套表单白名单里为什么必须放行 :id 和 :_destroy?

小结一句话

form_with 把字段命名、CSRF、回填、错误展示打包成约定,你只需守住白名单、记住失败 render 422,表单就不再有玄学。

延伸

  • Rails Form Helpers(官方表单指南)
  • 下一课:认证与授权——has_secure_password、Session 与权限控制。
加载本节练习…
← 上一课视图与组件下一课 →认证与授权