# Rails 布局和視圖渲染
本文介紹 Action Controller 和 Action View 中布局的基本功能。
讀完本文,你將學到:
* 如何使用 Rails 內建的各種渲染方法;
* 如何創建具有多個內容區域的布局;
* 如何使用局部視圖去除重復;
* 如何使用嵌套布局(子模板);
### Chapters
1. [概覽:各組件之間的協作](#%E6%A6%82%E8%A7%88%EF%BC%9A%E5%90%84%E7%BB%84%E4%BB%B6%E4%B9%8B%E9%97%B4%E7%9A%84%E5%8D%8F%E4%BD%9C)
2. [創建響應](#%E5%88%9B%E5%BB%BA%E5%93%8D%E5%BA%94)
* [渲染視圖](#%E6%B8%B2%E6%9F%93%E8%A7%86%E5%9B%BE)
* [使用 `render` 方法](#%E4%BD%BF%E7%94%A8-render-%E6%96%B9%E6%B3%95)
* [使用 `redirect_to` 方法](#%E4%BD%BF%E7%94%A8-redirect_to-%E6%96%B9%E6%B3%95)
* [使用 `head` 構建只返回報頭的響應](#%E4%BD%BF%E7%94%A8-head-%E6%9E%84%E5%BB%BA%E5%8F%AA%E8%BF%94%E5%9B%9E%E6%8A%A5%E5%A4%B4%E7%9A%84%E5%93%8D%E5%BA%94)
3. [布局結構](#%E5%B8%83%E5%B1%80%E7%BB%93%E6%9E%84)
* [靜態資源標簽幫助方法](#%E9%9D%99%E6%80%81%E8%B5%84%E6%BA%90%E6%A0%87%E7%AD%BE%E5%B8%AE%E5%8A%A9%E6%96%B9%E6%B3%95)
* [理解 `yield`](#%E7%90%86%E8%A7%A3-yield)
* [使用 `content_for` 方法](#%E4%BD%BF%E7%94%A8-content_for-%E6%96%B9%E6%B3%95)
* [使用局部視圖](#%E4%BD%BF%E7%94%A8%E5%B1%80%E9%83%A8%E8%A7%86%E5%9B%BE)
* [使用嵌套布局](#%E4%BD%BF%E7%94%A8%E5%B5%8C%E5%A5%97%E5%B8%83%E5%B1%80)
### 1 概覽:各組件之間的協作
本文關注 MVC 架構中控制器和視圖之間的交互。你可能已經知道,控制器的作用是處理請求,但經常會把繁重的操作交給模型完成。返回響應時,控制器會把一些操作交給視圖完成。本文要說明的就是控制器交給視圖的操作是怎么完成的。
總的來說,這個過程涉及到響應中要發送什么內容,以及調用哪個方法創建響應。如果響應是個完整的視圖,Rails 還要做些額外工作,把視圖套入布局,有時還要渲染局部視圖。后文會詳細介紹整個過程。
### 2 創建響應
從控制器的角度來看,創建 HTTP 響應有三種方法:
* 調用 `render` 方法,向瀏覽器發送一個完整的響應;
* 調用 `redirect_to` 方法,向瀏覽器發送一個 HTTP 重定向狀態碼;
* 調用 `head` 方法,向瀏覽器發送只含報頭的響應;
#### 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`,網頁中會看到顯示有“Books are coming soon!”。
網頁中顯示這些文字沒什么用,所以后續你可能會創建一個 `Book` 模型,然后在 `BooksController` 中添加 `index` 動作:
```
class BooksController < ApplicationController
def index
@books = Book.all
end
end
```
注意,基于“多約定,少配置”原則,在 `index` 動作末尾并沒有指定要渲染視圖,Rails 會自動在控制器的視圖文件夾中尋找 `action_name.html.erb` 模板,然后渲染。在這個例子中,Rails 渲染的是 `app/views/books/index.html.erb` 文件。
如果要在視圖中顯示書籍的屬性,可以使用 ERB 模板:
```
<h1>Listing Books</h1>
<table>
<tr>
<th>Title</th>
<th>Summary</th>
<th></th>
<th></th>
<th></th>
</tr>
<% @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 "Remove", book, method: :delete, data: { confirm: "Are you sure?" } %></td>
</tr>
<% end %>
</table>
<br>
<%= link_to "New book", new_book_path %>
```
真正處理渲染過程的是 `ActionView::TemplateHandlers` 的子類。本文不做深入說明,但要知道,文件的擴展名決定了要使用哪個模板處理程序。從 Rails 2 開始,ERB 模板(含有嵌入式 Ruby 代碼的 HTML)的標準擴展名是 `.erb`,Builder 模板(XML 生成器)的標準擴展名是 `.builder`。
#### 2.2 使用 `render` 方法
大多數情況下,`ActionController::Base#render` 方法都能滿足需求,而且還有多種定制方式,可以渲染 Rails 模板的默認視圖、指定的模板、文件、行間代碼或者什么也不渲染。渲染的內容格式可以是文本,JSON 或 XML。而且還可以設置響應的內容類型和 HTTP 狀態碼。
如果不想使用瀏覽器直接查看調用 `render` 方法得到的結果,可以使用 `render_to_string` 方法。`render_to_string` 和 `render` 的用法完全一樣,不過不會把響應發送給瀏覽器,而是直接返回字符串。
##### 2.2.1 什么都不渲染
或許 `render` 方法最簡單的用法是什么也不渲染:
```
render nothing: true
```
如果使用 cURL 查看請求,會得到一些輸出:
```
$ curl -i 127.0.0.1:3000/books
HTTP/1.1 200 OK
Connection: close
Date: Sun, 24 Jan 2010 09:25:18 GMT
Transfer-Encoding: chunked
Content-Type: */*; charset=utf-8
X-Runtime: 0.014297
Set-Cookie: _blog_session=...snip...; path=/; HttpOnly
Cache-Control: no-cache
$
```
可以看到,響應的主體是空的(`Cache-Control` 之后沒有數據),但請求本身是成功的,因為 Rails 把響應碼設為了“200 OK”。調用 `render` 方法時可以設置 `:status` 選項修改狀態碼。這種用法可在 Ajax 請求中使用,因為此時只需告知瀏覽器請求已經完成。
或許不應該使用 `render :nothing`,而要用后面介紹的 `head` 方法。`head` 方法用起來更靈活,而且只返回 HTTP 報頭。
##### 2.2.2 渲染動作的視圖
如果想渲染同個控制器中的其他模板,可以把視圖的名字傳遞給 `render` 方法:
```
def update
@book = Book.find(params[:id])
if @book.update(book_params)
redirect_to(@book)
else
render "edit"
end
end
```
如果更新失敗,會渲染同個控制器中的 `edit.html.erb` 模板。
如果不想用字符串,還可使用 Symbol 指定要渲染的動作:
```
def update
@book = Book.find(params[:id])
if @book.update(book_params)
redirect_to(@book)
else
render :edit
end
end
```
##### 2.2.3 渲染其他控制器中的動作模板
如果想渲染其他控制器中的模板該怎么做呢?還是使用 `render` 方法,指定模板的完整路徑即可。例如,如果控制器 `AdminProductsController` 在 `app/controllers/admin` 文件夾中,可使用下面的方式渲染 `app/views/products` 文件夾中的模板:
```
render "products/show"
```
因為參數中有個斜線,所以 Rails 知道這個視圖屬于另一個控制器。如果想讓代碼的意圖更明顯,可以使用 `:template` 選項(Rails 2.2 及先前版本必須這么做):
```
render template: "products/show"
```
##### 2.2.4 渲染任意文件
`render` 方法還可渲染程序之外的視圖(或許多個程序共用一套視圖):
```
render "/u/apps/warehouse_app/current/app/views/products/show"
```
因為參數以斜線開頭,所以 Rails 將其視為一個文件。如果想讓代碼的意圖更明顯,可以使用 `:file` 選項(Rails 2.2+ 必須這么做)
```
render file: "/u/apps/warehouse_app/current/app/views/products/show"
```
`:file` 選項的值是文件系統中的絕對路徑。當然,你要對使用的文件擁有相應權限。
默認情況下,渲染文件時不會使用當前程序的布局。如果想讓 Rails 把文件套入布局,要指定 `layout: true` 選項。
如果在 Windows 中運行 Rails,就必須使用 `:file` 選項指定文件的路徑,因為 Windows 中的文件名和 Unix 格式不一樣。
##### 2.2.5 小結
上述三種渲染方式的作用其實是一樣的。在 `BooksController` 控制器的 `update` 動作中,如果更新失敗后想渲染 `views/books` 文件夾中的 `edit.html.erb` 模板,下面這些用法都能達到這個目的:
```
render :edit
render action: :edit
render "edit"
render "edit.html.erb"
render action: "edit"
render action: "edit.html.erb"
render "books/edit"
render "books/edit.html.erb"
render template: "books/edit"
render template: "books/edit.html.erb"
render "/path/to/rails/app/views/books/edit"
render "/path/to/rails/app/views/books/edit.html.erb"
render file: "/path/to/rails/app/views/books/edit"
render file: "/path/to/rails/app/views/books/edit.html.erb"
```
你可以根據自己的喜好決定使用哪種方式,總的原則是,使用符合代碼意圖的最簡單方式。
##### 2.2.6 使用 `render` 方法的 `:inline` 選項
如果使用 `:inline` 選項指定了 ERB 代碼,`render` 方法就不會渲染視圖。如下所示的用法完全可行:
```
render inline: "<% products.each do |p| %><p><%= p.name %></p><% end %>"
```
但是很少這么做。在控制器中混用 ERB 代碼違反了 MVC 架構原則,也讓程序的其他開發者難以理解程序的邏輯思路。請使用單獨的 ERB 視圖。
默認情況下,行間渲染使用 ERB 模板。你可以使用 `:type` 選項指定使用其他處理程序:
```
render inline: "xml.p {'Horrid coding practice!'}", type: :builder
```
##### 2.2.7 渲染文本
調用 `render` 方法時指定 `:plain` 選項,可以把沒有標記語言的純文本發給瀏覽器:
```
render plain: "OK"
```
渲染純文本主要用于 Ajax 或無需使用 HTML 的網絡服務。
默認情況下,使用 `:plain` 選項渲染純文本,不會套用程序的布局。如果想使用布局,可以指定 `layout: true` 選項。
##### 2.2.8 渲染 HTML
調用 `render` 方法時指定 `:html` 選項,可以把 HTML 字符串發給瀏覽器:
```
render html: "<strong>Not Found</strong>".html_safe
```
這種方法可用來渲染 HTML 片段。如果標記很復雜,就要考慮使用模板文件了。
如果字符串對 HTML 不安全,會進行轉義。
##### 2.2.9 渲染 JSON
JSON 是一種 JavaScript 數據格式,很多 Ajax 庫都用這種格式。Rails 內建支持把對象轉換成 JSON,經渲染后再發送給瀏覽器。
```
render json: @product
```
在需要渲染的對象上無需調用 `to_json` 方法,如果使用了 `:json` 選項,`render` 方法會自動調用 `to_json`。
##### 2.2.10 渲染 XML
Rails 也內建支持把對象轉換成 XML,經渲染后再發回給調用者:
```
render xml: @product
```
在需要渲染的對象上無需調用 `to_xml` 方法,如果使用了 `:xml` 選項,`render` 方法會自動調用 `to_xml`。
##### 2.2.11 渲染普通的 JavaScript
Rails 能渲染普通的 JavaScript:
```
render js: "alert('Hello Rails');"
```
這種方法會把 MIME 設為 `text/javascript`,再把指定的字符串發給瀏覽器。
##### 2.2.12 渲染原始的主體
調用 `render` 方法時使用 `:body` 選項,可以不設置內容類型,把原始的內容發送給瀏覽器:
```
render body: "raw"
```
只有不在意內容類型時才可使用這個選項。大多數時候,使用 `:plain` 或 `:html` 選項更合適。
如果沒有修改,這種方式返回的內容類型是 `text/html`,因為這是 Action Dispatch 響應默認使用的內容類型。
##### 2.2.13 `render` 方法的選項
`render` 方法一般可接受四個選項:
* `:content_type`
* `:layout`
* `:location`
* `:status`
###### 2.2.13.1 `:content_type` 選項
默認情況下,Rails 渲染得到的結果內容類型為 `text/html`;如果使用 `:json` 選項,內容類型為 `application/json`;如果使用 `:xml` 選項,內容類型為 `application/xml`。如果需要修改內容類型,可使用 `:content_type` 選項
```
render file: filename, 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 |
306 | :reserved |
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 | :request_entity_too_large |
414 | :request_uri_too_long |
415 | :unsupported_media_type |
416 | :requested_range_not_satisfiable |
417 | :expectation_failed |
422 | :unprocessable_entity |
423 | :locked |
424 | :failed_dependency |
426 | :upgrade_required |
428 | :precondition_required |
429 | :too_many_requests |
431 | :request_header_fields_too_large |
| **服務器錯誤** | 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 |
##### 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 運行時選擇布局
可以使用一個 Symbol,在處理請求時選擇布局:
```
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,其代碼塊可以訪問 `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.rb`
```
class ApplicationController < ActionController::Base
layout "main"
end
```
* `posts_controller.rb`
```
class PostsController < ApplicationController
end
```
* `special_posts_controller.rb`
```
class SpecialPostsController < PostsController
layout "special"
end
```
* `old_posts_controller.rb`
```
class OldPostsController < SpecialPostsController
layout false
def show
@post = Post.find(params[:id])
end
def index
@old_posts = Post.older
render layout: "old"
end
# ...
end
```
在這個程序中:
* 一般情況下,視圖使用 `main` 布局渲染;
* `PostsController#index` 使用 `main` 布局;
* `SpecialPostsController#index` 使用 `special` 布局;
* `OldPostsController#show` 不用布局;
* `OldPostsController#index` 使用 `old` 布局;
##### 2.2.15 避免雙重渲染錯誤
大多數 Rails 開發者遲早都會看到一個錯誤消息:Can only render or redirect once per action(動作只能渲染或重定向一次)。這個提示很煩人,也很容易修正。出現這個錯誤的原因是,沒有理解 `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_to` 方法。有一個語句可以提供幫助,那就是 `and return`。下面的代碼對上述代碼做了修改:
```
def show
@book = Book.find(params[:id])
if @book.special?
render action: "special_show" and return
end
render action: "regular_show"
end
```
千萬別用 `&& return` 代替 `and return`,因為 Ruby 語言操作符優先級的關系,`&& return` 根本不起作用。
注意,`ActionController` 能檢測到是否顯式調用了 `render` 方法,所以下面這段代碼不會出錯:
```
def show
@book = Book.find(params[:id])
if @book.special?
render action: "special_show"
end
end
```
如果 `@book.special?` 的結果是 `true`,會渲染 `special_show` 視圖,否則就渲染默認的 `show` 模板。
#### 2.3 使用 `redirect_to` 方法
響應 HTTP 請求的另一種方法是使用 `redirect_to`。如前所述,`render` 告訴 Rails 構建響應時使用哪個視圖(以及其他靜態資源)。`redirect_to` 做的事情則完全不同:告訴瀏覽器向另一個地址發起新請求。例如,在程序中的任何地方使用下面的代碼都可以重定向到 `photos` 控制器的 `index` 動作:
```
redirect_to photos_url
```
`redirect_to` 方法的參數與 `link_to` 和 `url_for` 一樣。有個特殊的重定向,返回到前一個頁面:
```
redirect_to :back
```
##### 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` 命令,把代碼從一處轉到別處。這么理解是**不對**的。執行到 `redirect_to` 方法時,代碼會停止運行,等待瀏覽器發起新請求。你需要告訴瀏覽器下一個請求是什么,并返回 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` 模板,并顯示一個 Flash 消息,告知用戶出了什么問題。
#### 2.4 使用 `head` 構建只返回報頭的響應
`head` 方法可以只把報頭發送給瀏覽器。還可使用意圖更明確的 `render :nothing` 達到同樣的目的。`head` 方法的參數是 HTTP 狀態碼的符號形式(參見[前文表格](#the-status-option)),選項是一個 Hash,指定報頭名和對應的值。例如,可以只返回報錯的報頭:
```
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 靜態資源標簽幫助方法
靜態資源幫助方法用來生成鏈接到 Feed、JavaScript、樣式表、圖片、視頻和音頻的 HTML 代碼。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` 鏈接到 Feed
`auto_discovery_link_tag` 幫助方法生成的 HTML,大多數瀏覽器和 Feed 閱讀器都能用來自動識別 RSS 或 Atom Feed。`auto_discovery_link_tag` 接受的參數包括鏈接的類型(`:rss` 或 `:atom`),傳遞給 `url_for` 的 Hash 選項,以及該標簽使用的 Hash 選項:
```
<%= 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` 標簽。
如果啟用了 [Asset Pipeline](asset_pipeline.html),這個幫助方法生成的鏈接指向 `/assets/javascripts/` 而不是 Rails 舊版中使用的 `public/javascripts`。鏈接的地址由 Asset Pipeline 伺服。
Rails 程序或引擎中的 JavaScript 文件可存放在三個位置:`app/assets`,`lib/assets` 或 `vendor/assets`。詳細說明參見 Asset Pipeline 中的“[靜態資源的組織方式](asset_pipeline.html#asset-organization)”一節。
文件的地址可使用相對文檔根目錄的完整路徑,或者是 URL。例如,如果想鏈接到 `app/assets`、`lib/assets` 或 `vendor/assets` 文件夾中名為 `javascripts` 的子文件夾中的文件,可以這么做:
```
<%= 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>` 標簽。
如果啟用了 Asset Pipeline,這個幫助方法生成的鏈接指向 `/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` 創建的鏈接屬性為 `media="screen" rel="stylesheet"`。指定相應的選項(`:media`,`: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" %>
```
可以使用 Hash 指定額外的 HTML 屬性:
```
<%= image_tag "icons/delete.gif", {height: 45} %>
```
可以指定一個Alt屬性,在關閉圖片的瀏覽器中顯示。如果沒指定Alt屬性,Rails 會使用圖片的文件名,去掉擴展名,并把首字母變成大寫。例如,下面兩個標簽會生成相同的代碼:
```
<%= 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` 文件夾的路徑。而且也可以指定 `size: "#{width}x#{height}"` 選項。`video_tag` 還可指定其他 HTML 屬性,例如 `id`、`class` 等。
`video_tag` 方法還可使用 HTML Hash 選項指定所有 `<video>` 標簽的屬性,包括:
* `poster: "image_name.png"`:指定視頻播放前在視頻的位置顯示的圖片;
* `autoplay: true`:頁面加載后開始播放視頻;
* `loop: true`:視頻播完后再次播放;
* `controls: true`:為用戶提供瀏覽器對視頻的控制支持,用于和視頻交互;
* `autobuffer: true`:頁面加載時預先加載視頻文件;
把數組傳遞給 `video_tag` 方法可以指定多個視頻:
```
<%= video_tag ["trailer.ogg", "movie.ogg"] %>
```
生成的代碼如下:
```
<video><source src="trailer.ogg" /><source src="movie.ogg" /></video>
```
##### 3.1.6 使用 `audio_tag` 鏈接音頻
`audio_tag` 幫助方法為指定的文件生成 HTML5 `<audio>` 標簽。默認情況下,音頻文件存放在 `public/audio` 文件夾中。
```
<%= audio_tag "music.mp3" %>
```
還可指定音頻文件的路徑:
```
<%= audio_tag "music/first_song.mp3" %>
```
還可使用 Hash 指定其他屬性,例如 `: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` 區域插入內容,得使用 `content_for` 方法。
#### 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` 方法。`content_for` 方法還可用來在通用布局中引入特定頁面使用的 JavaScript 文件或 CSS 文件。
#### 3.4 使用局部視圖
局部視圖可以把渲染過程分為多個管理方便的片段,把響應的某個特殊部分移入單獨的文件。
##### 3.4.1 具名局部視圖
在視圖中渲染局部視圖可以使用 `render` 方法:
```
<%= render "menu" %>
```
渲染這個視圖時,會渲染名為 `_menu.html.erb` 的文件。注意文件名開頭的下劃線:局部視圖的文件名開頭有個下劃線,用于和普通視圖區分開,不過引用時無需加入下劃線。即便從其他文件夾中引入局部視圖,規則也是一樣:
```
<%= render "shared/menu" %>
```
這行代碼會引入 `app/views/shared/_menu.html.erb` 這個局部視圖。
##### 3.4.2 使用局部視圖簡化視圖
局部視圖的一種用法是作為“子程序”(subroutine),把細節提取出來,以便更好地理解整個視圖的作用。例如,有如下的視圖:
```
<%= render "shared/ad_banner" %>
<h1>Products</h1>
<p>Here are a few of our fine products:</p>
...
<%= render "shared/footer" %>
```
這里,局部視圖 `_ad_banner.html.erb` 和 `_footer.html.erb` 可以包含程序多個頁面共用的內容。在編寫某個頁面的視圖時,無需關心這些局部視圖中的詳細內容。
程序所有頁面共用的內容,可以直接在布局中使用局部視圖渲染。
##### 3.4.3 局部布局
和視圖可以使用布局一樣,局部視圖也可使用自己的布局文件。例如,可以這樣調用局部視圖:
```
<%= render partial: "link_area", layout: "graybar" %>
```
這行代碼會使用 `_graybar.html.erb` 布局渲染局部視圖 `_link_area.html.erb`。注意,局部布局的名字也以下劃線開頭,和局部視圖保存在同個文件夾中(不在 `layouts` 文件夾中)。
還要注意,指定其他選項時,例如 `:layout`,必須明確地使用 `:partial` 選項。
##### 3.4.4 傳遞本地變量
本地變量可以傳入局部視圖,這么做可以把局部視圖變得更強大、更靈活。例如,可以使用這種方法去除新建和編輯頁面的重復代碼,但仍然保有不同的內容:
```
<h1>New zone</h1>
<%= render partial: "form", locals: {zone: @zone} %>
```
```
<h1>Editing zone</h1>
<%= render partial: "form", locals: {zone: @zone} %>
```
```
<%= form_for(zone) do |f| %>
<p>
<b>Zone name</b><br>
<%= f.text_field :name %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
```
雖然兩個視圖使用同一個局部視圖,但 Action View 的 `submit` 幫助方法為 `new` 動作生成的提交按鈕名為“Create Zone”,為 `edit` 動作生成的提交按鈕名為“Update Zone”。
每個局部視圖中都有個和局部視圖同名的本地變量(去掉前面的下劃線)。通過 `object` 選項可以把對象傳給這個變量:
```
<%= render partial: "customer", object: @new_customer %>
```
在 `customer` 局部視圖中,變量 `customer` 的值為父級視圖中的 `@new_customer`。
如果要在局部視圖中渲染模型實例,可以使用簡寫句法:
```
<%= render @customer %>
```
假設實例變量 `@customer` 的值為 `Customer` 模型的實例,上述代碼會渲染 `_customer.html.erb`,其中本地變量 `customer` 的值為父級視圖中 `@customer` 實例變量的值。
##### 3.4.5 渲染集合
渲染集合時使用局部視圖特別方便。通過 `:collection` 選項把集合傳給局部視圖時,會把集合中每個元素套入局部視圖渲染:
```
<h1>Products</h1>
<%= render partial: "product", collection: @products %>
```
```
<p>Product Name: <%= product.name %></p>
```
傳入復數形式的集合時,在局部視圖中可以使用和局部視圖同名的變量引用集合中的成員。在上面的代碼中,局部視圖是 `_product`,在其中可以使用 `product` 引用渲染的實例。
渲染集合還有個簡寫形式。假設 `@products` 是 `product` 實例集合,在 `index.html.erb` 中可以直接寫成下面的形式,得到的結果是一樣的:
```
<h1>Products</h1>
<%= render @products %>
```
Rails 根據集合中各元素的模型名決定使用哪個局部視圖。其實,集合中的元素可以來自不同的模型,Rails 會選擇正確的局部視圖進行渲染。
```
<h1>Contacts</h1>
<%= render [customer1, employee1, customer2, employee2] %>
```
```
<p>Customer: <%= customer.name %></p>
```
```
<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 %>
```
這樣修改之后,在局部視圖中可以使用本地變量 `item` 訪問 `@products` 集合中的實例。
使用 `locals: {}` 選項可以把任意本地變量傳入局部視圖:
```
<%= render partial: "product", collection: @products,
as: :item, locals: {title: "Products Page"} %>
```
在局部視圖中可以使用本地變量 `title`,其值為 `"Products Page"`。
在局部視圖中還可使用計數器變量,變量名是在集合后加上 `_counter`。例如,渲染 `@products` 時,在局部視圖中可以使用 `product_counter` 表示局部視圖渲染了多少次。不過不能和 `as: :value` 一起使用。
在使用主局部視圖渲染兩個實例中間還可使用 `:spacer_template` 選項指定第二個局部視圖。
##### 3.4.7 間隔模板
```
<%= render partial: @products, spacer_template: "product_ruler" %>
```
Rails 會在兩次渲染 `_product` 局部視圖之間渲染 `_product_ruler` 局部視圖(不傳入任何數據)。
##### 3.4.8 集合局部視圖的布局
渲染集合時也可使用 `:layout` 選項。
```
<%= render partial: "product", collection: @products, layout: "special_layout" %>
```
使用局部視圖渲染集合中的各元素時會套用指定的模板。和局部視圖一樣,當前渲染的對象以及 `object_counter` 變量也可在布局中使用。
#### 3.5 使用嵌套布局
在程序中有時需要使用不同于常規布局的布局渲染特定的控制器。此時無需復制主視圖進行編輯,可以使用嵌套布局(有時也叫子模板)。下面舉個例子。
假設 `ApplicationController` 布局如下:
```
<html>
<head>
<title><%= @page_title or "Page Title" %></title>
<%= stylesheet_link_tag "layout" %>
<style><%= yield :stylesheets %></style>
</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` 的頁面中,想隱藏頂部目錄,在右側添加一個目錄:
```
<% content_for :stylesheets do %>
#top_menu {display: none}
#right_menu {float: right; background-color: yellow; color: black}
<% 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` 控制器的視圖會使用 `news.html.erb` 布局,隱藏了頂部目錄,在 `<div id="content">` 中添加一個右側目錄。
使用子模板方式實現這種效果有很多方法。注意,布局的嵌套層級沒有限制。使用 `render template: 'layouts/news'` 可以指定使用一個新布局。如果確定,可以不為 `News` 控制器創建子模板,直接把 `content_for?(:news_content) ? yield(:news_content) : yield` 替換成 `yield` 即可。
### 反饋
歡迎幫忙改善指南質量。
如發現任何錯誤,歡迎修正。開始貢獻前,可先行閱讀[貢獻指南:文檔](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation)。
翻譯如有錯誤,深感抱歉,歡迎 [Fork](https://github.com/ruby-china/guides/fork) 修正,或至此處[回報](https://github.com/ruby-china/guides/issues/new)。
文章可能有未完成或過時的內容。請先檢查 [Edge Guides](http://edgeguides.rubyonrails.org) 來確定問題在 master 是否已經修掉了。再上 master 補上缺少的文件。內容參考 [Ruby on Rails 指南準則](ruby_on_rails_guides_guidelines.html)來了解行文風格。
最后,任何關于 Ruby on Rails 文檔的討論,歡迎到 [rubyonrails-docs 郵件群組](http://groups.google.com/group/rubyonrails-docs)。
- Ruby on Rails 指南 (651bba1)
- 入門
- Rails 入門
- 模型
- Active Record 基礎
- Active Record 數據庫遷移
- Active Record 數據驗證
- Active Record 回調
- Active Record 關聯
- Active Record 查詢
- 視圖
- Action View 基礎
- Rails 布局和視圖渲染
- 表單幫助方法
- 控制器
- Action Controller 簡介
- Rails 路由全解
- 深入
- Active Support 核心擴展
- Rails 國際化 API
- Action Mailer 基礎
- Active Job 基礎
- Rails 程序測試指南
- Rails 安全指南
- 調試 Rails 程序
- 設置 Rails 程序
- Rails 命令行
- Rails 緩存簡介
- Asset Pipeline
- 在 Rails 中使用 JavaScript
- 引擎入門
- Rails 應用的初始化過程
- Autoloading and Reloading Constants
- 擴展 Rails
- Rails 插件入門
- Rails on Rack
- 個性化Rails生成器與模板
- Rails應用模版
- 貢獻 Ruby on Rails
- Contributing to Ruby on Rails
- API Documentation Guidelines
- Ruby on Rails Guides Guidelines
- Ruby on Rails 維護方針
- 發布記
- A Guide for Upgrading Ruby on Rails
- Ruby on Rails 4.2 發布記
- Ruby on Rails 4.1 發布記
- Ruby on Rails 4.0 Release Notes
- Ruby on Rails 3.2 Release Notes
- Ruby on Rails 3.1 Release Notes
- Ruby on Rails 3.0 Release Notes
- Ruby on Rails 2.3 Release Notes
- Ruby on Rails 2.2 Release Notes