1 概觀:各部分如何組合在一起
本指南著重於模型-視圖-控制器三角形中控制器和視圖之間的互動。如您所知,控制器負責協調處理 Rails 中請求的整個流程,儘管它通常會將任何繁重的程式碼交給模型。但是,當要將回應發送回給使用者時,控制器會將事情交給視圖。本指南的主題就是這種交接。
概括來說,這包括決定應該將什麼作為回應發送,並呼叫適當的方法來建立該回應。如果回應是完整的視圖,Rails 還會執行一些額外的工作,將視圖包裝在版面配置中,並可能引入局部視圖。您將在本指南的稍後部分看到所有這些路徑。
2 建立回應
從控制器的角度來看,有三種建立 HTTP 回應的方法
- 呼叫
render
來建立完整的回應並傳送回瀏覽器 - 呼叫
redirect_to
來將 HTTP 重定向狀態碼傳送至瀏覽器 - 呼叫
head
來建立僅包含 HTTP 標頭的回應並傳送回瀏覽器
2.1 預設渲染:Action 中的慣例優於組態
您聽說過 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 動作的結尾明確渲染任何內容。規則是,如果您沒有在控制器動作的結尾明確渲染任何內容,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 動作內,以下所有 render 呼叫都會在 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 是一種 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');"
這會將提供的字串以 text/javascript
的 MIME 類型傳送到瀏覽器。
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
錯誤。
您也可以在控制器動作中的請求物件上設定變體,而不是在 render 呼叫上設定變體。
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
透過此宣告,除了 rss
和 index
方法之外的所有項目都會使用 product
版面配置。
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 取得不同的重新導向狀態碼
當您呼叫 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
動作,然後控制器取得資料庫中的所有書籍並渲染索引範本,將其發送回瀏覽器,然後在螢幕上顯示。
雖然在小型應用程式中,此額外的延遲可能不是問題,但如果響應時間是一個問題,則需要考慮一下。我們可以使用一個虛構的範例來說明一種處理方法
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 狀態碼的數字或符號(請參閱 參考表)。選項引數被解釋為標頭名稱和值的雜湊。例如,您可以僅返回錯誤標頭
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
最常在佈局的 <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。例如,若要連結到 main.js
JavaScript 檔案,該檔案位於 app/assets/javascripts
、lib/assets/javascripts
或 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>
標籤。
如果您使用的 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" %>
您也可以指定特殊的大小標籤,格式為 "{寬度}x{高度}"
<%= 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
,目前正在渲染的視圖的全部內容都會插入到該 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 使用 Partial
Partial 範本(通常簡稱為「partial」)是將渲染過程分解為更易於管理的小塊的另一種方法。使用 partial,您可以將渲染回應的特定部分的程式碼移到其自己的檔案中。
3.4.1 命名 Partial
若要將 partial 渲染為視圖的一部分,您可以在視圖中使用 render
方法。
<%= render "menu" %>
這將在正在渲染的視圖中的該點渲染名為 _menu.html.erb
的檔案。請注意開頭的底線字元:partial 會使用開頭的底線來命名,以區分它們與一般視圖,即使它們在參照時不使用底線。當您從另一個資料夾提取 partial 時,這也是適用的。
<%= render "application/menu" %>
由於視圖 partial 依賴與範本和版面配置相同的 範本繼承,因此該程式碼將從 app/views/application/_menu.html.erb
提取 partial。
3.4.2 使用 Partial 簡化視圖
使用 partial 的一種方法是將它們視為子程式的等效項:將細節移出視圖,以便您可以更輕鬆地掌握發生的事情。例如,您可能會看到一個看起來像這樣的視圖
<%= 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
partial 可以包含您的應用程式中許多頁面共用的內容。當您專注於特定頁面時,您不需要查看這些部分的詳細資訊。
如本指南的前幾節所示,yield
是清理版面配置的非常強大的工具。請記住,它是純 Ruby,因此您幾乎可以在任何地方使用它。例如,我們可以利用它來簡化幾個類似資源的表單版面配置定義。
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 %>
對於應用程式中所有頁面共用的內容,您可以直接從版面配置中使用 partial。
3.4.3 Partial 版面配置
與視圖可以使用版面配置一樣,partial 可以使用其自己的版面配置檔案。例如,您可以像這樣呼叫 partial
<%= render partial: "link_area", layout: "graybar" %>
這將尋找名為 _link_area.html.erb
的 partial,並使用 _graybar.html.erb
版面配置來渲染它。請注意,partial 的版面配置遵循與一般 partial 相同的開頭底線命名,並放置在它們所屬的 partial 的相同資料夾中(而不是在主 layouts
資料夾中)。
另請注意,在傳遞其他選項(例如 :layout
)時,必須明確指定 :partial
。
3.4.4 傳遞區域變數
您也可以將區域變數傳遞到 partial 中,使其更加強大和靈活。例如,您可以使用此技術來減少新頁面和編輯頁面之間的重複,同時仍然保留一些不同的內容。
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 %>
儘管相同的 partial 將渲染到兩個視圖中,但 Action View 的 submit 輔助方法將為 new 動作傳回「Create Zone」,為 edit 動作傳回「Update Zone」。
若要僅在特定情況下將區域變數傳遞給 partial,請使用 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 %>
這樣就可以使用 partial 而無需宣告所有區域變數。
每個 partial 都有一個與 partial 同名的區域變數(減去開頭的底線)。您可以使用 :object
選項將物件傳遞給此區域變數。
<%= render partial: "customer", object: @new_customer %>
在 customer
partial 中,customer
變數將參照父視圖中的 @new_customer
。
如果您有一個要渲染到 partial 中的模型實例,則可以使用簡寫語法。
<%= render @customer %>
假設 @customer
實例變數包含 Customer
模型的實例,這將使用 _customer.html.erb
來渲染它,並將區域變數 customer
傳遞到 partial 中,該變數將參照父視圖中的 @customer
實例變數。
3.4.5 渲染集合
Partial 在渲染集合時非常有用。當您透過 :collection
選項將集合傳遞給 partial 時,partial 將會為集合中的每個成員插入一次。
index.html.erb
<h1>Products</h1> <%= render partial: "product", collection: @products %>
_product.html.erb
<p>Product Name: <%= product.name %></p>
當使用複數集合呼叫 partial 時,partial 的個別實例可以透過以 partial 命名的變數來存取正在渲染的集合成員。在這種情況下,partial 是 _product
,並且在 _product
partial 中,您可以參照 product
來取得正在渲染的實例。
這也有一個簡寫形式。假設 @products
是 Product
實例的集合,您只需在 index.html.erb
中撰寫此程式碼即可產生相同的結果。
<h1>Products</h1>
<%= render @products %>
Rails 會透過查看集合中的模型名稱來決定要使用的 partial 名稱。事實上,您甚至可以建立異質集合並以這種方式渲染它,Rails 會為集合的每個成員選擇適當的 partial。
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 partial。
如果集合為空,render
將傳回 nil,因此應該很容易提供替代內容。
<h1>Products</h1>
<%= render(@products) || "There are no products available." %>
3.4.6 區域變數
若要在 partial 中使用自訂區域變數名稱,請在呼叫 partial 時指定 :as
選項。
<%= render partial: "product", collection: @products, as: :item %>
透過此變更,您可以將 @products
集合的實例作為 partial 中的 item
區域變數存取。
您也可以使用 locals: {}
選項將任意區域變數傳遞給您正在渲染的任何 partial。
<%= render partial: "product", collection: @products,
as: :item, locals: {title: "Products Page"} %>
在這種情況下,partial 將可以存取值為「Products Page」的區域變數 title
。
3.4.7 計數器變數
Rails 還會提供一個計數器變數,該變數在由集合呼叫的 partial 中可用。該變數會以 partial 的標題加上 _counter
來命名。例如,當渲染集合 @products
時,partial _product.html.erb
可以存取變數 product_counter
。該變數會為 partial 在封閉視圖中渲染的次數建立索引,從第一次渲染的值 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
選項來指定要主要 partial 的實例之間渲染的第二個 partial。
<%= render partial: @products, spacer_template: "product_ruler" %>
Rails 會在每對 _product
partial 之間渲染 _product_ruler
partial(沒有傳遞任何資料給它)。
3.4.9 集合 Partial 版面配置
在渲染集合時,也可以使用 :layout
選項。
<%= render partial: "product", collection: @products, layout: "special_layout" %>
版面配置將與集合中每個項目的 partial 一起渲染。目前的物件和 object_counter 變數也將在版面配置中可用,就像它們在 partial 中一樣。
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
。