1 概述:各个部分如何协同工作
本指南重点介绍了模型-视图-控制器三角形中控制器和视图之间的交互。如您所知,控制器负责协调处理 Rails 中请求的整个过程,尽管它通常会将任何繁重的代码交给模型。但是,当需要将响应发送回用户时,控制器会将事情交给视图。本指南的主题就是这种移交。
概括地说,这涉及确定应该发送什么作为响应,并调用适当的方法来创建该响应。如果响应是完整的视图,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
模型并向 BooksController
添加索引操作
class BooksController < ApplicationController
def index
@books = Book.all
end
end
请注意,我们没有在索引操作的末尾显式渲染,这符合“约定优于配置”原则。规则是,如果您没有在控制器操作的末尾显式渲染任何内容,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
类中,在更新操作内,如果图书未成功更新,我们想渲染编辑模板,以下所有渲染调用都将渲染 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"
当您响应期望非正常 HTML 的 Ajax 或 Web 服务请求时,渲染纯文本最有用。
默认情况下,如果您使用 :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 是一种 JavaScript 数据格式,许多 Ajax 库都使用它。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.erb
app/views/home/index.html+desktop.erb
app/views/home/index.html.erb
如果不存在具有指定格式的模板,则会引发 ActionView::MissingTemplate
错误。
除了在渲染调用中设置变体外,您也可以在控制器操作中的请求对象上设置变体。
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
布局,Rails 将使用 .builder
布局(如果存在)。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
使用此声明,product
布局将用于除 rss
和 index
方法以外的所有内容。
2.2.14.4 布局继承
布局声明在层次结构中向下级联,更具体的布局声明始终覆盖更通用的声明。例如
application_controller.rb
class ApplicationController < ActionController::Base layout "main" end
articles_controller.rb
class ArticlesController < ApplicationController end
special_articles_controller.rb
class SpecialArticlesController < ArticlesController layout "special" end
old_articles_controller.rb
class 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 获取不同的重定向状态代码
Rails 在调用 `redirect_to` 时使用 HTTP 状态码 302(临时重定向)。如果您想使用不同的状态码,比如 301(永久重定向),您可以使用 `:status` 选项。
redirect_to photos_path, status: 301
就像 `render` 的 `:status` 选项一样,`redirect_to` 的 `:status` 选项也接受数字和符号形式的 HTTP 头标识。
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 页面发出新的请求,`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` 模板,并向浏览器返回带有闪现警报消息的响应,以告知用户发生了什么。
2.4 使用 `head` 创建仅包含头部的响应
可以使用 `head` 方法向浏览器发送仅包含头部的响应。`head` 方法接受表示 HTTP 状态码的数字或符号(参见 参考表)。`options` 参数被解释为一个包含头名称和值的哈希。例如,您可以仅返回错误头部
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 将视图链接到提要、JavaScript、样式表、图像、视频和音频。Rails 中有六个可用的资产标签助手
`auto_discovery_link_tag`
`javascript_include_tag`
`stylesheet_link_tag`
`image_tag`
`video_tag`
`audio_tag`
您可以在布局或其他视图中使用这些标签,尽管 `auto_discovery_link_tag`、`javascript_include_tag` 和 `stylesheet_link_tag` 通常在布局的 `<head>` 部分使用。
资产标签助手不验证指定位置是否存在资产;它们只是假定您知道自己在做什么,并生成链接。
3.1.1 使用 `auto_discovery_link_tag` 链接到提要
`auto_discovery_link_tag` 助手构建大多数浏览器和提要阅读器可以用来检测 RSS、Atom 或 JSON 提要存在情况的 HTML。它接收链接类型(`: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} %>
您可以为图像提供替代文本,如果用户在浏览器中禁用了图像,则将使用该文本。如果您没有显式指定替代文本,则默认为文件的文件名,首字母大写且没有扩展名。例如,这两个图像标签将返回相同的代码
<%= 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>
你还可以创建一个包含多个产出区域的布局。
<html>
<head>
<%= yield :head %>
</head>
<body>
<%= yield %>
</body>
</html>
视图的主体将始终呈现到未命名的 yield
中。要将内容呈现到命名的 yield
中,请使用与命名 yield
相同的参数调用 content_for
方法。
新生成的应用程序将在 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
方法在你的布局包含不同的区域(如侧边栏和页脚,这些区域应该插入自己的内容块)时非常有用。它也适用于将特定于页面的 JavaScript <script>
元素、CSS <link>
元素、特定于上下文的 <meta>
元素或任何其他元素插入到其他通用布局的 <head>
中。
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 上用于多个类似资源的表单布局定义。
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 的提交辅助函数将针对新操作返回“创建区域”,而针对编辑操作返回“更新区域”。
要在特定情况下将局部变量传递到局部模板,请使用 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 将根据集合中每个成员的需要使用客户或员工局部模板。
在集合为空的情况下,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" %>
就这样。News 视图将使用新的布局,隐藏顶部菜单并在“内容”div 中添加一个新的右侧菜单。
使用这种技术,可以使用不同的子模板方案以多种方式获得类似的结果。请注意,嵌套级别没有限制。可以使用 ActionView::render
方法通过 render template: 'layouts/news'
将新布局基于 News 布局。如果你确定不会对 News
布局进行子模板化,则可以将 content_for?(:news_content) ? yield(:news_content) : yield
替换为简单的 yield
。