1. 概览:各部分如何协同工作
本指南侧重于模型-视图-控制器(MVC)三元组中 Controller 和 View 之间的交互。如你所知,Controller 负责协调 Rails 中处理请求的整个过程,尽管它通常会将所有繁重的代码交给 Model。但是,当需要向用户发送响应时,Controller 会将事情交给 View。正是这种交接是本指南的主题。
概括地说,这涉及决定应发送什么作为响应,并调用适当的方法来创建该响应。如果响应是一个完整的视图,Rails 还会做一些额外的工作,将视图包装在一个布局中,并可能引入局部视图。你将在本指南后面看到所有这些路径。
2. 创建响应
从控制器的角度来看,有三种方法可以创建 HTTP 响应:
- 调用
render创建一个完整的响应发送回浏览器。 - 调用
redirect_to向浏览器发送 HTTP 重定向状态码。 - 调用
head创建一个仅包含 HTTP 头部的响应发送回浏览器。
2.1. 默认渲染:约定优于配置的实践
你已经听说过 Rails 推崇“约定优于配置”。默认渲染就是其中一个极好的例子。默认情况下,Rails 中的控制器会自动渲染与有效路由对应的视图。例如,如果你的 BooksController 类中有以下代码:
class BooksController < ApplicationController
end
并且你的路由文件中有以下内容:
resources :books
并且你有一个视图文件 app/views/books/index.html.erb:
<h1>Books are coming soon!</h1>
当你导航到 /books 时,Rails 会自动渲染 app/views/books/index.html.erb,你将在屏幕上看到“图书即将到来!”。
然而,“即将到来”屏幕的作用微乎其微,因此你很快就会创建 Book 模型并将 index 动作添加到 BooksController:
class BooksController < ApplicationController
def index
@books = Book.all
end
end
请注意,根据“约定优于配置”原则,我们在 index 动作的末尾没有显式调用 render。规则是,如果你在控制器动作的末尾没有显式渲染任何内容,Rails 将自动在控制器的视图路径中查找 action_name.html.erb 模板并渲染它。因此,在这种情况下,Rails 将渲染 app/views/books/index.html.erb 文件。
如果我们想在视图中显示所有图书的属性,我们可以使用这样的 ERB 模板来实现:
<h1>Listing Books</h1>
<table>
<thead>
<tr>
<th>Title</th>
<th>Content</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @books.each do |book| %>
<tr>
<td><%= book.title %></td>
<td><%= book.content %></td>
<td><%= link_to "Show", book %></td>
<td><%= link_to "Edit", edit_book_path(book) %></td>
<td><%= link_to "Destroy", book, data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to "New book", new_book_path %>
实际的渲染是由模块 ActionView::Template::Handlers 的嵌套类完成的。本指南不深入探讨这个过程,但重要的是要知道视图的文件扩展名控制着模板处理器的选择。
2.2. 使用 render
在大多数情况下,控制器的 render 方法承担了渲染应用程序内容以供浏览器使用的繁重工作。有多种方法可以自定义 render 的行为。你可以渲染 Rails 模板的默认视图,或特定模板,或文件,或内联代码,或根本不渲染任何内容。你可以渲染文本、JSON 或 XML。你还可以指定渲染响应的内容类型或 HTTP 状态。
如果你想看到 render 调用产生的确切结果,而不需要在浏览器中检查它,你可以调用 render_to_string。此方法接受与 render 完全相同的选项,但它返回一个字符串而不是向浏览器发送响应。
2.2.1. 渲染动作的视图
如果你想渲染与同一控制器中不同模板对应的视图,你可以使用 render 并附带视图的名称:
def update
@book = Book.find(params[:id])
if @book.update(book_params)
redirect_to(@book)
else
render "edit"
end
end
如果 update 调用失败,此控制器中的 update 动作将渲染属于同一控制器的 edit.html.erb 模板。
如果你愿意,可以使用符号而不是字符串来指定要渲染的动作:
def update
@book = Book.find(params[:id])
if @book.update(book_params)
redirect_to(@book)
else
render :edit, status: :unprocessable_entity
end
end
2.2.2. 渲染来自其他控制器的动作模板
如果你想渲染一个完全不同于包含动作代码的控制器的模板怎么办?你也可以使用 render 来实现,它接受要渲染的模板的完整路径(相对于 app/views)。例如,如果你在位于 app/controllers/admin 的 AdminProductsController 中运行代码,你可以这样渲染动作结果到 app/views/products 中的模板:
render "products/show"
Rails 知道这个视图属于不同的控制器,因为字符串中嵌入了斜杠字符。如果你想明确指定,可以使用 :template 选项(Rails 2.2 及更早版本需要此选项):
render template: "products/show"
2.2.3. 总结
上述两种渲染方式(渲染同一控制器中另一个动作的模板,以及渲染不同控制器中另一个动作的模板)实际上是相同操作的变体。
事实上,在 BooksController 类中,在 update 动作中,如果图书更新不成功,我们想渲染 edit 模板,以下所有渲染调用都会渲染 views/books 目录中的 edit.html.erb 模板:
render :edit
render action: :edit
render "edit"
render action: "edit"
render "books/edit"
render template: "books/edit"
你使用哪种方式实际上是风格和约定问题,但经验法则是使用对你正在编写的代码来说最简单且有意义的方式。
2.2.4. 将 render 与 :inline 结合使用
如果你愿意使用 :inline 选项在方法调用中提供 ERB,那么 render 方法可以完全不需要视图。这是完全有效的:
render inline: "<% products.each do |p| %><p><%= p.name %></p><% end %>"
很少有充分的理由使用此选项。将 ERB 混入控制器会破坏 Rails 的 MVC 导向,并会使其他开发者更难理解项目的逻辑。请改用单独的 ERB 视图。
默认情况下,内联渲染使用 ERB。你可以使用 :type 选项强制它使用 Builder:
render inline: "xml.p {'Horrid coding practice!'}", type: :builder
2.2.5. 渲染文本
你可以使用 render 的 :plain 选项将纯文本(没有任何标记)发送回浏览器:
render plain: "OK"
渲染纯文本在响应 Ajax 或 Web 服务请求时最有用,这些请求期望的不是正确的 HTML。
默认情况下,如果你使用 :plain 选项,文本将不使用当前布局进行渲染。如果你希望 Rails 将文本放入当前布局中,你需要添加 layout: true 选项并对布局文件使用 .text.erb 扩展名。
2.2.6. 渲染 HTML
你可以通过使用 render 的 :html 选项将 HTML 字符串发送回浏览器:
render html: helpers.tag.strong("Not Found")
这在渲染一小段 HTML 代码时很有用。但是,如果标记很复杂,你可能需要考虑将其移至模板文件。
当使用 html: 选项时,如果字符串不是由 html_safe 感知 API 组成的,HTML 实体将被转义。
2.2.7. 渲染 JSON
JSON 是一种被许多 Ajax 库使用的 JavaScript 数据格式。Rails 内置了将对象转换为 JSON 并将该 JSON 渲染回浏览器的支持:
render json: @product
你不需要对要渲染的对象调用 to_json。如果你使用 :json 选项,render 将自动为你调用 to_json。
2.2.8. 渲染 XML
Rails 还内置了将对象转换为 XML 并将该 XML 渲染回调用方的支持:
render xml: @product
你不需要对要渲染的对象调用 to_xml。如果你使用 :xml 选项,render 将自动为你调用 to_xml。
2.2.9. 渲染纯 JavaScript
Rails 可以渲染纯 JavaScript:
render js: "alert('Hello Rails');"
这会将提供的字符串发送到浏览器,MIME 类型为 text/javascript。
2.2.10. 渲染原始正文
你可以使用 render 的 :body 选项将原始内容发送回浏览器,而不设置任何内容类型:
render body: "raw"
仅当你不在乎响应的内容类型时才应使用此选项。大多数情况下,使用 :plain 或 :html 可能更合适。
除非被覆盖,否则此渲染选项返回的响应将是 text/plain,因为那是 Action Dispatch 响应的默认内容类型。
2.2.11. 渲染原始文件
Rails 可以从绝对路径渲染原始文件。这对于有条件地渲染静态文件(如错误页面)非常有用。
render file: "#{Rails.root}/public/404.html", layout: false
这会渲染原始文件(它不支持 ERB 或其他处理程序)。默认情况下,它会在当前布局中渲染。
将 :file 选项与用户输入结合使用可能会导致安全问题,因为攻击者可能会使用此操作访问文件系统中的安全敏感文件。
如果不需要布局,send_file 通常是一个更快更好的选择。
2.2.12. 渲染对象
Rails 可以渲染响应 #render_in 的对象。可以通过在对象上定义 #format 来控制格式。
class Greeting
def render_in(view_context)
view_context.render html: "Hello, World"
end
def format
:html
end
end
render Greeting.new
# => "Hello World"
这会在提供的对象上使用当前视图上下文调用 render_in。你也可以通过使用 render 的 :renderable 选项来提供对象:
render renderable: Greeting.new
# => "Hello World"
2.2.13. render 的选项
对 render 方法的调用通常接受六个选项:
:content_type:layout:location:status:formats:variants
2.2.13.1. :content_type 选项
默认情况下,Rails 将以 text/html 的 MIME 内容类型(如果你使用 :json 选项,则为 application/json,如果使用 :xml 选项,则为 application/xml)提供渲染操作的结果。有时你可能希望更改此设置,你可以通过设置 :content_type 选项来实现:
render template: "feed", content_type: "application/rss"
2.2.13.2. :layout 选项
对于 render 的大多数选项,渲染的内容都会作为当前布局的一部分显示。你将在本指南后面了解更多关于布局以及如何使用它们的信息。
你可以使用 :layout 选项告诉 Rails 使用特定文件作为当前动作的布局:
render layout: "special_layout"
你也可以告诉 Rails 完全不使用布局进行渲染:
render layout: false
2.2.13.3. :location 选项
你可以使用 :location 选项设置 HTTP Location 头:
render xml: photo, location: photo_url(photo)
2.2.13.4. :status 选项
Rails 会自动生成带有正确 HTTP 状态码的响应(在大多数情况下,这是 200 OK)。你可以使用 :status 选项来更改此设置:
render status: 500
render status: :forbidden
Rails 了解数字状态码和下面所示的相应符号。
| 响应类别 | HTTP 状态码 | 符号 |
|---|---|---|
| 信息响应 | 100 | :continue |
| 101 | :switching_protocols | |
| 102 | :processing | |
| 成功响应 | 200 | :ok |
| 201 | :created | |
| 202 | :accepted | |
| 203 | :non_authoritative_information | |
| 204 | :no_content | |
| 205 | :reset_content | |
| 206 | :partial_content | |
| 207 | :multi_status | |
| 208 | :already_reported | |
| 226 | :im_used | |
| 重定向 | 300 | :multiple_choices |
| 301 | :moved_permanently | |
| 302 | :found | |
| 303 | :see_other | |
| 304 | :not_modified | |
| 305 | :use_proxy | |
| 307 | :temporary_redirect | |
| 308 | :permanent_redirect | |
| 客户端错误 | 400 | :bad_request |
| 401 | :unauthorized | |
| 402 | :payment_required | |
| 403 | :forbidden | |
| 404 | :not_found | |
| 405 | :method_not_allowed | |
| 406 | :not_acceptable | |
| 407 | :proxy_authentication_required | |
| 408 | :request_timeout | |
| 409 | :conflict | |
| 410 | :gone | |
| 411 | :length_required | |
| 412 | :precondition_failed | |
| 413 | :payload_too_large | |
| 414 | :uri_too_long | |
| 415 | :unsupported_media_type | |
| 416 | :range_not_satisfiable | |
| 417 | :expectation_failed | |
| 421 | :misdirected_request | |
| 422 | :unprocessable_entity | |
| 423 | :locked | |
| 424 | :failed_dependency | |
| 426 | :upgrade_required | |
| 428 | :precondition_required | |
| 429 | :too_many_requests | |
| 431 | :request_header_fields_too_large | |
| 451 | :unavailable_for_legal_reasons | |
| 服务器错误 | 500 | :internal_server_error |
| 501 | :not_implemented | |
| 502 | :bad_gateway | |
| 503 | :service_unavailable | |
| 504 | :gateway_timeout | |
| 505 | :http_version_not_supported | |
| 506 | :variant_also_negotiates | |
| 507 | :insufficient_storage | |
| 508 | :loop_detected | |
| 510 | :not_extended | |
| 511 | :network_authentication_required |
如果你尝试使用非内容状态码(100-199、204、205 或 304)渲染内容,内容将从响应中删除。
2.2.13.5. :formats 选项
Rails 使用请求中指定的格式(或默认使用 :html)。你可以通过传递一个符号或数组的 :formats 选项来更改此设置:
render formats: :xml
render formats: [:json, :xml]
如果指定格式的模板不存在,则会引发 ActionView::MissingTemplate 错误。
2.2.13.6. :variants 选项
这会告诉 Rails 查找相同格式的模板变体。你可以通过传递一个符号或数组的 :variants 选项来指定变体列表。
一个使用示例是这样。
# called in HomeController#index
render variants: [:mobile, :desktop]
通过这组变体,Rails 将查找以下模板集并使用第一个存在的模板。
app/views/home/index.html+mobile.erbapp/views/home/index.html+desktop.erbapp/views/home/index.html.erb
如果指定格式的模板不存在,则会引发 ActionView::MissingTemplate 错误。
除了在 render 调用中设置变体外,你还可以在控制器动作中设置 request.variant。在 Action Controller 概述指南中了解更多关于变体的信息。
def index
request.variant = determine_variant
end
private
def determine_variant
variant = nil
# some code to determine the variant(s) to use
variant = :mobile if session[:use_mobile]
variant
end
添加许多与现有模板文件相似的新变体模板可能会使视图代码的维护变得更加困难。
2.2.14. 查找布局
为了找到当前布局,Rails 首先在 app/views/layouts 中查找与控制器同名的文件。例如,渲染 PhotosController 类中的动作将使用 app/views/layouts/photos.html.erb(或 app/views/layouts/photos.builder)。如果没有这样的控制器特定布局,Rails 将使用 app/views/layouts/application.html.erb 或 app/views/layouts/application.builder。如果没有 .erb 布局,如果存在 .builder 布局,Rails 将使用它。Rails 还提供了几种更精确地将特定布局分配给单个控制器和动作的方法。
2.2.14.1. 为控制器指定布局
你可以在控制器中使用 layout 声明来覆盖默认布局约定。例如:
class ProductsController < ApplicationController
layout "inventory"
#...
end
通过此声明,由 ProductsController 渲染的所有视图都将使用 app/views/layouts/inventory.html.erb 作为其布局。
要为整个应用程序指定特定布局,请在 ApplicationController 类中使用 layout 声明:
class ApplicationController < ActionController::Base
layout "main"
#...
end
有了这个声明,整个应用程序中的所有视图都将使用 app/views/layouts/main.html.erb 作为其布局。
2.2.14.2. 运行时选择布局
你可以使用符号来推迟布局的选择,直到请求被处理:
class ProductsController < ApplicationController
layout :products_layout
def show
@product = Product.find(params[:id])
end
private
def products_layout
@current_user.special? ? "special" : "products"
end
end
现在,如果当前用户是特殊用户,他们在查看产品时将获得一个特殊布局。
你甚至可以使用内联方法(例如 Proc)来确定布局。例如,如果你传递一个 Proc 对象,你给 Proc 的块将被赋予 controller 实例,因此可以根据当前请求确定布局:
class ProductsController < ApplicationController
layout Proc.new { |controller| controller.request.xhr? ? "popup" : "application" }
end
2.2.14.3. 条件布局
控制器级别指定的布局支持 :only 和 :except 选项。这些选项接受方法名或方法名数组,对应于控制器中的方法名:
class ProductsController < ApplicationController
layout "product", except: [:index, :rss]
end
通过此声明,除了 rss 和 index 方法之外,所有其他方法都将使用 product 布局。
2.2.14.4. 布局继承
布局声明在层次结构中向下级联,更具体的布局声明总是覆盖更一般的声明。例如:
application_controller.rbclass ApplicationController < ActionController::Base layout "main" endarticles_controller.rbclass ArticlesController < ApplicationController endspecial_articles_controller.rbclass SpecialArticlesController < ArticlesController layout "special" endold_articles_controller.rbclass OldArticlesController < SpecialArticlesController layout false def show @article = Article.find(params[:id]) end def index @old_articles = Article.older render layout: "old" end # ... end
在此应用程序中:
- 通常,视图将以
main布局渲染。 ArticlesController#index将使用main布局。SpecialArticlesController#index将使用special布局。OldArticlesController#show将完全不使用布局。OldArticlesController#index将使用old布局。
2.2.14.5. 模板继承
类似于布局继承逻辑,如果模板或局部视图在常规路径中找不到,控制器将在其继承链中查找要渲染的模板或局部视图。例如:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
end
# app/controllers/admin_controller.rb
class AdminController < ApplicationController
end
# app/controllers/admin/products_controller.rb
class Admin::ProductsController < AdminController
def index
end
end
admin/products#index 动作的查找顺序将是:
app/views/admin/products/app/views/admin/app/views/application/
这使得 app/views/application/ 成为共享局部视图的绝佳位置,然后可以在 ERB 中渲染它们,如下所示:
<%# app/views/admin/products/index.html.erb %>
<%= render @products || "empty_list" %>
<%# app/views/application/_empty_list.html.erb %>
There are no items in this list <em>yet</em>.
2.2.15. 避免双重渲染错误
迟早,大多数 Rails 开发者会看到错误消息“每个动作只能渲染或重定向一次”。虽然这很烦人,但相对容易修复。通常,这是因为对 render 的工作方式存在根本性的误解。
例如,这里有一些代码会触发此错误:
def show
@book = Book.find(params[:id])
if @book.special?
render action: "special_show"
end
render action: "regular_show"
end
如果 @book.special? 评估为 true,Rails 将开始渲染过程,将 @book 变量转储到 special_show 视图中。但这并不会阻止 show 动作中的其余代码运行,当 Rails 达到动作结束时,它将开始渲染 regular_show 视图——并抛出错误。解决方案很简单:确保在单个代码路径中只有一个 render 或 redirect 调用。一个有帮助的方法是 return。这是一个经过修补的方法版本:
def show
@book = Book.find(params[:id])
if @book.special?
render action: "special_show"
return
end
render action: "regular_show"
end
请注意,ActionController 执行的隐式渲染会检测是否已调用 render,因此以下代码将无错误地工作:
def show
@book = Book.find(params[:id])
if @book.special?
render action: "special_show"
end
end
这会使用 special_show 模板渲染 special? 设置为 true 的图书,而其他图书将使用默认的 show 模板进行渲染。
2.3. 使用 redirect_to
处理 HTTP 请求响应的另一种方法是使用 redirect_to。如你所见,render 告诉 Rails 在构建响应时使用哪个视图(或其他资产)。redirect_to 方法做的事情完全不同:它告诉浏览器为不同的 URL 发送新请求。例如,你可以通过此调用将代码中的任何位置重定向到应用程序中照片的索引:
redirect_to photos_url
你可以使用 redirect_back 将用户返回到他们刚刚来自的页面。此位置是从 HTTP_REFERER 头中提取的,该头不保证由浏览器设置,因此你必须提供在这种情况下使用的 fallback_location。
redirect_back(fallback_location: root_path)
redirect_to 和 redirect_back 不会立即停止并从方法执行中返回,而只是设置 HTTP 响应。方法中它们之后出现的语句仍将执行。如果需要,你可以通过显式的 return 或其他停止机制来停止。
2.3.1. 获取不同的重定向状态码
当你调用 redirect_to 时,Rails 使用 HTTP 状态码 302,这是一个临时重定向。如果你想使用不同的状态码,也许是 301,一个永久重定向,你可以使用 :status 选项:
redirect_to photos_path, status: 301
就像 render 的 :status 选项一样,redirect_to 的 :status 接受数字和符号标题指定。
2.3.2. render 和 redirect_to 的区别
有时,经验不足的开发者会将 redirect_to 视为一种 goto 命令,将执行从 Rails 代码中的一个位置移动到另一个位置。这是不正确的。
当前动作将完成,向浏览器返回一个响应。之后,你的代码停止运行并等待新的请求,只是你通过发送 HTTP 302 状态码告诉浏览器它接下来应该发出什么请求。
考虑以下动作以查看区别:
def index
@books = Book.all
end
def show
@book = Book.find_by(id: params[:id])
if @book.nil?
render action: "index"
end
end
以这种形式的代码,如果 @book 变量为 nil,很可能会出现问题。请记住,render :action 不会运行目标动作中的任何代码,因此不会设置 index 视图可能需要的 @books 变量。解决此问题的一种方法是重定向而不是渲染:
def index
@books = Book.all
end
def show
@book = Book.find_by(id: params[:id])
if @book.nil?
redirect_to action: :index
end
end
使用此代码,浏览器将对索引页面发出新的请求,index 方法中的代码将运行,一切都会正常。
这段代码唯一的缺点是它需要一次浏览器往返:浏览器通过 /books/1 请求了 show 动作,控制器发现没有图书,因此控制器向浏览器发送一个 302 重定向响应,告诉它跳转到 /books/,浏览器照做并向控制器发送一个新请求,现在请求 index 动作,控制器然后获取数据库中的所有图书并渲染 index 模板,将其发送回浏览器,浏览器然后在屏幕上显示它。
虽然在小型应用程序中,这种额外的延迟可能不是问题,但如果响应时间是一个考虑因素,则值得思考。我们可以通过一个构造的示例来演示如何处理此问题:
def index
@books = Book.all
end
def show
@book = Book.find_by(id: params[:id])
if @book.nil?
@books = Book.all
flash.now[:alert] = "Your book was not found"
render "index"
end
end
这将检测没有指定 ID 的图书,用模型中的所有图书填充 @books 实例变量,然后直接渲染 index.html.erb 模板,将其返回给浏览器,并附带一个 flash 警报消息,告知用户发生了什么。
2.4. 使用 head 构建仅包含头部的响应
head 方法可用于仅将头部信息发送到浏览器。head 方法接受一个数字或符号(参见参考表)表示 HTTP 状态码。选项参数被解释为头部名称和值的哈希。例如,你只能返回一个错误头部:
head :bad_request
这将生成以下头部:
HTTP/1.1 400 Bad Request
Connection: close
Date: Sun, 24 Jan 2010 12:15:53 GMT
Transfer-Encoding: chunked
Content-Type: text/html; charset=utf-8
X-Runtime: 0.013483
Set-Cookie: _blog_session=...snip...; path=/; HttpOnly
Cache-Control: no-cache
或者你可以使用其他 HTTP 头来传达其他信息:
head :created, location: photo_path(@photo)
这将产生:
HTTP/1.1 201 Created
Connection: close
Date: Sun, 24 Jan 2010 12:16:44 GMT
Transfer-Encoding: chunked
Location: /photos/1
Content-Type: text/html; charset=utf-8
X-Runtime: 0.083496
Set-Cookie: _blog_session=...snip...; path=/; HttpOnly
Cache-Control: no-cache
3. 布局结构
当 Rails 渲染视图作为响应时,它会根据本指南前面介绍的查找当前布局的规则,将视图与当前布局结合起来。在布局中,你可以使用以下三种工具来组合不同的输出片段以形成整体响应:
- 资产标签
yield和content_for- 局部视图
3.1. 资产标签助手
资产标签助手提供了用于生成 HTML 的方法,这些 HTML 将视图链接到 feed、JavaScript、样式表、图像、视频和音频。Rails 中有六个资产标签助手可用:
你可以在布局或其他视图中使用这些标签,尽管 auto_discovery_link_tag、javascript_include_tag 和 stylesheet_link_tag 最常用于布局的 <head> 部分。
资产标签助手不会验证指定位置的资产是否存在;它们只是假设你了解你在做什么并生成链接。
3.1.1. 使用 auto_discovery_link_tag 链接到 Feed
auto_discovery_link_tag 助手构建 HTML,大多数浏览器和 Feed 阅读器可以使用它来检测 RSS、Atom 或 JSON Feed 的存在。它接受链接类型(:rss、:atom 或 :json),一个传递给 url_for 的选项哈希,以及一个用于标签的选项哈希:
<%= auto_discovery_link_tag(:rss, {action: "feed"},
{title: "RSS Feed"}) %>
auto_discovery_link_tag 有三个可用的标签选项:
:rel指定链接中的rel值。默认值为“alternate”。:type指定显式 MIME 类型。Rails 会自动生成适当的 MIME 类型。:title指定链接的标题。默认值为大写的:type值,例如“ATOM”或“RSS”。
3.1.2. 使用 javascript_include_tag 链接到 JavaScript 文件
javascript_include_tag 助手为每个提供的源返回一个 HTML script 标签。
如果你在 Rails 中启用了 资产管道,此助手将生成一个指向 /assets/javascripts/ 而不是早期 Rails 版本中使用的 public/javascripts 的链接。此链接随后由资产管道提供。
Rails 应用程序或 Rails 引擎中的 JavaScript 文件位于三个位置之一:app/assets、lib/assets 或 vendor/assets。这些位置在资产管道指南的资产组织部分中进行了详细说明。
如果你愿意,你可以指定相对于文档根目录的完整路径,或者一个 URL。例如,要链接到位于 app/assets/javascripts、lib/assets/javascripts 或 vendor/assets/javascripts 之一内部的 JavaScript 文件 main.js,你可以这样做:
<%= javascript_include_tag "main" %>
Rails 然后会输出一个这样的 script 标签:
<script src='/assets/main.js'></script>
此资产的请求随后由 Sprockets gem 提供服务。
同时包含多个文件,例如 app/assets/javascripts/main.js 和 app/assets/javascripts/columns.js:
<%= javascript_include_tag "main", "columns" %>
包含 app/assets/javascripts/main.js 和 app/assets/javascripts/photos/columns.js:
<%= javascript_include_tag "main", "/photos/columns" %>
包含 http://example.com/main.js:
<%= javascript_include_tag "http://example.com/main.js" %>
3.1.3. 使用 stylesheet_link_tag 链接到 CSS 文件
stylesheet_link_tag 助手为每个提供的源返回一个 HTML <link> 标签。
如果你在 Rails 中启用了“资产管道”,此助手将生成一个指向 /assets/stylesheets/ 的链接。此链接随后由 Sprockets gem 处理。样式表文件可以存储在三个位置之一:app/assets、lib/assets 或 vendor/assets。
你可以指定相对于文档根目录的完整路径,或者一个 URL。例如,要链接到位于 app/assets、lib/assets 或 vendor/assets 之一内部名为 stylesheets 的目录中的样式表文件,你可以这样做:
<%= stylesheet_link_tag "main" %>
包含 app/assets/stylesheets/main.css 和 app/assets/stylesheets/columns.css:
<%= stylesheet_link_tag "main", "columns" %>
包含 app/assets/stylesheets/main.css 和 app/assets/stylesheets/photos/columns.css:
<%= stylesheet_link_tag "main", "photos/columns" %>
包含 http://example.com/main.css:
<%= stylesheet_link_tag "http://example.com/main.css" %>
默认情况下,stylesheet_link_tag 创建带有 rel="stylesheet" 的链接。你可以通过指定适当的选项(:rel)来覆盖此默认设置:
<%= stylesheet_link_tag "main_print", media: "print" %>
3.1.4. 使用 image_tag 链接到图像
image_tag 助手构建一个指向指定文件的 HTML <img /> 标签。默认情况下,文件从 public/images 加载。
请注意,你必须指定图像的扩展名。
<%= image_tag "header.png" %>
你可以提供图像的路径,如果你喜欢:
<%= image_tag "icons/delete.gif" %>
你可以提供一个包含额外 HTML 选项的哈希:
<%= image_tag "icons/delete.gif", {height: 45} %>
你可以为图像提供替代文本,如果用户在浏览器中关闭了图像,将使用该文本。如果你没有明确指定 alt 文本,它默认为文件的文件名,大写且没有扩展名。例如,这两个图像标签将返回相同的代码:
<%= image_tag "home.gif" %>
<%= image_tag "home.gif", alt: "Home" %>
你还可以指定一个特殊的尺寸标签,格式为“{width}x{height}”:
<%= image_tag "home.gif", size: "50x20" %>
除了上述特殊标签外,你还可以提供一个包含标准 HTML 选项的最终哈希,例如 :class、:id 或 :name:
<%= image_tag "home.gif", alt: "Go Home",
id: "HomeImage",
class: "nav_bar" %>
3.1.5. 使用 video_tag 链接到视频
video_tag 助手构建一个指向指定文件的 HTML5 <video> 标签。默认情况下,文件从 public/videos 加载。
<%= video_tag "movie.ogg" %>
生成:
<video src="/videos/movie.ogg" />
像 image_tag 一样,你可以提供路径,可以是绝对路径,也可以是相对于 public/videos 目录的路径。此外,你可以像 image_tag 一样指定 size: "#{width}x#{height}" 选项。视频标签也可以在末尾指定任何 HTML 选项(id、class 等)。
视频标签还支持通过 HTML 选项哈希实现所有 <video> HTML 选项,包括:
poster: "image_name.png",提供一个图像,在视频开始播放之前放置在视频位置。autoplay: true,在页面加载时开始播放视频。loop: true,视频播放到末尾后循环。controls: true,为用户提供浏览器提供的控件以与视频交互。autobuffer: true,视频将在页面加载时为用户预加载文件。
你还可以通过向 video_tag 传递一个视频数组来指定要播放的多个视频:
<%= video_tag ["trailer.ogg", "movie.ogg"] %>
这将生成:
<video>
<source src="/videos/trailer.ogg">
<source src="/videos/movie.ogg">
</video>
3.1.6. 使用 audio_tag 链接到音频文件
audio_tag 助手构建一个指向指定文件的 HTML5 <audio> 标签。默认情况下,文件从 public/audios 加载。
<%= audio_tag "music.mp3" %>
如果你愿意,可以提供音频文件的路径:
<%= audio_tag "music/first_song.mp3" %>
你还可以提供一个包含额外选项的哈希,例如 :id、:class 等。
像 video_tag 一样,audio_tag 也有特殊选项:
autoplay: true,在页面加载时开始播放音频。controls: true,为用户提供浏览器提供的控件以与音频交互。autobuffer: true,音频将在页面加载时为用户预加载文件。
3.2. 理解 yield
在布局上下文中,yield 标识了一个应插入视图内容的区域。最简单的使用方式是只有一个 yield,其中插入了当前渲染视图的全部内容:
<html>
<head>
</head>
<body>
<%= yield %>
</body>
</html>
你还可以创建一个具有多个 yield 区域的布局:
<html>
<head>
<%= yield :head %>
</head>
<body>
<%= yield %>
</body>
</html>
视图的主体内容将始终渲染到未命名的 yield 中。要将内容渲染到命名的 yield 中,请调用 content_for 方法,并使用与命名 yield 相同的参数。
新生成的应用程序将在其 app/views/layouts/application.html.erb 模板的 <head> 元素中包含 <%= yield :head %>。
3.3. 使用 content_for 方法
content_for 方法允许你将内容插入到布局中命名的 yield 块中。例如,这个视图将与你刚才看到的布局一起工作:
<% content_for :head do %>
<title>A simple page</title>
<% end %>
<p>Hello, Rails!</p>
将此页面渲染到提供的布局中的结果将是这样的 HTML:
<html>
<head>
<title>A simple page</title>
</head>
<body>
<p>Hello, Rails!</p>
</body>
</html>
content_for 方法在你的布局包含侧边栏和页脚等不同区域时非常有用,这些区域应该插入自己的内容块。它也适用于在通用布局的 <head> 中插入页面特定的 JavaScript <script> 元素、CSS <link> 元素、上下文特定的 <meta> 元素或任何其他元素。
3.4. 使用局部视图
局部模板——通常简称为“局部视图”——是另一种将渲染过程分解为更易于管理的块的设备。使用局部视图,你可以将渲染响应特定部分的代码移动到自己的文件中。
3.4.1. 命名局部视图
要将局部视图作为视图的一部分进行渲染,你可以在视图中使用 render 方法:
<%= render "menu" %>
这将在渲染的视图中的该点渲染一个名为 _menu.html.erb 的文件。请注意开头的下划线字符:局部视图以开头的下划线命名,以区别于常规视图,尽管在引用它们时没有下划线。即使你从另一个文件夹中引入局部视图,这也适用:
<%= render "application/menu" %>
由于视图局部视图依赖于与模板和布局相同的模板继承,该代码将从 app/views/application/_menu.html.erb 中引入局部视图。
3.4.2. 使用局部视图简化视图
使用局部视图的一种方法是将其视为子程序的等价物:一种将细节从视图中移出,以便你更容易理解正在发生的事情。例如,你可能有一个视图看起来像这样:
<%= render "application/ad_banner" %>
<h1>Products</h1>
<p>Here are a few of our fine products:</p>
<%# ... %>
<%= render "application/footer" %>
在这里,_ad_banner.html.erb 和 _footer.html.erb 局部视图可以包含应用程序中许多页面共享的内容。当你专注于特定页面时,你不需要查看这些部分的细节。
如本指南前几节所示,yield 是清理布局的强大工具。请记住,它是纯 Ruby,因此你几乎可以在任何地方使用它。例如,我们可以用它来 DRY(Don't Repeat Yourself)化几个相似资源的表单布局定义:
users/index.html.erb<%= render "application/search_filters", search: @q do |form| %> <p> Name contains: <%= form.text_field :name_contains %> </p> <% end %>roles/index.html.erb<%= render "application/search_filters", search: @q do |form| %> <p> Title contains: <%= form.text_field :title_contains %> </p> <% end %>application/_search_filters.html.erb<%= form_with model: search do |form| %> <h1>Search form:</h1> <fieldset> <%= yield form %> </fieldset> <p> <%= form.submit "Search" %> </p> <% end %>
对于应用程序中所有页面共享的内容,你可以直接从布局中使用局部视图。
3.4.3. 局部视图布局
局部视图可以使用自己的布局文件,就像视图可以使用布局一样。例如,你可以像这样调用局部视图:
<%= render partial: "link_area", layout: "graybar" %>
这将查找名为 _link_area.html.erb 的局部视图,并使用布局 _graybar.html.erb 渲染它。请注意,局部视图的布局遵循与常规局部视图相同的下划线开头命名约定,并且与它们所属的局部视图放在同一个文件夹中(而不是在主 layouts 文件夹中)。
另请注意,当传递 :layout 等额外选项时,需要显式指定 :partial。
3.4.4. 传递局部变量
你还可以将局部变量传递到局部视图中,使它们更加强大和灵活。例如,你可以使用此技术减少新页面和编辑页面之间的重复,同时仍保留一些不同的内容:
new.html.erb<h1>New zone</h1> <%= render partial: "form", locals: {zone: @zone} %>edit.html.erb<h1>Editing zone</h1> <%= render partial: "form", locals: {zone: @zone} %>_form.html.erb<%= form_with model: zone do |form| %> <p> <b>Zone name</b><br> <%= form.text_field :name %> </p> <p> <%= form.submit %> </p> <% end %>
尽管同一个局部视图将被渲染到两个视图中,Action View 的 submit 助手将为新建动作返回“Create Zone”,为编辑动作返回“Update Zone”。
要仅在特定情况下向局部视图传递局部变量,请使用 local_assigns。
index.html.erb<%= render user.articles %>show.html.erb<%= render article, full: true %>_article.html.erb<h2><%= article.title %></h2> <% if local_assigns[:full] %> <%= simple_format article.body %> <% else %> <%= truncate article.body %> <% end %>
这样,就可以使用局部视图而无需声明所有局部变量。
每个局部视图还有一个与局部视图同名(不带前导下划线)的局部变量。你可以通过 :object 选项将对象传递给这个局部变量:
<%= render partial: "customer", object: @new_customer %>
在 customer 局部视图中,customer 变量将引用父视图中的 @new_customer。
如果你有一个模型实例要渲染到局部视图中,你可以使用简写语法:
<%= render @customer %>
假设 @customer 实例变量包含 Customer 模型的一个实例,这将使用 _customer.html.erb 来渲染它,并将局部变量 customer 传递到局部视图中,该变量将引用父视图中的 @customer 实例变量。
3.4.5. 渲染集合
局部视图在渲染集合时非常有用。当你通过 :collection 选项将集合传递给局部视图时,局部视图将为集合中的每个成员插入一次:
index.html.erb<h1>Products</h1> <%= render partial: "product", collection: @products %>_product.html.erb<p>Product Name: <%= product.name %></p>
当调用带有复数集合的局部视图时,局部视图的各个实例可以通过以局部视图命名的变量访问正在渲染的集合成员。在这种情况下,局部视图是 _product,在 _product 局部视图中,你可以引用 product 以获取正在渲染的实例。
这也有一个简写形式。假设 @products 是 Product 实例的集合,你只需在 index.html.erb 中写入以下内容即可产生相同的结果:
<h1>Products</h1>
<%= render @products %>
Rails 通过查看集合中的模型名称来确定要使用的局部视图的名称。事实上,你甚至可以创建异构集合并以这种方式渲染它,Rails 将为集合中的每个成员选择正确的局部视图:
index.html.erb<h1>Contacts</h1> <%= render [customer1, employee1, customer2, employee2] %>customers/_customer.html.erb<p>Customer: <%= customer.name %></p>employees/_employee.html.erb<p>Employee: <%= employee.name %></p>
在这种情况下,Rails 将根据集合中的每个成员适当地使用 customer 或 employee 局部视图。
如果集合为空,render 将返回 nil,因此提供替代内容应该相当简单。
<h1>Products</h1>
<%= render(@products) || "There are no products available." %>
3.4.6. 局部变量
要在局部视图中使用自定义局部变量名称,请在对局部视图的调用中指定 :as 选项:
<%= render partial: "product", collection: @products, as: :item %>
通过此更改,你可以将 @products 集合的实例作为局部视图中的 item 局部变量进行访问。
你还可以使用 locals: {} 选项将任意局部变量传递给正在渲染的任何局部视图:
<%= render partial: "product", collection: @products,
as: :item, locals: {title: "Products Page"} %>
在这种情况下,局部视图将能够访问值为“产品页面”的局部变量 title。
3.4.7. 计数器变量
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。
3.4.8. 间隔模板
你还可以通过使用 :spacer_template 选项来指定一个第二个局部视图,该局部视图将在主局部视图的实例之间渲染:
<%= render partial: @products, spacer_template: "product_ruler" %>
Rails 将在每对 _product 局部视图之间渲染 _product_ruler 局部视图(不向其传递任何数据)。
3.4.9. 集合局部视图布局
渲染集合时也可以使用 :layout 选项:
<%= render partial: "product", collection: @products, layout: "special_layout" %>
布局将与集合中的每个项目的局部视图一起渲染。当前对象和 object_counter 变量也将在布局中可用,就像它们在局部视图中一样。
3.5. 使用嵌套布局
你可能会发现你的应用程序需要一个与常规应用程序布局略有不同的布局来支持某个特定控制器。与其重复主布局并进行编辑,你可以通过使用嵌套布局(有时称为子模板)来实现这一点。下面是一个示例:
假设你有以下 ApplicationController 布局:
app/views/layouts/application.html.erb<html> <head> <title><%= @page_title or "Page Title" %></title> <%= stylesheet_link_tag "layout" %> <%= yield :head %> </head> <body> <div id="top_menu">Top menu items here</div> <div id="menu">Menu items here</div> <div id="content"><%= content_for?(:content) ? yield(:content) : yield %></div> </body> </html>
在 NewsController 生成的页面上,你想要隐藏顶部菜单并添加一个右侧菜单:
app/views/layouts/news.html.erb<% content_for :head do %> <style> #top_menu {display: none} #right_menu {float: right; background-color: yellow; color: black} </style> <% end %> <% content_for :content do %> <div id="right_menu">Right menu items here</div> <%= content_for?(:news_content) ? yield(:news_content) : yield %> <% end %> <%= render template: "layouts/application" %>
就是这样。新闻视图将使用新的布局,隐藏顶部菜单并在“content”div 内部添加一个新的右侧菜单。
使用这种技术,有几种方法可以使用不同的子模板方案获得类似的结果。请注意,嵌套级别没有限制。可以使用 ActionView::render 方法通过 render template: 'layouts/news' 将新布局基于新闻布局。如果你确定不会对 News 布局进行子模板化,你可以将 content_for?(:news_content) ? yield(:news_content) : yield 替换为简单的 yield。