[Rails] Action View, Layout, Partial, Render
Action View Overview @ RailsGuides Layouts and Rendering in Rails @ RailsGuides
[TOC]
速記
# 當 yield 後沒有接任何參數時,
# 它會自動轉譯該 controller#action 下的
# views/controller_name/action_name.html.erb 樣版(template)
<%= yield %>
概念
在 Rails 中,web requests 是透過 Action Controller 和 Action Viewer 來處理,一般來說 Action Controller 關注的是和資料庫溝通以及處理 CRUD;Action View 則負責編譯要回傳的 response。
從 Controller 中,我們主要有三種建立回應的方式,分別是 render
, redirect_to
和 head
:
render
: 建立一個完整的回應,並回傳給瀏覽器。redirect_to
:傳送 HTTP redirect status code 給瀏覽器。head
:傳送只有 HTTP headers 的內容給瀏覽器。
在 Rails 中,一份完整的 HTML 檔會包含 templates
, partials
和 layouts
三個部分。
Templates
一般來說,我們不會在 View 中去使用 puts
或 print
來輸出結果,而是直接使用 <%= %>
。
慣例:在
articles_controller.rb
中的 index action (articles#index
)會使用在app/views/articles
中的index.html.erb
這支 Template。
Partials
keywords: render
, collection
Partial Templates 一般會簡稱為 partials,透過 partials,可以把重複的 code 拉出來,在其他的 templates 中重複使用它。
慣例:partials 命名的慣例會在檔名前面加上底線
_
,以此來區別是一般的 templates 或是 partials 。
render 的使用(in Views)
使用 render
關鍵字可以載入 partials:
# 會去轉譯同資料夾下的 _menu.html.erb 這支檔案
<%= render "menu" %>
# 會去轉譯 app/views/shared/_menu.html.erb 這支檔案
<%= render "shared/menu" %>
render 可以接受兩個參數,partials
和 locals
,在預設的情況下,會以和該 partials 同名的方式來命名 partial 中的 local variable。
透過 locals
我們可以在 _product.html.erb
這個 partial 中使用 product
這個 local variable,值則是 @product
:
<%= render partial: "product", locals: { product: @product } %>
# 縮寫
<%= render "product", product: @product %>
# 再縮
<%= render partial: "product" %>
options
object
:可以在 partial 的檔案中透過product
這個 local_variable 取得@item
:
<%= render partial: 'product', object: @item %>
as
:搭配object
使用,可以在 partial 的檔案中透過item
這個 local_variable 取得@item
:
<%= render partial: 'product', object: @item , as: 'item'%>
# 等同於
<%= render partial: "product", locals: { item: @item } %>
使用 collection 疊代陣列
有時候我們需要將陣列的資料以疊代的方式呈現在 HTML 上,這時候如果疊代的資料要套用 partial 的話,可以使用 collection
關鍵字,如此將可以在 partial 中使用到該 collection (@products
)的陣列元素(product
),partial 中陣列元素 local variable 的命名是使用該 partial 名稱:
# 原本的寫法
<% @products.each do |product| %>
<%= render partial: "product", locals: { product: product } %>
<% end %>
# 使用 collection 關鍵字
<%= render partial: "product", collection: @products %>
# 縮寫
<%= render @products %>
render 的使用 (in Controller)
render :Symbol
render template: 'index' # 等同於 render 'index', 也等同於 render :index
render plain: "OK"
render html: "<strong>Not Found</strong>".html_safe
# 不用另外呼叫 to_json, to_xml,Rails 會自動處理
render json: @product
render xml: @product
# 以 MIME type of text/javascript 回傳瀏覽器
render js: "alert('Hello Rails');"
在 Rails 中不斷強調慣例優於設定,因此如果你沒有明確的在 controller 的 action 中定義要 render
的內容,Rails 會自動尋找並轉譯名為 app/views/controller_name/action_name.html.erb
的 template:
# 會自動轉譯 `app/views/books/index.html.erb`
class BooksController < ApplicationController
def index
# render 'index' # 可以省略會自動轉譯
end
end
轉譯同一個 controller 內的不同 template
render [String || Symbol]
class BooksController < ApplicationController
def index
render :index # 等同於 render 'index'
end
end
轉譯不同 controller 中的 template
如果你是在 app/controllers/admin
中的 AdminProductsController
你可以透過 render "products/show"
來轉譯 app/views/products
:
# 在 app/controllers/admin 的 controller 中轉譯 app/views/products
class AdminProductsController < ApplicationController
def index
render 'products/show'
# 等同於 render template: "products/show"
end
end
其他相同結果但不同寫法
wrapping it up @ RailsGuides
options for render
:content_type
:改變 content_type 格式,預設是text/html
。:layout
:location
:設定 HTTP Location header:status
:Rails 預設會自動產生適當的 status code,可以透過這個指定 status code。:formats
:預設是 html
# content_type:
render file: filename, content_type: "application/rss"
# :layout:
render layout: "special_layout"
render layout: false
# location:
render xml: photo, location: photo_url(photo)
# status:
render status: 500
render status: :forbidden
# formats:
render formats: :xml
render formats: [:json, :xml]
Semantic Status Code @ RailsGuides
respond_to 的使用(in Controller)
在 Rails 中當我們寫,會自動代入 render 'index'
的指令,而且預設可以回傳任何形態的檔案:
def index
@posts = Post.all
end
所以當網址是使用不同的副檔名時,都會回傳相對應的內下:
http://localhost:3000/posts # 等同於 http://localhost:3000/posts.html
http://localhost:3000/posts.js
http://localhost:3000/posts.json
但也可以透過 respond_to
指定只能回傳的檔案類型,這裡則是可以回傳 html
和 json
格式的檔案,當我們去請求其他的檔案型態時,都會回傳錯誤:
respond_to :html, :json
至於要回傳哪個資料型態回去,取決於 client 所傳送的 HTTP Accept Header 中希望的資料型態為何:
def create
@post = Post.new(post_params)
respond_to do |format|
if @post.save
format.html { redirect_to @post, notice: 'Post was successfully created.' }
format.json { render :show, status: :created, location: @post }
else
format.html { render :new }
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
end
respond_with 的使用
透過 respond_to
可以設定要回傳的資料型態,但如果在每個 action 中都要寫一次的話有點麻煩,因此可以使用 respond_with
這個方法,可以先告知 controller respond_to 支援哪幾種檔案形態,如此一來在 action 內只要寫 respond_with 就可以了:
respond_to :html, :xml, :json
def index
@topics = Topic.all
respond_with(@topics)
end
Layout
Rails 會先去 app/views/layouts
中和 controller 相同名稱的 layout,例如 PhotosController 會使用 app/views/layouts/photos.html.erb
,如果 layout 不存在,則會使用 app/views/layouts/application.html.erb
。
如果想要變更預設值,可以使用:
# Products Controller 中的 View 會以 app/views/layouts/inventory.html.erb 作為 layout
class ProductsController < ApplicationController
layout "inventory"
end
# 整個 Controller 會使用同一個 Layout,app/views/layouts/main.html.erb
class ApplicationController < ActionController::Base
layout "main"
end
Dynamic Layout 動態指定 layout
# 透過 layout Symbol 我們可以在拿到 request 時才決定套用哪個 layout
class ProductsController < ApplicationController
layout :products_layout
# layout false 整個 class 都不會套用版型
def show
@product = Product.find(params[:id])
end
private
def products_layout
@current_user.special? ? "special" : "products"
end
end
限定情況使用 Layout
Layout 支援使用 :only
和 :except
,可以接收 method name 或 an array of method namse:
class ProductsController < ApplicationController
layout "product", except: [:index, :rss]
# render layout: false 這個 action 不要套用版型
end
注意事項
要避免在重複 Render 的錯誤出現,在下面的例子中,可能會同時 render action: "special_show"
和 render action: "regular_show"
,這時候為導致錯誤:
def show
@book = Book.find(params[:id])
if @book.special?
render action: "special_show"
end
render action: "regular_show"
end
其中一個簡單的解法是透過 and return
(不能使用 && return
)
def show
@book = Book.find(params[:id])
if @book.special?
render action: "special_show" and return
end
render action: "regular_show"
end
由於 Rails 中自動化的 render(implicit render)會自己偵測 render
有沒有被呼叫過,因此下面這段程式碼不會有錯誤:
def show
@book = Book.find(params[:id])
if @book.special?
render action: "special_show"
end
end
使用 yield 和 content_for
keywords: yield
, content_for
單一個 yield
當 yield
後沒有接任何參數時,它會自動轉譯該 controller#action 下的樣版(template),因此,如果是在 cars#show
下,則會自動轉譯 views/cars/show.html.erb.
這個樣版(template)
<html>
<head>
</head>
<body>
<%= yield %>
</body>
</html>
多個 yield
<html>
<head>
<%= yield :head %>
</head>
<body>
<%= yield %>
</body>
</html>
要在 yield 中轉譯內容,必須透過 content_for
關鍵字:
<% content_for :head do %>
<title>A simple page</title>
<% end %>
<p>Hello, Rails!</p>
透過 content_for,可以用來區隔 sidebars 和 footers,或者是插入頁面特定的 JavaScript, css 檔。
redirect_to
透過 render
來告訴 Rails 在發送 response 的時候要使用哪一個 view;透過 redirect_to
則是透過瀏覽器向另一個 URL 發送 request。
redirect_to photos_url
redirect_to photos_path, status: 301
# 回到上一頁
redirect_back(fallback_location: root_path)
render 和 redirect_to 的重要差異
在下面的程式碼中,當我們使用 render
時,並不會去執行 index
action 中的內容,因此 index 頁面無法取得 @books 變數;@books 這個變數會是 nil
;但是如果我們使用的是 redirect_to
時,瀏覽器會對 index 頁面建立一個全新的 request,因此 index action 中的內容將會重新執行,所以 @books 將不會是空值。
def index
@books = Book.all
end
def show
@book = Book.find_by(id: params[:id])
if @book.nil?
# render action: "index"
redirect_to action: :index
end
end
但是這樣的寫法因為需要重新轉導到 index
頁面,會消耗比較多的時間,因此更好的寫法如下:
def index
@books = Book.all
end
def show
@book = Book.find_by(id: params[:id])
if @book.nil?
@books = Book.all # 直接把 books 的資料撈出來
flash.now[:alert] = "Your book was not found"
render "index"
end
end
Assets
使用 Rails 內建的靜態檔案(Assets)輔助方法有幾個好處:
- Rails 會合併 Stylesheet 和 JavasSript 檔案,可以加速瀏覽器的下載。
- Rails 會編譯 Sass 和 CoffeeScript 等透過 Assets template engine 產生的 Stylesheet 和 JavasScript。
- Rails 會在靜態檔案網址中加上時間序號,如果內容有修改則會重新產生。這樣的好處是強迫用戶的瀏覽器一定會下載到最新的版本,而不會有瀏覽器快取到舊版本的問題。
- 變更 Assets host 主機位址時,可以一次搞定,例如上 CDN 時。透過 Helpers,Rails 可以幫所有的 Assets 加上靜態檔案伺服器網址。
若想變更預設 assets
的位置,可以到 config/environments/production.rb
中可以看到相關的設定檔。
參考資料
- Action View Overview @ RailsGuides
- Layouts and Rendering in Rails @ RailsGuides
- Ruby on Rails 實戰聖經 @ ihower