大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
视图与组件
这一课你将能做到
- 用 ERB 模板渲染数据,理解
<%= %>与<% %>的区别 - 用布局 + yield + content_for 组织页面骨架,用 partial 消除重复片段
- 说清楚 Rails 服务端渲染与 Next.js 客户端渲染的取舍,知道 Hotwire 补位交互
为什么要学
视图是用户唯一能看见的部分。Rails 的视图层哲学是「HTML 优先」:服务端把数据填进模板直接吐出完整 HTML,浏览器零 JS 也能用;需要交互的地方交给 Hotwire(Turbo + Stimulus)做渐进增强。这和本站的 Next.js 路线(浏览器下载 JS bundle → 客户端取数据渲染)是两种范式,各自有明确的适用边界——内容型、后台型产品服务端渲染交付更快,重交互应用才需要前端框架。
先建立图像
视图的套娃结构:
layouts/application.html.erb ← 最外层:html/head/body 骨架
└── yield ← 内容插槽
└── articles/index.html.erb ← 动作对应视图
└── render "article" ← partial 局部视图(每个文章卡片)
└── _article.html.erb
渲染顺序由内而外:partial → 动作视图 → 布局 → 完整 HTML 响应。
ERB 两种标签:
<% @articles.each do |a| %> ← 只执行,不输出
<%= a.title %> ← 执行并输出(HTML 转义)
<% end %>
跟着做
步骤 1:第一个视图
app/views/articles/index.html.erb:
<h1>文章列表</h1>
<ul>
<% @articles.each do |article| %>
<li>
<%= link_to article.title, article_path(article) %>
<small><%= article.created_at.strftime("%Y-%m-%d") %></small>
</li>
<% end %>
</ul>
<%= link_to "写新文章", new_article_path %>
控制器里 @articles = Article.all,视图直接用实例变量。文件名规则:动作名.格式.模板引擎——index.html.erb 意为「HTML 格式、ERB 引擎」。
步骤 2:布局与 yield
app/views/layouts/application.html.erb:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title><%= content_for(:title) || "我的博客" %></title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>
<body>
<nav><%= link_to "首页", root_path %></nav>
<% flash.each do |type, msg| %>
<div class="flash <%= type %>"><%= msg %></div>
<% end %>
<main><%= yield %></main>
<footer>© 2026</footer>
</body>
</html>
每个动作视图的内容会被塞进 yield 位置。子页面想定制标题:
<%# app/views/articles/show.html.erb %>
<% content_for :title, @article.title %>
<h1><%= @article.title %></h1>
csrf_meta_tags 输出 CSRF 令牌给 JS 用,stylesheet_link_tag/javascript_importmap_tags 挂资产——都是布局的标配。
步骤 3:partial 消除重复
列表页和搜索页都要渲染文章卡片,抽成 partial。文件名以下划线开头:
<%# app/views/articles/_article.html.erb %>
<article class="card">
<h2><%= link_to article.title, article_path(article) %></h2>
<p><%= truncate(article.body, length: 120) %></p>
</article>
使用:
<%# 单条渲染 %>
<%= render "article", article: @article %>
<%# 集合渲染:自动对每条记录调用一次 %>
<%= render @articles %>
render @articles 是 Rails 的约定魔法:按模型类名找到 _article.html.erb,循环渲染并把局部变量 article 依次赋值——等价于手写 each + render,但短得多。
步骤 4:helper 放视图逻辑
视图中不该有复杂 Ruby 逻辑,抽到 app/helpers/articles_helper.rb:
module ArticlesHelper
def status_badge(article)
if article.published?
tag.span "已发布", class: "badge badge-green"
else
tag.span "草稿", class: "badge badge-gray"
end
end
end
视图里一行调用:<%= status_badge(article) %>。tag.span 是内置的 HTML 生成器,比手写字符串安全(自动转义属性)。
步骤 5:了解组件化方向——ViewComponent
当 partial + helper 的组合在大型项目里失控(隐式依赖实例变量、难以测试),社区方案 ViewComponent(GitHub 开源)把视图封装成 Ruby 类:
# app/components/article_card_component.rb
class ArticleCardComponent < ViewComponent::Base
def initialize(article:)
@article = article
end
end
<%# app/components/article_card_component.html.erb %>
<article class="card">
<h2><%= link_to @article.title, article_path(@article) %></h2>
</article>
调用:<%= render ArticleCardComponent.new(article: @article) %>。显式传参、可单测——思路和 React 组件一致,但渲染发生在服务端。初学者先把 partial 用熟,项目大了再上组件库。
讲清楚要点
<%= %>输出并自动转义:<%= "<b>x</b>" %>会显示成文本<b>x</b>而不是加粗——这是 XSS 防线。确需输出可信 HTML 用raw()或.html_safe,但永远不要对用户输入这么做。- 实例变量是控制器到视图的桥:
@articles在控制器赋值,视图直接可用。partial 里用局部变量(render 时显式传),别依赖实例变量——隐式依赖让 partial 无法复用。 - 布局可多套:
layout "admin"切换布局;render layout: false不要布局(如弹窗片段)。 - helper 自动可用:
ArticlesHelper的方法在 articles 相关视图直接调用。所有 helper 默认全局可见,命名要清晰避免冲突。 - Hotwire 是视图层的交互答案:Turbo 把链接/表单提交变成局部替换(无刷新体验),Stimulus 是轻量 JS 控制器处理富交互。80% 的「想要 SPA 感」需求,Hotwire 就够,不必上 React。
你可能会懵的地方
-
ERB 和其他模板引擎(Haml/Slim)选哪个? ERB 是官方默认、文档最全、团队零学习成本。Haml/Slim 用缩进代替闭合标签,写起来快但可读性见仁见智。初学者用 ERB,别折腾。
-
render和render partial:区别?render "article"在视图里默认就是渲染 partial(找下划线文件);render template: "articles/special"才渲染完整模板。日常 99% 是 partial。 -
集合渲染怎么做分页? 分页是查询层的事:控制器里
@articles = Article.page(params[:page]).per(20)(kaminari 或 pagy gem),视图<%= render @articles %>照旧,加分页导航<%= paginate @articles %>。 -
视图里能直接查数据库吗? 语法上可以(
<% Article.count %>),但这是严重坏味道——视图查询绕过控制器,既难缓存又容易 N+1。所有数据在控制器备好。 -
什么时候该用 Next.js 而不是 Rails 视图? 交互密度是分水岭:拖拽编辑器、实时协作、复杂状态机——Next.js;CRUD、内容展示、表单流程、后台管理——Rails 视图 + Hotwire 更快更省。很多产品是混合架构:对外的营销页 Rails 渲染,核心交互应用 Next.js。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
<%= raw(comment.body) %> 渲染用户输入 | XSS 漏洞 | 默认 <%= %> 自动转义,富文本用 sanitize |
partial 里直接用 @article | 隐式依赖,别处无法复用 | render "article", article: a 显式传局部变量 |
视图里 <% Article.where... %> | 绕过控制器,易 N+1 | 数据在控制器查询 |
| 五层嵌套 if/else 在 ERB 里 | 逻辑混乱难测 | 抽 helper 或 presenter |
| 所有页面共用一套布局硬编码 | 后台/前台风格冲突 | 多布局 + layout "admin" |
卡点急救
ActionView::MissingTemplate:文件名不符合动作.格式.引擎约定,或放错目录(应在app/views/控制器名/下)。检查单复数。- partial 报
undefined local variable or method 'article':渲染时没传局部变量,补render "article", article: @article;集合渲染确认模型类名与 partial 文件名匹配。 - 修改视图不生效:生产/预编译环境模板有缓存,开发环境正常应即时生效;若在开发环境不生效,重启服务器并检查是否改对了环境对应的文件。
面试常问
-
Rails 服务端渲染和 React/Next.js 客户端渲染怎么选? 答:看交互密度和团队结构。内容/CRUD/后台类产品,Rails 服务端渲染 + Hotwire 交付快、SEO 天然友好、一套代码无 API 层;重交互(编辑器、实时协作)、多端复用数据、前端团队独立时,前后端分离更合适。成本模型:前者省一层 API 和状态同步,后者换来交互上限和团队并行。
-
ERB 的
<%= %>为什么默认转义? 答:防 XSS。用户输入若含<script>,转义后显示为文本而非执行。输出可信 HTML 需显式 raw/sanitize——把「危险操作」做成显式动作,是 Rails 的安全设计哲学。 -
partial 和 ViewComponent 的取舍? 答:partial 轻量、约定式,适合页面片段复用;但依赖隐式约定、无法独立测试。ViewComponent 显式传参、有 Ruby 类可单测、支持插槽,适合设计系统和大型项目。小项目 partial 足够,组件超过几十个考虑迁移。
自测三问
<% %>和<%= %>的区别?后者为什么天然防 XSS?<%= render @articles %>背后发生了什么约定推断?- 布局里的
yield和content_for分别解决什么问题?
小结一句话
Rails 视图 = 布局套模板、模板嵌 partial,默认转义保安全,数据永远来自控制器——HTML 优先,交互交给 Hotwire 增强。
延伸
- Action View Overview(官方视图指南)
- 下一课:表单——form_with、Strong Parameters 与文件上传。