PJCHENder 未整理筆記

[Rails] Action View, Layout, Partial, Render

2017-10-03

@(Ruby on Rails)[Ruby, RailsGuides]

[Rails] Action View, Layout, Partial, Render

Action View Overview @ RailsGuides
Layouts and Rendering in Rails @ RailsGuides

[TOC]

速記

1
2
3
4
5
# 當 yield 後沒有接任何參數時,
# 它會自動轉譯該 controller#action 下的
# views/controller_name/action_name.html.erb 樣版(template)

<%= yield %>

概念

在 Rails 中,web requests 是透過 Action ControllerAction Viewer 來處理,一般來說 Action Controller 關注的是和資料庫溝通以及處理 CRUD;Action View 則負責編譯要回傳的 response。

從 Controller 中,我們主要有三種建立回應的方式,分別是 render, redirect_tohead

  • render: 建立一個完整的回應,並回傳給瀏覽器。
  • redirect_to:傳送 HTTP redirect status code 給瀏覽器。
  • head:傳送只有 HTTP headers 的內容給瀏覽器。

在 Rails 中,一份完整的 HTML 檔會包含 templates, partialslayouts 三個部分。

Templates

一般來說,我們不會在 View 中去使用 putsprint 來輸出結果,而是直接使用 <%= %>

慣例:在 articles_controller.rb 中的 index actionarticles#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:

1
2
3
4
5
# 會去轉譯同資料夾下的 _menu.html.erb 這支檔案
<%= render "menu" %>

# 會去轉譯 app/views/shared/_menu.html.erb 這支檔案
<%= render "shared/menu" %>

render 可以接受兩個參數,partialslocals在預設的情況下,會以和該 partials 同名的方式來命名 partial 中的 local variable

透過 locals 我們可以在 _product.html.erb 這個 partial 中使用 product 這個 local variable,值則是 @product

1
2
3
4
5
6
7
<%= render partial: "product", locals: { product: @product } %>

# 縮寫
<%= render "product", product: @product %>

# 再縮
<%= render partial: "product" %>

options

  • object:可以在 partial 的檔案中透過 product 這個 local_variable 取得 @item
1
<%= render partial: 'product', object: @item %>
  • as:搭配 object 使用,可以在 partial 的檔案中透過 item 這個 local_variable 取得 @item
1
2
3
4
<%= 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 名稱:

1
2
3
4
5
6
7
8
9
10
# 原本的寫法
<% @products.each do |product| %>
<%= render partial: "product", locals: { product: product } %>
<% end %>

# 使用 collection 關鍵字
<%= render partial: "product", collection: @products %>

# 縮寫
<%= render @products %>

render 的使用 (in Controller)

1
2
3
4
5
6
7
8
9
10
11
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:

1
2
3
4
5
6
# 會自動轉譯 `app/views/books/index.html.erb`
class BooksController < ApplicationController
def index
# render 'index' # 可以省略會自動轉譯
end
end

轉譯同一個 controller 內的不同 template

render [String || Symbol]

1
2
3
4
5
class BooksController < ApplicationController
def index
render :index # 等同於 render 'index'
end
end

轉譯不同 controller 中的 template

如果你是在 app/controllers/admin 中的 AdminProductsController 你可以透過 render "products/show" 來轉譯 app/views/products

1
2
3
4
5
6
7
# 在 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 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' 的指令,而且預設可以回傳任何形態的檔案

1
2
3
def index
@posts = Post.all
end

所以當網址是使用不同的副檔名時,都會回傳相對應的內下:

1
2
3
http://localhost:3000/posts        # 等同於 http://localhost:3000/posts.html
http://localhost:3000/posts.js
http://localhost:3000/posts.json

但也可以透過 respond_to 指定只能回傳的檔案類型,這裡則是可以回傳 htmljson 格式的檔案,當我們去請求其他的檔案型態時,都會回傳錯誤:

1
respond_to :html, :json

至於要回傳哪個資料型態回去,取決於 client 所傳送的 HTTP Accept Header 中希望的資料型態為何:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 就可以了:

1
2
3
4
5
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

如果想要變更預設值,可以使用:

1
2
3
4
5
6
7
8
9
# 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 透過 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:

1
2
3
4
class ProductsController < ApplicationController
layout "product", except: [:index, :rss]
# render layout: false 這個 action 不要套用版型
end

注意事項

要避免在重複 Render 的錯誤出現,在下面的例子中,可能會同時 render action: "special_show"render action: "regular_show",這時候為導致錯誤:

1
2
3
4
5
6
7
def show
@book = Book.find(params[:id])
if @book.special?
render action: "special_show"
end
render action: "regular_show"
end

其中一個簡單的解法是透過 and return (不能使用 && return

1
2
3
4
5
6
7
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 有沒有被呼叫過,因此下面這段程式碼不會有錯誤:

1
2
3
4
5
6
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)

1
2
3
4
5
6
7
<html>
<head>
</head>
<body>
<%= yield %>
</body>
</html>

多個 yield

1
2
3
4
5
6
7
8
<html>
<head>
<%= yield :head %>
</head>
<body>
<%= yield %>
</body>
</html>

要在 yield 中轉譯內容,必須透過 content_for 關鍵字:

1
2
3
4
5
<% 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。

1
2
3
4
5
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 將不會是空值。

1
2
3
4
5
6
7
8
9
10
11
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 頁面,會消耗比較多的時間,因此更好的寫法如下:

1
2
3
4
5
6
7
8
9
10
11
12
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 中可以看到相關的設定檔。

參考資料

掃描二維條碼,分享此文章