1 什么是 Action View?
Action View 是 MVC 中的 V。 Action Controller 和 Action View 协同工作以处理 Web 请求。Action Controller 负责与模型层(MVC)进行通信并检索数据。然后,Action View 负责使用该数据向 Web 请求渲染响应主体。
默认情况下,Action View 模板(也简称为“视图”)使用嵌入式 Ruby (ERB) 编写,允许在 HTML 文档中使用 Ruby 代码。
Action View 为动态生成表单、日期和字符串的 HTML 标签提供了许多 辅助方法。还可以根据需要向应用程序添加自定义辅助方法。
Action View 可以利用 Active Model 的功能,例如 to_param
和 to_partial_path
来简化代码。但这并不意味着 Action View 依赖于 Active Model。Action View 是一个独立的包,可以与任何 Ruby 库一起使用。
2 在 Rails 中使用 Action View
Action View 模板(又名“视图”)存储在 app/views
目录中的子目录中。每个控制器都有一个与其名称匹配的子目录。该子目录内的视图文件用于渲染特定视图以响应控制器操作。
例如,当您使用脚手架生成 article
资源时,Rails 会在 app/views/articles
中生成以下文件
$ bin/rails generate scaffold article
[...]
invoke scaffold_controller
create app/controllers/articles_controller.rb
invoke erb
create app/views/articles
create app/views/articles/index.html.erb
create app/views/articles/edit.html.erb
create app/views/articles/show.html.erb
create app/views/articles/new.html.erb
create app/views/articles/_form.html.erb
[...]
文件名遵循 Rails 命名约定。它们与关联的控制器操作共享相同的名称。例如 index.html.erb
、edit.html.erb
等。
通过遵循此命名约定,Rails 会在控制器操作结束时自动查找并渲染匹配的视图,而无需您显式指定它。例如,articles_controller.rb
中的 index
操作将自动渲染 app/views/articles/
目录内的 index.html.erb
视图。文件名及其位置都很重要。
返回给客户端的最终 HTML 由 .html.erb
ERB 文件、一个将其包装的布局模板以及 ERB 文件可能引用的所有部分组合而成。在本指南的其余部分,您将找到有关这三个组件的更多详细信息:模板
、部分
、布局
。
3 模板
Action View 模板可以用不同的格式编写。如果模板文件具有 .erb
扩展名,则它使用嵌入式 Ruby 来构建 HTML 响应。如果模板具有 .jbuilder
扩展名,则它使用 Jbuilder gem 来构建 JSON 响应。具有 .builder
扩展名的模板使用 Builder::XmlMarkup
库来构建 XML 响应。
Rails 使用文件扩展名来区分多个模板系统。例如,使用 ERB 模板系统的 HTML 文件将具有 .html.erb
作为文件扩展名,而使用 Jbuilder 模板系统的 JSON 文件将具有 .json.jbuilder
文件扩展名。其他库也可能会添加其他模板类型和文件扩展名。
3.1 ERB
ERB 模板是一种在静态 HTML 中使用特殊 ERB 标签(如 <% %>
和 <%= %>
)来添加 Ruby 代码的方法。
当 Rails 处理以 .html.erb
结尾的 ERB 视图模板时,它会评估嵌入式 Ruby 代码并将 ERB 标签替换为动态输出。该动态内容与静态 HTML 标记相结合,形成最终的 HTML 响应。
在 ERB 模板中,可以使用 <% %>
和 <%= %>
标签来包含 Ruby 代码。<% %>
标签(没有 =
)用于您想要执行 Ruby 代码但不直接输出结果的情况,例如条件或循环。<%= %>
标签用于生成输出的 Ruby 代码,并且您希望该输出在模板中渲染,例如本例中的模型属性 person.name
<h1>Names</h1>
<% @people.each do |person| %>
Name: <%= person.name %><br>
<% end %>
循环使用常规嵌入标签(<% %>
)设置,名称使用输出嵌入标签(<%= %>
)插入。
请注意,诸如 print
和 puts
之类的函数不会使用 ERB 模板渲染到视图中。因此,像这样使用将不起作用
<%# WRONG %>
Hi, Mr. <% puts "Frodo" %>
上面的示例显示了如何在 <%# %>
标签中添加 ERB 中的注释。
要抑制前导和尾随空格,您可以使用 <%-
-%>
与 <%
和 %>
互换使用。
3.2 Jbuilder
Jbuilder
是一个由 Rails 团队维护的 gem,包含在默认的 Rails Gemfile
中。它用于使用模板构建 JSON 响应。
如果您没有它,可以将以下内容添加到您的 Gemfile
中
gem "jbuilder"
名为 json
的 Jbuilder
对象会自动提供给具有 .jbuilder
扩展名的模板。
这是一个基本示例
json.name("Alex")
json.email("[email protected]")
将生成
{
"name": "Alex",
"email": "[email protected]"
}
请参阅 Jbuilder 文档 以获取更多示例。
3.3 Builder
Builder 模板是 ERB 的更具编程性的替代方案。它类似于 Jbuilder
,但用于生成 XML 而不是 JSON。
名为 xml
的 XmlMarkup
对象会自动提供给具有 .builder
扩展名的模板。
这是一个基本示例
xml.em("emphasized")
xml.em { xml.b("emph & bold") }
xml.a("A Link", "href" => "https://rubyonrails.net.cn")
xml.target("name" => "compile", "option" => "fast")
这将生成
<em>emphasized</em>
<em><b>emph & bold</b></em>
<a href="https://rubyonrails.net.cn">A link</a>
<target option="fast" name="compile" />
任何带有块的方法都将被视为一个 XML 标记,其中嵌套了块中的标记。例如,以下代码
xml.div {
xml.h1(@person.name)
xml.p(@person.bio)
}
将生成类似以下内容:
<div>
<h1>David Heinemeier Hansson</h1>
<p>A product of Danish Design during the Winter of '79...</p>
</div>
请参阅 Builder 文档 以获取更多示例。
3.4 模板编译
默认情况下,Rails 会将每个模板编译成一个方法来渲染它。在开发环境中,当您更改模板时,Rails 会检查文件的修改时间并重新编译它。
还有片段缓存,用于当页面的不同部分需要分别缓存和过期时。在 缓存指南 中了解更多信息。
4 部分
部分模板(通常简称为“部分”)是一种将视图模板分解成更小的可重用块的方法。使用部分,您可以将主模板中的一部分代码提取到一个单独的较小的文件中,并在主模板中渲染该文件。您还可以从主模板将数据传递给部分文件。
让我们通过一些示例来看看它是如何工作的
4.1 渲染部分
要在视图中渲染部分,您可以在视图中使用 render
方法
<%= render "product" %>
这将在同一个文件夹中查找名为 _product.html.erb
的文件以在该视图中渲染。部分文件名以领先的下划线字符开头,这是惯例。文件名区分部分和常规视图。但是,当在视图中引用部分进行渲染时,不会使用下划线。即使您从另一个目录引用部分,也是如此
<%= render "application/product" %>
该代码将在 app/views/application/
中查找并显示名为 _product.html.erb
的部分文件。
4.2 使用部分来简化视图
使用部分的一种方法是将它们视为方法的等效项。将详细信息从视图中移出的一种方法,以便您可以更轻松地理解正在发生的事情。例如,您可能有一个看起来像这样的视图
<%= render "application/ad_banner" %>
<h1>Products</h1>
<p>Here are a few of our fine products:</p>
<% @products.each do |product| %>
<%= render partial: "product", locals: { product: product } %>
<% end %>
<%= render "application/footer" %>
这里,_ad_banner.html.erb
和 _footer.html.erb
部分可能包含应用中许多页面共享的内容。当您专注于“产品”页面时,您不需要查看这些部分的详细信息。
上面的示例还使用了 _product.html.erb
部分。此部分包含用于渲染单个产品的详细信息,并用于渲染集合 @products
中的每个产品。
4.3 使用 locals
选项将数据传递给部分
渲染部分时,可以从渲染视图将数据传递给部分。为此,您可以使用 locals:
选项哈希。locals:
选项中的每个键都可作为部分局部变量使用。
<%# app/views/products/show.html.erb %>
<%= render partial: "product", locals: { my_product: @product } %>
<%# app/views/products/_product.html.erb %>
<%= tag.div id: dom_id(my_product) do %>
<h1><%= my_product.name %></h1>
<% end %>
“部分局部变量”是在给定部分内局部且仅从该部分内可用的变量。在上面的示例中,my_product
是部分局部变量。在从原始视图传递给部分时,它被分配了 @product
的值。
请注意,通常我们只会将此局部变量称为 product
。在此示例中,我们使用 my_product
来区分它与实例变量名和模板名。
由于 locals
是哈希,因此您可以根据需要传递多个变量,例如 locals: { my_product: @product, my_reviews: @reviews }
。
但是,如果模板引用了未作为 locals:
选项的一部分传递给视图的变量,则模板将引发 ActionView::Template::Error
。
<%# app/views/products/_product.html.erb %>
<%= tag.div id: dom_id(my_product) do %>
<h1><%= my_product.name %></h1>
<%# => raises ActionView::Template::Error for `product_reviews` %>
<% product_reviews.each do |review| %>
<%# ... %>
<% end %>
<% end %>
4.4 使用 local_assigns
每个部分都有一个名为 local_assigns 的方法可用。您可以使用此方法访问通过 locals:
选项传递的键。如果部分不是使用 :some_key
设置渲染的,则 local_assigns[:some_key]
的值将在部分内为 nil
。
例如,在以下示例中,product_reviews
为 nil
,因为只有 product
设置在 locals:
中
<%# app/views/products/show.html.erb %>
<%= render partial: "product", locals: { product: @product } %>
<%# app/views/products/_product.html.erb %>
<% local_assigns[:product] # => "#<Product:0x0000000109ec5d10>" %>
<% local_assigns[:product_reviews] # => nil %>
local_assigns
的一个用例是可选地传递一个局部变量,然后根据局部变量是否已设置,在部分中条件地执行操作。例如
<% if local_assigns[:redirect] %>
<%= form.hidden_field :redirect, value: true %>
<% end %>
Active Storage 的 _blob.html.erb
中的另一个示例。此示例根据渲染包含此行的部分时是否设置了 in_gallery
局部变量来设置大小
<%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
4.5 不带 partial
和 locals
选项的 render
在上面的示例中,render
接受 2 个选项:partial
和 locals
。但是,如果这些是您唯一需要使用的选项,则可以跳过键 partial
和 locals
,并仅指定值。
例如,而不是
<%= render partial: "product", locals: { product: @product } %>
您可以写
<%= render "product", product: @product %>
您也可以根据约定使用这种简写
<%= render @product %>
这将在 app/views/products/
中查找名为 _product.html.erb
的部分,并将名为 product
的局部变量设置为值 @product
。
4.6 as
和 object
选项
默认情况下,传递给模板的对象位于与模板同名的局部变量中。所以,给定
<%= render @product %>
在 _product.html.erb
部分内,您将在局部变量 product
中获得 @product
实例变量,就好像您写了一样
<%= render partial: "product", locals: { product: @product } %>
object
选项可用于指定不同的名称。当模板的对象在别处(例如在不同的实例变量或局部变量中)时,这很有用。
例如,而不是
<%= render partial: "product", locals: { product: @item } %>
您可以写
<%= render partial: "product", object: @item %>
这将实例变量 @item
分配给名为 product
的部分局部变量。如果您想将局部变量名从默认的 product
更改为其他名称怎么办?您可以为此使用 :as
选项。
使用 as
选项,您可以像这样为局部变量指定不同的名称
<%= render partial: "product", object: @item, as: "item" %>
这等效于
<%= render partial: "product", locals: { item: @item } %>
4.7 渲染集合
视图通常会迭代集合,例如 @products
,并为集合中的每个对象渲染部分模板。此模式已实现为一个接受数组并为数组中的每个元素渲染部分的单一方法。
所以这个渲染所有产品的例子
<% @products.each do |product| %>
<%= render partial: "product", locals: { product: product } %>
<% end %>
可以重写为一行
<%= render partial: "product", collection: @products %>
当部分使用集合调用时,部分的单个实例可以通过以部分命名的变量访问正在渲染的集合成员。在本例中,由于部分是 _product.html.erb
,因此您可以使用 product
来引用正在渲染的集合成员。
您也可以使用以下约定基于简写语法来渲染集合。
<%= render @products %>
以上假设 @products
是 Product
实例的集合。Rails 使用命名约定通过查看集合中的模型名称(在本例中为 Product
)来确定要使用的部分名称。事实上,您甚至可以使用这种简写语法渲染由不同模型的实例组成的集合,Rails 会为集合中的每个成员选择合适的部分。
4.8 间隔模板
您还可以使用 :spacer_template
选项指定第二个部分,该部分将在主要部分的实例之间渲染
<%= render partial: @products, spacer_template: "product_ruler" %>
Rails 将在每个 _product.html.erb
部分对之间渲染 _product_ruler.html.erb
部分(没有任何数据传递给它)。
4.9 计数器变量
Rails 还使集合调用的部分内可以使用计数器变量。变量名称是部分标题后跟 _counter
。例如,渲染集合 @products
时,部分 _product.html.erb
可以访问变量 product_counter
。该变量索引部分在封闭视图中渲染的次数,从第一次渲染时的值 0
开始。
<%# index.html.erb %>
<%= render partial: "product", collection: @products %>
<%# _product.html.erb %>
<%= product_counter %> # 0 for the first product, 1 for the second product...
这在使用 as:
选项更改局部变量名时也适用。因此,如果您做了 as: :item
,则计数器变量将是 item_counter
。
注意:以下两节,“严格的局部变量”和“带有模式匹配的局部变量”是使用部分的更高级功能,为了完整性而包含在此处。
4.10 带有模式匹配的 local_assigns
由于 local_assigns
是一个 Hash
,因此它与 Ruby 3.1 的模式匹配赋值运算符 兼容
local_assigns => { product:, **options }
product # => "#<Product:0x0000000109ec5d10>"
options # => {}
当除 :product
之外的键分配到部分局部 Hash
变量时,它们可以被 splat 到辅助方法调用中
<%# app/views/products/_product.html.erb %>
<% local_assigns => { product:, **options } %>
<%= tag.div id: dom_id(product), **options do %>
<h1><%= product.name %></h1>
<% end %>
<%# app/views/products/show.html.erb %>
<%= render "products/product", product: @product, class: "card" %>
<%# => <div id="product_1" class="card">
# <h1>A widget</h1>
# </div>
%>
模式匹配赋值也支持变量重命名
local_assigns => { product: record }
product # => "#<Product:0x0000000109ec5d10>"
record # => "#<Product:0x0000000109ec5d10>"
product == record # => true
您也可以有条件地读取变量,然后在键不属于 locals:
选项的一部分时使用 fetch
回退到默认值
<%# app/views/products/_product.html.erb %>
<% local_assigns.fetch(:related_products, []).each do |related_product| %>
<%# ... %>
<% end %>
将 Ruby 3.1 的模式匹配赋值与调用 Hash#with_defaults 相结合,可以实现紧凑的部分局部默认变量赋值
<%# app/views/products/_product.html.erb %>
<% local_assigns.with_defaults(related_products: []) => { product:, related_products: } %>
<%= tag.div id: dom_id(product) do %>
<h1><%= product.name %></h1>
<% related_products.each do |related_product| %>
<%# ... %>
<% end %>
<% end %>
4.11 严格局部变量
Action View 部分在幕后编译成普通的 Ruby 方法。由于在 Ruby 中不可能动态创建局部变量,因此传递给部分的每个 locals
组合都需要编译另一个版本
<%# app/views/articles/show.html.erb %>
<%= render partial: "article", layout: "box", locals: { article: @article } %>
<%= render partial: "article", layout: "box", locals: { article: @article, theme: "dark" } %>
上面的代码片段将导致部分被编译两次,占用更多时间并使用更多内存。
def _render_template_2323231_article_show(buffer, local_assigns, article:)
# ...
end
def _render_template_3243454_article_show(buffer, local_assigns, article:, theme:)
# ...
end
当组合数量较少时,这并不是什么大问题,但如果数量很大,它可能会浪费相当数量的内存,并需要很长时间才能编译。为了解决这个问题,您可以使用严格的局部变量来定义编译后的部分签名,并确保只编译部分的单个版本
<%# locals: (article:, theme: "light") -%>
...
您可以使用与 Ruby 方法签名相同的语法,使用 locals:
签名来强制执行模板接受多少个 locals
以及哪些 locals
,设置默认值等等。
以下是 locals:
签名的几个示例
<%# app/views/messages/_message.html.erb %>
<%# locals: (message:) -%>
<%= message %>
以上使得 message
成为必需的局部变量。在没有 :message
局部变量参数的情况下渲染部分将引发异常
render "messages/message"
# => ActionView::Template::Error: missing local: :message for app/views/messages/_message.html.erb
如果设置了默认值,则如果 message
未在 locals:
中传递,则可以使用它
<%# app/views/messages/_message.html.erb %>
<%# locals: (message: "Hello, world!") -%>
<%= message %>
在没有 :message
局部变量的情况下渲染部分使用 locals:
签名中设置的默认值
render "messages/message"
# => "Hello, world!"
在 local:
签名中未指定局部变量的情况下渲染部分也会引发异常
render "messages/message", unknown_local: "will raise"
# => ActionView::Template::Error: unknown local: :unknown_local for app/views/messages/_message.html.erb
您可以使用双 splat **
运算符允许可选的局部变量参数
<%# app/views/messages/_message.html.erb %>
<%# locals: (message: "Hello, world!", **attributes) -%>
<%= tag.p(message, **attributes) %>
或者,您可以通过将 locals:
设置为空 ()
来完全禁用 locals
<%# app/views/messages/_message.html.erb %>
<%# locals: () %>
使用任何局部变量参数渲染部分将引发异常
render "messages/message", unknown_local: "will raise"
# => ActionView::Template::Error: no locals accepted for app/views/messages/_message.html.erb
Action View 将在支持 #
开头的注释的任何模板引擎中处理 locals:
签名,并将从部分中的任何一行读取签名。
仅支持关键字参数。定义位置参数或块参数将在渲染时引发 Action View 错误。
local_assigns
方法不包含 local:
签名中指定的默认值。要访问与保留的 Ruby 关键字(如 class
或 if
)同名的具有默认值的局部变量,可以通过 binding.local_variable_get
访问其值
<%# locals: (class: "message") %>
<div class="<%= binding.local_variable_get(:class) %>">...</div>
5 布局
布局可用于围绕 Rails 控制器操作的结果渲染常见的视图模板。Rails 应用程序可以拥有多个页面可以在其中渲染的布局。
例如,应用程序可能为已登录用户提供一个布局,为网站的营销部分提供另一个布局。已登录用户的布局可能包含应该在许多控制器操作中出现的顶级导航。SaaS 应用的销售布局可能包含用于“定价”和“联系我们”页面等内容的顶级导航。不同的布局可以具有不同的页眉和页脚内容。
要查找当前控制器操作的布局,Rails 首先在 app/views/layouts
中查找与控制器同名的基本名称的文件。例如,渲染来自 ProductsController
类的操作将使用 app/views/layouts/products.html.erb
。
如果不存在特定于控制器的布局,Rails 将使用 app/views/layouts/application.html.erb
。
以下是在 application.html.erb
文件中简单布局的示例
<!DOCTYPE html>
<html>
<head>
<title><%= "Your Rails App" %></title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>
<body>
<nav>
<ul>
<li><%= link_to "Home", root_path %></li>
<li><%= link_to "Products", products_path %></li>
<!-- Additional navigation links here -->
</ul>
</nav>
<%= yield %>
<footer>
<p>© <%= Date.current.year %> Your Company</p>
</footer>
在上面的示例布局中,视图内容将在 <%= yield %>
的位置渲染,并被相同的 <head>
、<nav>
和 <footer>
内容包围。
Rails 提供了更多方法将特定布局分配给单个控制器和操作。您可以在 Rails 中的布局和渲染 指南中详细了解布局。
5.1 部分布局
部分可以应用自己的布局。这些布局与应用于控制器操作的布局不同,但它们的工作方式相似。
假设您在一个页面上显示一篇文章,该文章应该被包裹在一个 div
中以供显示。首先,您将创建一个新的 Article
Article.create(body: "Partial Layouts are cool!")
在 show
模板中,您将渲染被 box
布局包裹的 _article
部分
<%# app/views/articles/show.html.erb %>
<%= render partial: 'article', layout: 'box', locals: { article: @article } %>
box
布局只是将 _article
部分包裹在一个 div
中
<%# app/views/articles/_box.html.erb %>
<div class="box">
<%= yield %>
</div>
请注意,局部布局可以访问传递到 `render` 调用中的本地 `article` 变量,尽管在本例中它没有在 `_box.html.erb` 中使用。
与应用程序范围的布局不同,局部布局在名称中仍然带有下划线前缀。
您也可以在局部布局中渲染代码块,而不是调用 `yield`。例如,如果您没有 `_article` 部分,则可以这样做
<%# app/views/articles/show.html.erb %>
<%= render(layout: 'box', locals: { article: @article }) do %>
<div>
<p><%= article.body %></p>
</div>
<% end %>
假设您使用的是上面相同的 `_box` 部分,这将生成与上一个示例相同的输出。
5.2 带有局部布局的集合
渲染集合时,也可以使用 `:layout` 选项
<%= render partial: "article", collection: @articles, layout: "special_layout" %>
布局将与集合中每个项目的局部一起渲染。当前对象和对象计数器变量(在上面的示例中为 `article` 和 `article_counter`)将在布局中可用,就像它们在局部中一样。
6 助手
Rails 提供了许多助手方法来与 Action View 一起使用。这些方法包括
- 格式化日期、字符串和数字
- 创建指向图像、视频、样式表等的 HTML 链接...
- 清理内容
- 创建表单
- 本地化内容
您可以在 Action View 助手指南 和 Action View 表单助手指南 中了解更多关于助手的知识。
7 本地化视图
Action View 能够根据当前区域设置渲染不同的模板。
例如,假设您有一个带有 `show` 操作的 `ArticlesController`。默认情况下,调用此操作将渲染 `app/views/articles/show.html.erb`。但如果您设置 `I18n.locale = :de`,那么 Action View 将首先尝试渲染模板 `app/views/articles/show.de.html.erb`。如果本地化模板不存在,将使用未修饰的版本。这意味着您不需要为所有情况提供本地化视图,但如果有,它们将被优先使用。
您可以使用相同的技术来本地化公共目录中的救援文件。例如,设置 `I18n.locale = :de` 并创建 `public/500.de.html` 和 `public/404.de.html` 将允许您拥有本地化的救援页面。
有关更多详细信息,请参见 Rails 国际化 (I18n) API 文档。