Action View:__Helpers方法

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 标籤

移除 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 _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_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 %>

更多线上资源