Ruby on Rails 實戰聖經

使用 Rails 5.0+ 及 Ruby 2.3+

電子書製作中,歡迎留下 E-mail,有消息將會通知您。若您有任何意見、鼓勵或勘誤,也歡迎來信給我。願意贊助支持的話,这是我的支付宝微信 和乙太幣 ETH 地址
0x232b7245EBE02900c21682be1e6Ad4e839751F6a

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 ControllerModel程式中呼叫Helper,則可以加上ApplicationController.helpers前置詞。

靜態檔案輔助方法

使用Rails內建的靜態檔案(Assets)輔助方法有幾個好處:

  • Rails會合併StylesheetJavasSript檔案,可以加速瀏覽器的下載。
  • Rails會編譯SassCoffeeScript等透過Assets template engine產生的StylesheetJavasSript
  • Rails會在靜態檔案網址中加上時間序號,如果內容有修改則會重新產生。這樣的好處是強迫用戶的瀏覽器一定會下載到最新的版本,而不會有瀏覽器快取到舊版本的問題。
  • 變更Assets host主機位址時,可以一次搞定,例如上CDN時。透過HelpersRails可以幫所有的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標籤

移除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-mail
  • button_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逸出,例如會將<符號變成&lt;。也就是說如果使用者在表單中輸入了

<script>alert("Hack you!");</script>

那麼Rails在輸出<%= @event.description %>時,並不會乖乖地顯示一模一樣的<script>alert("Hack you!");</script>,因為如果如此的話,每個瀏覽這個網頁的使用者,就會去執行這個JavaSCript而被迫跳出一個alert視窗。

Rails會逸出HTML輸出成為:

&lt;script&gt;alert(&quot;Hack you!&quot;);&lt;/script&gt;

這樣使用者就會看到 <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逸出,造成顯示不正確,變成了:

&lt;div&gt;&lt;a href=&quot;/conferences/ae98a23f8fa23a3b060f&quot;&gt;foo&lt;/a&gt;&lt;br&gt;bar&lt;/div&gt;

這是因為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_tagtag方法來產生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>HTMLtag(: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_date
  • select_datetime
  • hidden_field
  • submit

搭配model用的f.check_box :column_namecheck_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標籤,瀏覽器會檢查必填、有PlaceholderAuto 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 %>

更多線上資源

》回到頁首