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内建的linkto方法,它可以将字串变成超连结。_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>标籤,这样浏览器看到的画面才有换行的呈现。

  1. <%= simple_format("foo\nbar") %>
  2. # 输出 "<p>foo\n<br />bar</p>"

truncate

撷取前几个字符

  1. <%= truncate("Once upon a time in a world far far away") %>
  2. # 输出 "Once upon a time in a world..."
  3. <%= truncate("Once upon a time in a world far far away", length: 17) %>
  4. # 输出 "Once upon a ti..."

strip_tags

移除HTML标籤

移除HTML超连结标籤

distance_of_time_in_words

输出很潮的时间距离,例如

  1. distance_of_time_in_words(Time.now, Time.now + 60.minutes)
  2. => "about 1 hour"

distance_of_time_in_words_to_now

  1. distance_of_time_in_words_to_now(Time.now - 1.second)
  2. => "less than a minute"

time_tag

输出HTML5时间标籤

  1. time_tag(Time.now)
  2. => "<time datetime=\"2014-11-03T23:55:11+08:00\">November 03, 2014 23:55</time>"

number_with_delimiter

  1. number_with_delimiter(1234567)
  2. => "1,234,567"

number_with_precision

  1. number_with_precision(123.4567, precision: 2)
  2. => "123.46"

URL辅助方法

  • link_to 文字超连结

除了学过的 <%= link_to '超连结文字', xxx_path %>用法之外,如果超连结文字很多甚至有图片,可以用 block 的方式改写,例如:

  1. <%= link_to user_path(user) do %>
  2. <%= image_tag user.avatar.url %> <%= user.display_name %>
  3. <% end %>
  • mail_to E-mail
  • button_to 按钮连结,这个默认会改用 POST 出去(实际是个只有按钮的表单)。因此如果超连结有用 :method => :post 等非 GET 方法时,建议可以考虑改用 button_to 而不是 link_to,这样在 JavaScript 失效的情况下仍然可以作用,满足网页无障碍的标准。
  • currentpage?(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里面定义的方法,也可以用helpermethod曝露出来当作_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内建的contenttagtag方法来产生_HTML

def user_link(user)
  content_tag(:div,
      link_to(user.name, conference_path(user)) + tag(:br) + @event.description,
    :class => 'user' )
end

其中contenttag(:div, "YOUR_CONTENT", :class => "YOUR_CSS_CLASS" )可以产生<div class="YOUR_CSS_CLASS"> YOUR_CONTENT </div>的_HTMLtag(:br)会产生<br />

表单辅助方法

对网页应用程式来说,表单是非常重要的用户输入接口。Rails在这方面也提供了很多好用的Helper方法。基本上,Rails处理表单分成两种类型:

一种是对应到Model物件的新增、修改,我们会使用formfor这个_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 %>

formfor有些类似,但是其中不需要传_Block变量f,其中的字段Helper需要多加_tag结尾。不像formfor的字段名称一定要是_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 %>

记得把:customfield也加到_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 %>

更多线上资源