Action View: 样板设计

Action View - 样板设计

Any fool can write code that a computer can understand. Good programmers write code that humans can understand. - Martin Fowler

在这一章中我们将进入 MVC 架构中的 View ,也就是提供接口给用户操作,与我们的应用程式做互动。

ActionView 是 Rails 中处理 View 的元件名称,而提供给用户的文件,我们会用 Template 样板来呈现。本章假设读者们都对 HTML 有基本的认识。

Template样板

什么是 Template 样板呢?我们知道服务器最终提供给浏览器的格式是 HTML 文件,而 Template 样板就是动态产生 HTML 的方式。

相对的说,我们用静态 HTML 来称呼不经过程式产生的 HTML 文件

Rails 默认用来产生 Template 的方式是 Embedded Ruby(ERb) ,如果你曾经使用过 PHP 、 JSP 或 ASP ,那么你会非常熟悉这种内嵌程式码的风格,这是一种最为直觉且容易学习的方法。例如以下是一小段嵌入目前时间的 ERb ,中间<%= %>的部份便是 Ruby 程式:

<h1><%= Time.now.to_s %></h1>

Rails 的 Template 档案位置和名称也是有玄机的,例如 app/views/welcome/index.html.erb 来说, welcome 目录是它的 Controller 名称,档案第一段 index 是它的 Action 名称,附档名则是用来指定要用什么方式来产生什么格式的文件: index.html.erb 表示用 ERb 产生 HTML 格式的文件。会有这样惯例的原因,你可能已经猜到,那就是使用 ERb 不代表一定就是用来产生HTML。用什么 Template 引擎(在 Rails 中又叫作 Template Handler)产生文件,和文件的 Format 格式是两回事情。所以 ERb 其实可以用来产生任何文字档格式,例如 CSV 、 XML 、 JavaScript 等等。

虽然可以,但 ERb 并不是产生 XML 的最好方式,通常在我们会用 Builder 来产生 XML ,例如一个叫做 show.xml.builder 的档案:

people do |p|
  p.person "test"
end

就会产生以下的 XML :

<people>
  <person>test<person>
</people>

以下是内建的样板引擎与格式组合:

格式 引擎 用法
html、xhtml、js 任何文字格式都可以 erb HTML 样板,用 &lt% ruby code %> 和 &lt%= ruby variable %> 来内迁 Ruby 程式
html、xhtml、js 任何文字格式都可以 ruby Ruby 程式,最后的 return 值就是输出
html、xhtml、js 任何文字格式都可以 raw 直接输出不处理
json jbuilder 请参考 <https://github.com/rails/jbuilder>
xml、rss、atom builder 请参考 <https://github.com/jimweirich/builder>

扩充Template Handler

Rails 默认只有内建 ERb 和 Builder 这两套样板引擎,但要扩充非常容易。例如在 Rails 社群中,也很流行用 HAML 和 Slim 这两套样板引擎来取代 ERb 。这两套都利用缩排的技术简化 HTML 撰写的格式,例如以下的 HAML :

%section.container
    %h1= post.title
    %h2= post.subtitle
    .content
        = post.content

等同于以下的 ERb :

<section class=”container”>
    <h1><%= post.title %></h1>
    <h2><%= post.subtitle %></h2>
    <div class=”content”>
        <%= post.content %>
    </div>
</section>

要安装使用,只需要在 Gemfile 档案中加上gem "haml-rails"然后bundle install即可。不过相较于 ERb ,使用 HAML 虽然可以更为有效率地撰写 HTML 样板,但是还是需要考量团队中的网页设计师是否能够配合使用。

使用Renderer在Controller中直接回传结果

有一些格式的本质不一定需要 Template 引擎,可以在 Controller 中直接 render 其结果即可,例如 JSON 和 CSV 或是 XML 。 Rails 对 ActiveRecord model 提供了to _xml_ 和to_json方法。而_CSV_则可以使用 _FasterCSV 函式库。范例如下:

require 'csv'
class PeopleController < ApplicationController

def index
    @people = Person.all
    respond_to do |format|
      format.html
      format.json{ render :json => @person.to_json }
      format.xml { render :xml => @person.to_xml }
      format.csv do
        csv_string = CSV.generate do |csv|
            csv << ["Name", "Created At"]
            @people.each do |person|
                csv << [person.name, person.created_at]
            end
        end
        render :text => csv_string
      end
    end
end

ERb标籤

除了上述介绍的 ERb 标籤<%= %>会输出中间的Ruby程式执行结果,还有一些其他用法:

<% %>

这样就不会输出任何结果,通常用在if或循环条件中,例如:

<% @people.each do |person| %>
    <% if person.name.present? %>
        <p><%= person.name %></p>
    <% end %>
<% end %>

上述的<% %>标籤虽然不会输出 HTML 内容,但是还是在 HTML 原始码中换行了,为了避免输出时多馀的换行,可以改用<%- -%>。不过实际上并没有很多人在乎就是了,毕竟这不影响用户的页面。

<%# blah blah %>

这是注解,不会输出任何内容。不过如果需要整段多行注解,有个小技巧可以善用:

<% if false %>
    <%= foo %>
    <hr>
    <%= bar %>
<% end %>

Layout版型

Layout 可以用来包裹 Template 样板,让不同 View 可以共享 Layout 作为文件的头尾。因此我们可以为全站的页面建立共享的版型。这个档案默认是 app/views/layouts/application.html.erb 。如果在 app/views/layouts 目录下有跟某 Controller 同名的 Layout 档案,那这个 Controller 下的所有 Views 就会使用这个同名的 Layout 。

