更多内容请访问 rubyonrails.org:

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_paramto_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.erbedit.html.erb 等。

通过遵循此命名约定,Rails 将在控制器动作结束时自动查找并渲染匹配的视图,而无需您指定。例如,articles_controller.rb 中的 index 动作将自动渲染 app/views/articles/ 目录中的 index.html.erb 视图。文件的名称和位置都很重要。

返回给客户端的最终 HTML 由 .html.erb ERB 文件、包裹它的布局模板以及 ERB 文件可能引用的所有局部视图组合而成。在本指南的其余部分,您将找到有关这三个组件的更多详细信息:TemplatesPartialsLayouts

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 模板是一种使用特殊 ERB 标签(如 <% %><%= %>)在静态 HTML 中嵌入 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 %>

循环使用常规嵌入标签 (<% %>) 设置,名称使用输出嵌入标签 (<%= %>) 插入。

请注意,像 printputs 这样的函数不会在 ERB 模板中渲染到视图。所以像这样是行不通的

<%# WRONG %>
Hi, Mr. <% puts "Frodo" %>

上面的例子表明注释可以在 ERB 中通过 <%# %> 标签添加。

要抑制前导和尾随空格,您可以将 <%- -%><%%> 互换使用。

3.2. Jbuilder

Jbuilder 是由 Rails 团队维护并包含在默认 Rails Gemfile 中的 gem。它用于使用模板构建 JSON 响应。

如果您没有,可以将其添加到您的 Gemfile

gem "jbuilder"

一个名为 jsonJbuilder 对象会自动提供给带有 .jbuilder 扩展名的模板。

这是一个基本示例

json.name("Alex")
json.email("alex@example.com")

将生成

{
  "name": "Alex",
  "email": "alex@example.com"
}

有关更多示例,请参阅 Jbuilder 文档

3.3. Builder

Builder 模板是 ERB 的一种更具编程性的替代方案。它类似于 JBuilder,但用于生成 XML,而不是 JSON。

一个名为 xmlXmlMarkup 对象会自动提供给带有 .builder 扩展名的模板。

这是一个基本示例

xml.em("emphasized")
xml.em { xml.b("emph & bold") }
xml.a("A Link", "href" => "https://rubyonrails.cn")
xml.target("name" => "compile", "option" => "fast")

将生成

<em>emphasized</em>
<em><b>emph &amp; bold</b></em>
<a href="https://rubyonrails.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_reviewsnil,因为只有 productlocals: 中设置

<%# 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. 不带 partiallocals 选项的 render

在上面的示例中,render 接受 2 个选项:partiallocals。但是,如果这些是您唯一需要使用的选项,则可以跳过键 partiallocals,只指定值。

例如,不是

<%= render partial: "product", locals: { product: @product } %>

你可以这样写

<%= render "product", product: @product %>

您还可以根据约定使用此速记

<%= render @product %>

这将查找 app/views/products/ 中名为 _product.html.erb 的局部视图,并传递一个名为 product 的局部变量,其值为 @product

4.6. asobject 选项

默认情况下,传递给模板的对象位于与模板同名的局部变量中。因此,给定

<%= 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 %>

上述假设 @productsProduct 实例的集合。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 变量时,它们可以被展开到助手方法调用中

<%# 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: 签名的一些示例

<%# 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

如果设置了默认值,那么在 locals: 中未传入 message 时可以使用它

<%# 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

您可以使用双星号 ** 运算符允许可选的局部变量参数


<%# 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 保留关键字(如 classif)相同的具有默认值的局部变量,可以通过 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>&copy; <%= 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" %>

布局将与集合中每个项目的局部视图一起渲染。当前对象和对象计数器变量(在上述示例中为 articlearticle_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.htmlpublic/404.de.html 将允许您拥有本地化的救援页面。

有关更多详细信息,请参阅 Rails 国际化 (I18n) API 文档



回到顶部