原文地址: http://guides.ruby-china.org/layouts_and_rendering.html
Rails 布局和視圖渲染
本文介紹 Action Controller 和 Action View 中布局的基本功能。
讀完本文,你將學到:
- 如何使用 Rails 內建的各種渲染方法;
- 如果創建具有多個內容區域的布局;
- 如何使用局部視圖去除重復;
- 如何使用嵌套布局(子模板);
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 狀態碼的符號形式(參見前文表格),選項是一個 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,這個幫助方法生成的鏈接指向 /assets/javascripts/
而不是 Rails 舊版中使用的 public/javascripts
。鏈接的地址由 Asset Pipeline 伺服。
Rails 程序或引擎中的 JavaScript 文件可存放在三個位置:app/assets
,lib/assets
或 vendor/assets
。詳細說明參見 Asset Pipeline 中的“靜態資源的組織方式”一節。
文件的地址可使用相對文檔根目錄的完整路徑,或者是 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
}
%>
|
可以指定一個替代文本,在關閉圖片的瀏覽器中顯示。如果沒指定替代文本,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
即可。