默认的 Layout 长得如下:

<!DOCTYPE html>
<html>
<head>
  <title>YourApplicationName</title>
  <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
  <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
  <%= csrf_meta_tags %>
</head>
<body>

<%= yield %>

</body>
</html>

其中的<%= yield %>会被替换成个别Action的样板。

开头的<!DOCTYPE html>说明了这是一份 HTML5 文件,这种宣告法向下相容于所有浏览器的 HTML4 。

如果需要指定 Controller 的 Layout ,可以这么做:

class EventsController < ApplicationController
   layout "special"
end

这样就会指定 Events Controller 下的 Views 都使用 app/views/layouts/special.html.erb 这个 Layout ,你可以加上参数:only或:except表示只有特定的 Action :

class EventsController < ApplicationController
   layout "special", :only => :index
end

或是

class EventsController < ApplicationController
   layout "special", :except => [:show, :edit, :new]
end

请注意到使用字串和 Symbol 是不同的。使用 Symbol 的话,它会透过一个同名的方法来动态决定,例如以下的 Layout 是透过determine_layout这个方法来决定:

class EventsController < ApplicationController
   layout :determine_layout

    private

    def determine_layout
          ( rand(100)%2 == 0 )? "event_open" : "event_closed"
    end
end

除了在 Controller 层级设定 Layout ,我们也可以设定个别的 Action 使用不同的 Layout ,例如:

def show
   @event = Event.find(params[:id])
    render :layout => "foobar"
end

这样 show Action 的样板就会套用 foobar Layout 。更常见的情形是关掉 Layout ,这时候我们可以写render :layout => false。

自定Layout内容

除了<%= yield %>会加载 Template 内容之外,我们也可以预先自定一些其他的区块让 Template 可以置入内容。例如,要在 Layout 中放一个侧栏用的区块,取名叫做:sidebar:

<div id="sidebar">
    <%= yield :sidebar %>
</div>
<div id="content">
    <%= yield %>
</div>

那么在 Template 样板中,任意地方放:

<%= content_for :sidebar do %>
   <ul>
       <li>foo</li>
       <li>bar</li>
   </ul>
<% end %>

那么这段内容就会被置入到 Layout 的<%= yield :sidebar %>之中。

除了侧栏之外,也常用这招让每一页的 HTML meta 特制化,例如我们可以放Facebook Open Graph,这样分享到 Facebook 时,就会抓取你设定的中介资料:

<head>
  <title>YourApplicationName</title>
  <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
  <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
  <%= csrf_meta_tags %>
  <%= yield :head %>
</head>

在 Template 样板中,加入:

<%= content_for :head do %>
    <%= tag(:meta, :content => @event.name, :property => "og:title") %>
    <%= tag(:meta, :content => @event.description, :property => "og:description") %>
    <%= tag(:meta, :content => "article", :property => "og:type") %>
    <%= tag(:meta, :content => @event.logo.url, :property => "og:image") %>
    <%= tag(:meta, :content => event_url(@event), :property => "og:url") %>
<% end %>

在Template中可以使用的变量

我们已经认识到,在 Controller Action 中使用@的物件变量,就会被传进 Template 中可以被存取。除此之外,还包括cookies、session、flash、params、request、response等在 Controller 中使用的变量也可以在 Template 中使用。

比较特别的是, Template 中的controller变量,我们可以用这个变量让每一页有不同的 CSS class ,例如

<%= tag(:body, :class => "#{controller.controller_name} #{controller.action_name}") %>

这样会输出成

<body class="events show">

局部样板Partials

局部样板可以将 Template 中重复的程式码抽出来,例如我们在 Part1 中示范过的新增和编辑的表单。 Partial Template 的命名惯例是底线开头,但是呼叫时不需加上底线,例如:

<%= render :partial => "common/nav" %>

在这个情境下,可以省略:partial键:

<%= render "common/nav" %>

这样便会使用 app/views/common/_nav.html.erb 这个样板。如果使用 Partial 的样板和 Partial 所在的目录相同,可以省略第一段的 common 路径。

在 Partial 样板中是可以直接使用实例变量的(也就是@开头的变量)。不过好的实务作法是透过:locals明确传递区域变量,这样程式会比较清楚, Partial 样板也比较容易被重复使用:

<%= render :partial => "common/nav", :locals => { :a => 1, :b => 2 } %>

在这个情境下,也可以进一步把locals键也省略:

<%= render "common/nav", :a => 1, :b => 2 %>

这样在 partial 样板中,就可以存取到区域变量a和b。

阵列型Collection

如果是阵列的资料,像是tr或li这类会一直重复的 Template 元素,我们可以使用collection参数来处理,例如像以下的程式:

<ul>
    <% @people.each do |person| %>
        <%= render :partial => "person", :locals => { :person => person } %>
    <% end %>
<ul>

我们可以改写成使用 collection 参数来支援阵列形式:

<ul>
    <%= render :partial => "person", :collection => @people, :as => :person %>
<ul>

在 __person.html.erb_ 这个 partial 中,会有一个额外的索引变量person_counter纪录编号。

使用 collection 的好处不只是少打字而已,还有执行效能上的大大改善, Rails 内部会针对这种形式做执行效率最佳化。

更多线上资源