Action View - 样板设计

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

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

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

Template样板

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

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

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

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

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

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

  1. people do |p|
  2. p.person "test"
  3. end

就会产生以下的XML

  1. <people>
  2. <person>test<person>
  3. </people>

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

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

扩充Template Handler

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

  1. %section.container
  2. %h1= post.title
  3. %h2= post.subtitle
  4. .content
  5. = post.content

等同于以下的ERb

  1. <section class=”container”>
  2. <h1><%= post.title %></h1>
  3. <h2><%= post.subtitle %></h2>
  4. <div class=”content”>
  5. <%= post.content %>
  6. </div>
  7. </section>

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

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

有一些格式的本质不一定需要Template引擎,可以在Controller中直接render其结果即可,例如JSONCSV或是XMLRailsActiveRecord model提供了toxmlto_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

如果需要指定ControllerLayout,可以这么做:

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中可以被存取。除此之外,还包括cookiessessionflashparamsrequestresponse等在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样板中,就可以存取到区域变量ab

阵列型Collection

如果是阵列的资料,像是trli这类会一直重复的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内部会针对这种形式做执行效率最佳化。

更多线上资源