Rails :布局和視圖渲染


原文地址: 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_stringrender 的用法完全一樣,不過不會把響應發送給瀏覽器,而是直接返回字符串。

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 方法,指定模板的完整路徑即可。例如,如果控制器 AdminProductsControllerapp/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.erbapp/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

這么聲明后,除了 rssindex 動作之外,其他動作都使用 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 視圖,導致錯誤出現。解決的辦法很簡單,確保在一次代碼運行路線中只調用一次 renderredirect_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_tourl_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 renderredirect_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/1show 動作發起請求,控制器做了查詢,但沒有找到對應的圖書,所以返回 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 渲染響應的視圖時,會把視圖和當前模板結合起來。查找當前模板的方法前文已經介紹過。在布局中可以使用三種工具把各部分合在一起組成完整的響應:

  • 靜態資源標簽
  • yieldcontent_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_tagjavascript_include_tagstylesheet_link_tag 最常出現在布局的 <head> 中。

靜態資源標簽幫助方法不會檢查指定位置是否存在靜態資源,假定你知道自己在做什么,只負責生成對應的鏈接。

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/assetslib/assetsvendor/assets。詳細說明參見 Asset Pipeline 中的“靜態資源的組織方式”一節。

文件的地址可使用相對文檔根目錄的完整路徑,或者是 URL。例如,如果想鏈接到 app/assetslib/assetsvendor/assets 文件夾中名為 javascripts 的子文件夾中的文件,可以這么做:

<%= javascript_include_tag "main" %>

Rails 生成的 script 標簽如下:

< script src = '/assets/main.js' ></ script >

對這個靜態資源的請求由 Sprockets gem 伺服。

同時引入 app/assets/javascripts/main.jsapp/assets/javascripts/columns.js 可以這么做:

<%= javascript_include_tag "main" , "columns" %>

引入 app/assets/javascripts/main.jsapp/assets/javascripts/photos/columns.js

<%= javascript_include_tag "main" , "/photos/columns" %>

引入 http://example.com/main.js

<%= javascript_include_tag "http://example.com/main.js" %>

stylesheet_link_tag 幫助方法為指定的每個資源生成 HTML <link> 標簽。

如果啟用了 Asset Pipeline,這個幫助方法生成的鏈接指向 /assets/stylesheets/,由 Sprockets gem 伺服。樣式表文件可以存放在三個位置:app/assetslib/assetsvendor/assets

文件的地址可使用相對文檔根目錄的完整路徑,或者是 URL。例如,如果想鏈接到 app/assetslib/assetsvendor/assets 文件夾中名為 stylesheets 的子文件夾中的文件,可以這么做:

<%= stylesheet_link_tag "main" %>

引入 app/assets/stylesheets/main.cssapp/assets/stylesheets/columns.css

<%= stylesheet_link_tag "main" , "columns" %>

引入 app/assets/stylesheets/main.cssapp/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 屬性,例如 idclass 等。

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 引用渲染的實例。

渲染集合還有個簡寫形式。假設 @productsproduct 實例集合,在 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 即可。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM