Action View - Helpers 方法
Measuring programming progress by lines of code is like measuring aircraft building progress by weight. - Bill Gates
在Rails中,Helper指的是可以在Template中使用的輔助方法,主要用途是可以將資料轉化成輸出用的HTML字串,例如我們已經用過了Rails內建的link_to
方法,它可以將字串變成超連結。Rails還內建了許多Helper方法,可以讓我們建構HTML更為容易。我們在一章中將介紹其中較為常用的幾個方法。
另一個使用Helper的理由是可以簡化Template中的複雜結構,將Template中較為複雜的程式都用Helper包裝起來,最好讓Template只包含單純的變數以及最簡單的條件邏輯和迴圈,這樣就算是不會程式的網頁設計師,也能夠輕易了解套版甚至修改Template樣板。
因為Helper預設只能在Template中使用,如果想在rails console中呼叫,必須加上
helper
,例如helper.link_to
。另外,雖然機會不多,如果真的要在Rails Controller或Model程式中呼叫Helper,則可以加上ApplicationController.helpers
前置詞。
靜態檔案輔助方法
使用Rails內建的靜態檔案(Assets)輔助方法有幾個好處:
- Rails會合併Stylesheet和JavasSript檔案,可以加速瀏覽器的下載。
- Rails會編譯Sass和CoffeeScript等透過Assets template engine產生的Stylesheet和JavasSript
- Rails會在靜態檔案網址中加上時間序號,如果內容有修改則會重新產生。這樣的好處是強迫用戶的瀏覽器一定會下載到最新的版本,而不會有瀏覽器快取到舊版本的問題。
- 變更Assets host主機位址時,可以一次搞定,例如上CDN時。透過Helpers,Rails可以幫所有的Assets加上靜態檔案伺服器網址。
幾個常用的方法:
javascript_include_tag
stylesheet_link_tag
auto_discovery_link_tag
favicon_link_tag
image_tag
video_tag
audio_tag
格式化輔助方法
simple_format
將\n
換行字元換成HTML的<br>
標籤。在表單輸入的<textarea></textarea>
中,換行其實是\n
控制字元,因此輸出在網頁上時,\n
代表的是在HTML原始碼中換行,因此我們經常需要將\n
再換成<br>
標籤,這樣瀏覽器看到的畫面才有換行的呈現。
<%= simple_format("foo\nbar") %>
# 輸出 "<p>foo\n<br />bar</p>"
truncate
擷取前幾個字元
<%= truncate("Once upon a time in a world far far away") %>
# 輸出 "Once upon a time in a world..."
<%= truncate("Once upon a time in a world far far away", length: 17) %>
# 輸出 "Once upon a ti..."
strip_tags
移除HTML標籤
strip_links
移除HTML超連結標籤
distance_of_time_in_words
輸出很潮的時間距離,例如
distance_of_time_in_words(Time.now, Time.now + 60.minutes)
=> "about 1 hour"
distance_of_time_in_words_to_now
distance_of_time_in_words_to_now(Time.now - 1.second)
=> "less than a minute"
time_tag
輸出HTML5時間標籤
time_tag(Time.now)
=> "<time datetime=\"2014-11-03T23:55:11+08:00\">November 03, 2014 23:55</time>"
number_with_delimiter
number_with_delimiter(1234567)
=> "1,234,567"
number_with_precision
number_with_precision(123.4567, precision: 2)
=> "123.46"
URL輔助方法
link_to
文字超連結
除了學過的 <%= link_to '超連結文字', xxx_path %>
用法之外,如果超連結文字很多甚至有圖片,可以用 block 的方式改寫,例如:
<%= link_to user_path(user) do %>
<%= image_tag user.avatar.url %> <%= user.display_name %>
<% end %>
mail_to
E-mailbutton_to
按鈕連結,這個預設會改用 POST 出去(實際是個只有按鈕的表單)。因此如果超連結有用:method => :post
等非 GET 方法時,建議可以考慮改用button_to
而不是link_to
,這樣在 JavaScript 失效的情況下仍然可以作用,滿足網頁無障礙的標準。current_page?(url)
是否目前是url這個頁面,通常是在layout上搭配tab樣式做active效果
自定Helper
除了使用Rails內建的Helper,我們可以建立自定的Helper,只需要將方法定義在app/helpers/目錄下的任意一個檔案就可以了。在產生Controller的同時,Rails就會自動產生一個同名的Helper檔案,照慣例該Controller下的Template所用的Helper,就放在該檔案下。如果是全站使用的Helper,則會放在app/helpers/application_helper_rb,例如:
module ApplicationHelper
def gravatar_url(email)
gravatar_email = Digest::MD5.hexdigest(email.downcase)
return "http://www.gravatar.com/avatar/#{gravatar_email}?s=48"
end
end
如此便可以在Template中這樣使用:
<%= image_tag gravatar_url(user.email) %>
Helper是全域的,定義在哪一個檔案中沒有關係,檔案名稱也不需要與Controller名稱對應。
如果想要寫出 Helper 可以傳 Block 參數,例如上述的 link_to
傳 block,像這樣:
<%= my_helper do %>
blah
<% end %>
則可以這樣定義 Helper:
def my_helper(&block)
tmp = capture(&block)
"header #{tmp} footer" # 最後輸出 header blah foobar
end
或是這樣用
def my_helper(&block)
content_tag(:p, {}, &block) # 最後輸出 <p>blah</p>
end
另外,Controller裡面定義的方法,也可以用helper_method
曝露出來當作Helper,例如
class ApplicationController < ActionController::Base
#...
helper_method :current_user
protected
def current_user
@current_user = User.find(session[:user_id]) if session[:user_id]
end
end
如何安全地處理HTML逸出問題?
Rails在輸出任何內容在網頁上時,為了安全性都會作HTML逸出,例如會將<
符號變成<
。也就是說如果使用者在表單中輸入了
<script>alert("Hack you!");</script>
那麼Rails在輸出<%= @event.description %>
時,並不會乖乖地顯示一模一樣的<script>alert("Hack you!");</script>
,因為如果如此的話,每個瀏覽這個網頁的使用者,就會去執行這個JavaSCript而被迫跳出一個alert
視窗。
Rails會逸出HTML輸出成為:
<script>alert("Hack you!");</script>
這樣使用者就會看到 <script>alert("Hack you!");</script>
,而不是去執行<script>alert("Hack you!");</script>
。
XSS是很常見的網站攻擊手法,攻擊者可以以此植入一些惡意的JavaScript讓其他使用者不經意執行,包括竊取Cookie和盜用權限等等。Rails預設採用了全部逸出的方式來防睹這個安全性。不過,也因此開發者必須了解如何正確地關閉這個功能,當你想要顯示出HTML不要逸出的時候。
如何顯示使用者輸入的HTML內容?
如果你的表單允許使用者輸入HTML,那麼你會想要在輸出的時候,可以開放一些白名單不要做HTML逸出,這時候就使用sanitize
這個輔助方法,例如:
<%= sanitize( @event.description ) %>
預設允許的HTML標籤和屬性如下:
ActionView::Base.sanitized_allowed_tags
=> #<Set: {"strong", "em", "b", "i", "p", "code", "pre", "tt", "samp", "kbd", "var", "sub", "sup", "dfn", "cite", "big", "small", "address", "hr", "br", "div", "span", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "li", "dl", "dt", "dd", "abbr", "acronym", "a", "img", "blockquote", "del", "ins"}>
ActionView::Base.sanitized_allowed_attributes
=> #<Set: {"href", "src", "width", "height", "alt", "cite", "datetime", "title", "class", "name", "xml:lang", "abbr"}>
如果需要增加,可以在config/application.rb
中新增,例如:
config.action_view.sanitized_allowed_tags = %w[table tr td]
config.action_view.sanitized_allowed_attributes = "rel"
當然,如果表單輸入資料的地方,只限於後台有權限可信任使用者,我們也可以完全放行不要逸出:
<%= raw( @event.description ) %>
或
<%= @event.description.html_safe %>
自訂 Helper 的技巧
當你想要自訂一個Helper組合一些標籤和變數的時候,你可能第一次會嘗試這樣寫:
def user_link(user)
"<div>" +
link_to(user.name, user_path(user)) + "<br>" + user.description +
"</div>"
end
這裡我們試圖組合一個字串是<div>
包超連結和user.description
變數。不過輸出在畫面上的時候,Rails還是會做了HTML逸出,造成顯示不正確,變成了:
<div><a href="/conferences/ae98a23f8fa23a3b060f">foo</a><br>bar</div>
這是因為Rails預設會將字串判斷成尚未逸出,如果一個未逸出的字串跟一個逸出的安全字串相加,就會髒掉也變成一個未逸出的字串。
為了正確顯示,接下來你可能會嘗試關閉HTML逸出:
def user_link(user)
str = "<div>" +
link_to(user.name, conference_path(user)) + "<br>" + @event.description +
"</div>"
str.html_safe # 或 raw(str)
end
這樣畫面上就顯示正確了。不過這卻是一個錯誤的作法,因為讓整個str
不要逸出,卻讓其中的@event.description
變成一個安全上的漏洞。
一個辦法是我們小心翼翼的幫每個不用逸出的字串加上html_safe
:
def user_link(user)
"<div class='user'>".html_safe +
link_to(user.name, conference_path(user)) + "<br>".html_safe + @event.description +
"</div>".html_safe
end
或用 safe_join
方法:
def user_link(user)
safe_join([ "<div class='user'>".html_safe,
link_to(user.name, conference_path(user)),
"<br>".html_safe,
@event.description,
"</div>".html_safe])
end
如果你用原生的 Array#join 來把陣列串接成字串的話
["<div class='user'>".html_safe, link_to(user.name, conference_path(user)), "<br>".html_safe, @event.description, "</div>".html_safe].join
最後輸出仍會逸出,要改用safe_join
另一個比較漂亮程式化的作法,則是善用Rails內建的content_tag
和tag
方法來產生HTML:
def user_link(user)
content_tag(:div,
link_to(user.name, conference_path(user)) + tag(:br) + @event.description,
:class => 'user' )
end
其中content_tag(:div, "YOUR_CONTENT", :class => "YOUR_CSS_CLASS" )
可以產生<div class="YOUR_CSS_CLASS"> YOUR_CONTENT </div>
的HTML。tag(:br)
會產生<br />
。
表單輔助方法
對網頁應用程式來說,表單是非常重要的用戶輸入介面。Rails在這方面也提供了很多好用的Helper方法。基本上,Rails處理表單分成兩種類型:
一種是對應到Model物件的新增、修改,我們會使用form_for
這個Helper。它的好處在於透過傳入Model物件,可以在修改的時候自動幫你將預設值帶入。例如我們已經在Part1使用過的event表單:
<%= form_for @event do |f| %>
<%= f.text_field :name %>
<%= f.submit %>
<% end %>
另一種是就是沒有對應Model的表單,我們使用form_tag
這個方法。例如:
<%= form_tag "/search" do %>
<%= text_field_tag :keyword %>
<%= submit_tag %>
<% end %>
和form_for
有些類似,但是其中不需要傳Block變數f
,其中的欄位Helper需要多加_tag結尾。不像form_for
的欄位名稱一定要是Model的屬性之一,在form_tag
之中的欄位名稱則完全不受限。
幾個常用的表單欄位輔助方法:
- label
- text_field
- text_area
- radio_button
- check_box
- file_field
- select
- 使用 select 有一個坑:如果你要加
class
的話,必須這樣寫f.select :xxx, {}, :class => "your-class-name"
,多出來的{}
是因為這個 select API 的最後兩個參數都是 Hash,必須多包一個 {} 才能讓:class
擠到最後的參數去。
- 使用 select 有一個坑:如果你要加
- select_date
- select_datetime
- hidden_field
- submit
搭配model用的
f.check_box :column_name
和check_box_tag :input_name
有微妙的差異,前者會多產生一個隱藏的hidden_field :column_name, "0"
來表示沒有勾選的狀態,後者則不會。這是因為如果你沒有勾選的話,瀏覽器就不會送出check_box的資料,因此Rails用了一個隱藏欄位來處理反勾選。
搭配ActiveRecord關聯的輔助方法:
- collection_select
- collection_radio_buttons
- collection_check_boxes
一些HTML5的輔助方法
- color_field
- date_field
- email_field
- month_field
- number_field
- url_field
- range_field
- search_field
以下這些屬性可以設成true
加到表單方法方法裡面
disabled readonly multiple checked autobuffer autoplay controls loop selected hidden scoped async defer reversed ismap seamless muted required autofocus novalidate formnovalidate open pubdate itemscope allowfullscreen default inert sortable truespeed typemustmatch
例如
<%= f.text_field :name, :required => true, :autofocus => true, :placeholder => "Please enter your name" %>
會產生出這樣的HTML5標籤,瀏覽器會檢查必填、有Placeholder和Auto focus:
<input placeholder="Please enter your name" required="required" autofocus="autofocus" class="form-control" type="text" name="event[name]" id="event_name">
如何處理Model中不存在的屬性
使用form_for時,其中的欄位必須是Model有的屬性,那如果資料庫沒有這個欄位呢?這時候你依需要在Model程式中加上存取方法,例如:
class Event < ApplicationRecord
#...
def custom_field
# 根據其他屬性的值或條件,來決定這個欄位的值
end
def custom_field=(value)
# 根據value,來調整其他屬性的值
end
end
這樣就可以在form_for裡使用custom_field
了。
<%= form_for @event do |f| %>
<%= f.text_field :custom_field %>
<%= f.submit %>
<% end %>
記得把:custom_field
也加到Strong Parameters清單裡,這樣按下送出後,就可以跟著@event
本來的欄位一起處理了。
資料驗證錯誤時的處理
當Model物件儲存失敗時,我們通常會重新顯示表單,這時候該怎麼顯示Model的錯誤訊息呢? 以下是一個預設的範例:
<%= form_for(@person) do |f| %>
<% if @person.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@person.errors.count, "error") %> prohibited this person from being saved:</h2>
<ul>
<% @person.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<%= f.text_field :name %>
<%= f.submit %>
<% end %>
透過檢查@person.errors
我們可以把所有的錯誤訊息顯示出來。除了這種作法,我們也可以把錯誤訊息放在輸入框的旁邊:
<%= form_for(@person) do |f| %>
<%= f.text_field :name %>
<% if @person.errors[:name].presence %>
<%= @person.errors[:name].join(", ") %>
<% end %>
<%= f.submit %>
<% end %>
如何在 Controller 和 Model 裡面呼叫 Helper 方法
雖然違反 MVC 原則,不過有時候要處理的問題就是 HTML,因此也是有可能在 Controller 或 Model 內,想要呼叫 Helper 方法。
如果是在 Controller 裡面,可以直接透過 helpers
來轉接,例如想要用 link_to
方法:
class UsersController < ApplicationController
def index
link_html = helpers.link_to("回首頁", root_path)
# ......
end
end
如果是在 Model 內,則改用 ApplicationController.helpers
,例如:
class User < ActiveRecord::Base
def link_html
ApplicationController.helpers.link_to("用戶連結", self.id)
end
end