美文网首页
Action View

Action View

作者: Cloneable | 来源:发表于2021-02-08 13:50 被阅读0次

    我们已经学习过路由组件如何操作 controller 以及 controller 如何选择 action 的。然后我们还学习了 controller 和 action 是如何确定向用户渲染的内容。通常渲染的操作都出现在 action 结尾。本章将围绕这个问题展开,Action View 包括了所有用于渲染模板的工具,比如较常用的向用户渲染 HTML、XML 或 JavaScript 等。根据此组件名称可知,Action View 是 MVC 中的 view 部分。

    在本章中,我们要从模板开始讲起,Rails 为其提供了许多参数。接着要学习为用户提供的输入方式:表单、文件上传和链接。最后学习使用辅助方法、布局和 partial 简化运维工作。

    使用模板

    你在编写 view 的时候其实是在编写模板,到时将根据模板展开生成结果。要了解模板如何运作,必须先了解三块知识。

    • 模板路径

    • 模板运行环境

    • 模板中的内容

    模板路径

    render() 方法期望在 app/views 路径中找到模板。在文件夹下,为每个 controller 都准备了相应的子文件夹使维护十分方便。实际例子就是 Depot 应用中的 products 和 store controller。最终你会在 app/views/products 和 app/view/store 中找到模板。每个路径下都包含了相应 controller 的 action 命名的模板。

    也有些模板并不是以 action 名字命名。你可以在 controller 中通过下列的调用方式渲染指定模板:

    render(action: 'fake_action_name')
    render(template: 'controller/name')
    render(file: 'dir/template')
    

    最后一种渲染方式可以处理文件系统中任意路径中的模板。如果你打算在多个应用中共用模板它将是最佳选择。

    模板环境

    模板中是代码和文本的混合体。模板中的代码可以处理响应中的动态内容。这些代码运行于由 controller 创立并提供可访问信息的环境中。

    • controller 中的所有实例变量都可以应用于模板中。这也是 action 与模板交互的方式。

    • controller 对象的 flash、headers、logger、params、request、response 和 session 都可以在 view 中访问。除了 flash 之外其他几项 view 并不能直接操作,因为它们处理的响应结果还遗留于 controller 中。不过在排查错误时它们十分有用。例如,下列的 html.erb 模板通过 debug() 方法展示 session 的内容、参数的细节和当前的响应:

    <h4>Session</h4> <%= debug(session) %>
    <h4>Params</h4> <%= debug(params) %>
    <h4>Response</h4> <%= debug(response) %>
    
    • 当前 controller 对象是通过属性名称 controller 获取。它允许模板调用任何 controller 中的公共方法(包括 ActionController::Base 中的方法)。

    • 模板的基础路径是存储在 base_path 属性中。

    模板中的内容

    Rails 创造性地支持四种类型的模板。

    • 使用 Builder 库构建 XML 响应模板。我们会在 393 页讨论 Builder 的相关知识。

    • CoffeeeScript 模板用于创建 JavaScript,它可以改变浏览器中内容的表现和行为。

    • ERB 模板是内容与嵌入 Ruby 代码的混合体。它通常用来生成 HTML 页面。我们会在 395 页继续讨论 ERB。

    • SCSS 模板用于创建 CSS 样式表,它可以控制浏览器内容的展现形式。

    虽然有如此多类型的模板,但最常用的还是 ERB。实际上,在 Depot 的开发中已经广泛地应用过 ERB 模板了。

    本章接下来的内容主要关注输出。在 309 页,我们再关注输入操作。在设计良好的应用中,输入输出有很强的关联性,我们输出的内容包括表单、链接和导航按钮,以此引导终端用户处理下一步的输入。此时你可能已经开始期盼,Rails 会向我们提供大量深思熟虑的辅助方法。

    生成表单

    HTML 提供了许多元素、属性以及用于收集输入的属性值。虽然可以在模板中硬编码,但我们并不需要这样做。

    在本节中,我们要学习 Rails 提供用于帮助我们处理流程的服务方法。在 351 页,我们会展示如何自定义辅助方法。

    HTML 提供了许多方法收集表单数据。一些更常见的方法在下图中展示。要注意表单本身并不代表典型用法,通常我们只是使用其中的一部分方法收集数据。

    Some of the common ways to enter data into forms

    让我们看看生成表单的模板:

    <%= form_for(:model) do |form| %>
      <p>
        <%= form.label :input %>
        <%= form.text_field :input, :formplaceholder => 'Enter text here...' %>
      </p>
      <p>
        <%= form.label :appddress, :style => 'float: left' %>
        <%= form.text_area :address, :rows => 3, :cols => 40 %>
      </p>
      <p>
        <%= form.label :color %>:
        <%= form.radio_button :color, 'red' %>
        <%= form.label :red %>
        <%= form.radio_button :color, 'yellow' %>
        <%= form.label :yellow %>
        <%= form.radio_button :color, 'green' %>
        <%= form.label :green %>
      </p>
      <p>
        <%= form.label 'condiment' %>:
        <%= form.check_box :ketchup %>
        <%= form.label :ketchup %>
        <%= form.check_box :mustard %>
        <%= form.label :mustard %>
        <%= form.check_box :mayonnaise %>
        <%= form.label :mayonnaise %>
      </p>
      <p>
        <%= form.label :priority %>:
        <%= form.select :priority, (1..10) %>
      </p>
      <p>
        <%= form.label :start %>:
        <%= form.date_select :start %>
      </p>
      <p>
        <%= form.labellabel :alarm %>:
        <%= form.time_select :alarm %>
      </p>
    <% end %>
    

    在模板中,你会看到许多 label,比如在第 3 行就有。通过 label 可以在输入框前展示关联的文本。如果没有明确定义的话,label 的文本默认使用属性名。

    当使用 text_field()text_area() 辅助方法时可以收集单行和多行输入框数据(第 4 行和第 8 行是典型例子)。你也可以指定 placeholder,当用户没有输入数据时在输入框中将显示 placeholder 数据。并不是所有的浏览器都支持这种方法,不支持的浏览器只会显示一个空输入框。当出现完全降级时,并不需要你设计最小公分母,因为我们会从所看到的方法中获益。

    placeholder 是许多 HTML5 提供的微小但美观实用的特性之一。即使用户的浏览器尚未安装但 Rails 已经准备妥当。你可以通过 search_field()telephone_field()url_field()email_field()number_field()range_field() 辅助方法提示指定的输入框类型。浏览器是如何使不同的信息发挥作用的?根据不同的浏览器显示同一种元素时会产生些许差异。比如,Mac 中的 Safari 会将搜索框显示为圆角,并且会添加符号 x 用于清除输入的数据。还有些会提供额外的验证。比如,Opera 会在提交信息前先验证 URL 属性。iPad 在输入邮箱地址时提前预备好 @ 符。

    尽管这些方法的差异是浏览器导致的,但如果没有额外支持的情况下它们将按原始形态展示,也就是不加任何修饰的输入框。再说一次,不作为便无所得。如果你打算显示一个邮箱地址的输入框,便不要单纯使用 text_field(),而首先应该考虑使用 email_field()

    12、22 和 32 行通过三种不同的方式提供了约束条件。尽管不同的浏览器显示会有差异,但这种方式是跨浏览器时的最佳选择。select() 特别灵活,它基于简单的 Enumeration 显示,也就是一组成对的名字及数据,或者使用 Hash。许多表单的 option 方法都可以通过不同的数据源生成列表,包括数据库这种数据源。

    最后 37 行和 42 行分别展示了关于日期和时间的提示。正如你所期望的,Rails 在此处也提供了许多选项。

    hidden_field()password_field() 并没有在上述例子中展示。隐藏的输入框将一直不显示,但其中的数据依然会传回服务器。这对于在 session 中存储的临时数据比较有用,通过这种方式便可以在两个请求间传递数据。密码输入框也显示的,但输入的字符会被隐藏。

    这些方法已经能够满足一个初学者的需要。如果你发现自己需要的功能当前方法无法满足时,可以查找相应的辅助方法,或者使用 gem。最后的起步指导是 Rails 指南。

    同时,让我们探究一番表单数据的提交流程。

    表单流程

    在下图中我们可以看到多个 model 的属性通过 controller 传递至 view 中,直到 HTML 页面中,最后再回到 model。model 对象也有 namecountrypassword 这些属性。模板可以通过辅助方法构建 HTML 表单,使用户能够编辑 model 中的数据。要注意表单字段的命名方式。比如,country 属性匹配 HTML 中命名为 user[country] 的输入域。

    Models, controller, and views work together

    当用户提交表单时,原生的 POST 数据会传送给我们的应用。Rails 将从表单中提取字段并构建 params 哈希对象。简单些的数据(比如 id 字段,会由来自表单 action 的路由提取)是直接存储在哈希中。但如果参数名称被中括号包裹,Rails 会认为它是结构化数据中的一部分并通过构建哈希对象处理它。在哈希中,中括号里的字符串表示键。如果参数名出现在多个中括号中就重复此步骤。

    Form Parameters Params
    id=123 { id: "123" }
    user[name]=Dave { user: { name: "Dave" }}
    user[address][city]=Wien { user: { address: { city: "Wien" }}}

    在流程整体的最后,model 对象会从哈希对象中获取新的属性值,用代码如下表示:

    user.update(user_params)

    Rails 还进行了更加深入的集成。看看前面图像中的 .html.erb 文件,我们可以看见模板使用一组辅助方法创建了 HTML 表单,比如 form_for()text_field() 方法。

    在继续之前,params 不止用于非简单文本的场景。比如上传文件,我们会在接下来学习。

    Rails 应用中提交文件

    也许你的应用提供了提交文件的功能。比如,bug 报告系统需要用户提交日志文件及代码样例用于跟踪问题,博客网站用户也会为文章上传封面图片。

    在 HTTP 中,通过 multipart/form-data POST 方式上传文件。按名字理解需要通过表单生成此类信息。在表单中,你可以编写一至多个 type="file"<input> 标签。当浏览器渲染后,用户可以在此类型的输入框中选择上传文件。随后提交表单,被选择的文件将与表单中其他数据一同被传送至服务器。

    为了说明文件上传的流程,我们需要展示一段上传图片的代码,并且图片需要携带一段评论信息。要实现此功能,首先要创建 pictures 表存储数据。

    class CreatePictures < ActiveRecord::Migration
      def change
        create_table :pictures do |t|
          t.string :comment
          t.string :name
          t.string :content_type
          # If using MySQL, blobs default to 64k, so we have to give
          # an explicit size to extend them
          t.binary :data, :limit => 1.megabyte
        end
      end
    end
    

    同时还需要创建上传文件的 controller 说明流程。get action 是一种约定,它只是创建一个新的图片对象并渲染表单。

    # controllers/upload_controller.rb
    
    class UploadController < ApplicationController
      def get
        @picture = Picture.new
      end
      # . . .
    
      private
        # Never trust parameters from the scary internet, only allow the white
        # list through.
        def picture_params
          params.require(:picture).permit(:comment, :uploaded_picture)
        end
    end
    

    get 模板包含上传图片的表单(以及评论)。要注意我们是重写覆盖通过响应返回数据的字符类型的。

    <!-- views/upload/get.html.rb -->
    
    <%= form_for(:picture,
                 url: {action: 'save'},
                 html: {multipart: true}) do |form| %>
    
        Comment:             <%= form.text_field("comment") %><br>
        Upload your picture: <%= form.file_field("uploaded_picture") %><br>
    
        <%= submit_tag("Upload file") %>
    <% end %>
    

    表单除了上面代码外没有其他需要注意的地方。图片通过 updated_picture 参数上传。但是数据库并没有包含此字段。这表示在 model 中肯定使用了神奇的手法。

    # models/picture.rb
    
    class Picture < ActiveRecord::Base
    
      validates_format_of :content_type,
                          with: /^image/,
                          message: "must be a picture"
    
      def uploaded_picture=(picture_field)
        self.name         = base_part_of(picture_field.original_filename)
        self.content_type = picture_field.content_type.chomp
        self.data         = picture_field.read
      end
    
      def base_part_of(file_name)
        File.basename(file_name).gsub(/[^\w._-]/, '')
      end
    end
    

    我们定义了 uploaded_picture=() 方法接收表单上传的文件。由表单返回的对象是一个混杂的数据。它与文件类似,因此我们可以通过 read() 方法读取其中的内容,这也是我们需要从 data 字符读取数图片数据的原因。图片数据中拥有 content_typeoriginal_filename 属性,这都是上传文件附带的元数据。接收器方法将这些信息逐个分离,并组织为对象存储于数据库中。

    在 model 中我们还添加了一个文件类型验证,它将检验文件类型是否符合 image/xxx 形式。因为我们并不希望有 JavaScript 文件上传。

    controller 中的 save action 也是约定之一。

    # controllers/upload_controller.rb
    
    def save
      @picture = Picture.new(picture_params)
      if @picture.save
        redirect_to(action: 'show', id: @picture.id)
      else
        render(action: :get)
      end
    end
    

    此时我们已经在数据库中存储了一张图片,但我们要如何展示它呢?一种方式是提供图片的 URL 并使用图片标签链接图片。比如,我们可以通过 upload/picture/123 返回 ID 为 123 的图片。然后通过 send_data() 向浏览器传输图片。不过我们要设置文件名及文件类型,只有这样浏览器才可以解析数据,并在用户选择存储图片时提供默认文件名。

    # controllers/upload_controller.rb
    
    def picture
      @picture = Picture.find(params[:id])
      send_data(@picture.data,
                filename: @picture.name,
                type: @picture.content_type,
                disposition: "inline")
    end
    

    最后我们要实现 show action,通过它显示图片和评论。此 action 将直接加载图片 model 对象。

    # controllers/upload_controller.rb
    
    def show
      @picture = Picture.find(params[:id])
    end
    

    在模板中,图片标签将链接返回图片数据的 action。在下图中,我们可以看出 getshow action 是其中的核心。

    Uploading a file
    <!-- views/upload/show.html.erb -->
    
    <h3><%= @picture.comment %></h3>
    
    <img src="<%= url_for(:action => 'picture', :id => @picture.id) %>/>
    

    如果你想要通过更简单的方式上传和存储图片,可以了解一下 thoughtbot 的 Paperclip 或 Rick Olson 的 attachment_fu 插件。

    创建一个包含所需字段的数据库表(在 Rick 的网站上有说明),插件将自动管理上传的数据和元数据。不像我们之前的方式,它可以将上传在文件系统或数据库中的文件进行存储。

    表单和文件上传的例子中使用了 Rails 的辅助方法。接下来我们要展示如何自定义辅助方法,并且 Rails 也提供了一些辅助方法。

    使用辅助方法

    之前我们已经说过模板中是可以编写代码的。如今我们将修改声明。在模板中编写代码是大家都接受的,毕竟它可以完成动态内容的展示,但是在模板中增添过多代码便无法容忍。

    有这种情绪主要有三个原因。首先,在 view 端添加过多代码容易导致代码质量下滑,并且在模板中添加应用级的函数。这明显是一种不好的形式,因为我们都希望将应用级的方法存放在 controller 和 model 层,以便随处复用。当你换个角度看待应用时将更容易成功。

    第二个原因是 html.erb 是基于 HTML 的。当你编写此类模板时就相当于在编写 HTML 文件。甚至如果你拥有布局设计师时,他们更愿意像 HTML 一样处理模板。在模板中添加过多 Ruby 代码会让设计师的工作难以开展。

    最后一个原因是测试嵌入 view 中的代码困难重重,所以建议将代码单独隔离在辅助模块中,这将使单元测试更加容易开展。

    Rails 为表达辅助方法提供了良好的方案。辅助方法作为模块中的一个方法用于支持 view。辅助方法对于输出内容十分重要。继承于模板行为的辅助方法可以生成 HTML(或者 XML,或者 JavaScript)。

    自定义辅助方法

    每个 controller 默认可获取它们自身的辅助模块。而且,对于应用级的辅助模块会命名为 application_helper.rb。Rails 设置了规定帮助 controller 和 view 链接到相应的辅助模块。使所有的 view 辅助方法对所有的 controller 都可用是一种不错的实践。ProductController 关联的 view 的辅助方法按约定被命名为 ProductHelper,并且归置在 app/helpers/product_helper.rb 文件中。你不需要记得所有的细节,因为 rails generate controller 脚本会为你自动创建相应的辅助模块。

    在 149 页中,我们创建了 hidden_div_if() 辅助方法,它的功能是在指定情况下隐藏购物车。同样的技术也可以整理应用的布局。我们有如下代码:

    <h3><%= @page_title || "Pragmatic Store" %></h3>

    让我们将模块中的代码转移至辅助方法中。因为我们的目标是 store controller,所以需要在 app/helpers/store_helper.rb 中编写代码。

    module StoreHelper
      def page_title
        @page_title || "Pragmatic Store"
      end
    end
    

    然后在 view 中调用辅助方法。

    <h3><%= page_title %></h3>

    (如果还想消除更多的代码重复,我们可以将标题的渲染代码迁移至 partial 模板中,于是所有 controller 的 view 都可以共用,但我们在 363 页才讨论 partial 模板)。

    格式化及链接辅助方法

    Rails 已经提供了许多现成的辅助方法,并且对所有 view 都有效。在本节中我们希望你亲自尝试,并且在 Action View 的 RDoc 中你将了解到更多辅助方法。

    格式化辅助方法

    有些辅助方法是关于日期、数字和文本的。

    <%= distance_of_time_in_words(Time.now, Time.local(2013, 12, 25)) %> => 4 个月

    <%= distance_of_time_in_words(Time.now, Time.now + 33, include_seconds: false) %> => 1 分钟

    <%= distance_of_time_in_words(Tiem.now, Time.now + 33, include_seconds: true) %> => 半分钟

    <%= time_ago_in_words(Time.local(2012, 12, 25)) %> => 7 个月

    <%= number_to_currency(123.45) %> => $123.45

    <%= number_to_currency(234.56, unit: "CAN$", precision: 0) %> => CAN$235

    <%= number_to_human_size(123_456) %> => 120.6 KB

    <%= number_to_percentage(66.66666) %> => 66.667%

    <%= number_to_percentage(66.66666, precision: 1) %> => 66.7%

    <%= number_to_phone(2125551212) %> => 212-555-1212

    <%= number_to_phone(2125551212, area_code: true, delimiter: " ") %> => (212) 555 1212

    <%= number_with_delimiter(12345678) %> => 12,345,678

    <%= number_with_delimiter(12345678, delimiter: "_") %> => 12_345_678

    <%= number_with_precision(50.0/3, precision: 2) %> => 16.67

    debug() 方法会通过 YAML 格式显示辅助方法的参数,并展示用于 HTML 界面的计算结果。当想查找 model 对象数据或请求参数时这将有所帮助。

    <%= debug(params) %>

    还有一组处理文本的辅助方法。这些辅助方法可以清空字符串和强调字符串中的单词。

    <%= simple_format(@trees) %>

    格式化字符串,进行段落分隔。比如提供了 Joyce Kilmer 的诗 Trees 普通文本,此方法将按下述形式为文本添加 HTML 格式。

    <p> I think that I shall never see <br/>A poem lovely as a tree.</p><p>A tree whose hungry mouth in prest <br/>Against the sweet earth's flowing breast;</p>

    <%= excerpt(@trees, "lovely", 8) %> =>

    ...A poem lovely as a tre...

    <%= highlight(@trees, "tree") %> =>

    I think that, I shall never see A poem lovely as a <strong class="highlight">tree</strong>. A <strong class="highlight">tree</strong> whose hungry mouth is prest Against the sweet earth's flowing breast;

    truncate(@trees, length: 20) %> =>

    I think that I sh...

    下面的方法是使名词复数化的。

    <%= pluralize(1, "person") %> but <%= pluralize(2, "person") %> =>

    1 person but 2 people

    如果你想制作精良的网站,并能将 URL 和邮箱地址超链接化,辅助方法能够帮助到你。同样也可以将文本处理为超链接。

    回到 73 页,我们了解到 cycle() 方法可以会根据调用时的序列返回连续的数据,必要情况下可以重复使用此序列。我们通常使用此方法为表格和列表创建两种风格的行。current_cycle()reset_cycle() 也同样有效。

    最后,如果你已经开发了博客网站的一些功能,或你为商铺应用增加了评论功能,你可以使用户能够使用 Markdown(BlueCloth)或富文本(RedCloth)方式编写评论。这些都是简单的文字格式编辑器,它使编辑文字更加方便、更友好地添加格式,并将文字转化为 HTML 格式。

    链接至其他页面和资源

    ActionView::Helpers::AssetTagHelperActionView::Helpers::UrlHelper 模块包含一些链接当前模板额外资源的方法。通常我们都使用 link_to(),它将为应用中的其他 action 创建超链接。

    <%= link_to "Add Comment", new_comments_path %>

    link_to() 第一个参数用于显示链接文字,接下来的参数是字符串或哈希值,主要用于指定链接目标。

    第三个参数是用于生成链接的 HTML 属性。

    <%= link_to "Delete", product_path(@product), {class: "dangerous", method: 'delete'} %>

    第三个参数支持额外两个改变链接行为的参数。浏览器中的每个元素都可以应用 JavaScript。

    :method 参数是一个后门,它允许你创建应用中链接,就像由 POST、PUT、PATCH 或 DELETE 创建的请求。当链接被点击时创建提交请求的 JavaScript 将被执行,如果浏览器不支持 JavaScript 将使用 GET 请求。

    :data 参数用于设置自定义属性。最常用的是 :confirm 参数,它将显示一个短信息。如果使用此参数 JavaSript 驱动将显示此信息,并且在链接跳转前获取用户的确认。

    <%= link_to "Delete", product_path(@product), method: :delete, data: { confirm: 'Are you sure?' } %>

    button_to()link_to() 的功能相似,差别在于为表单生成一个按钮而不是超链接。链接至有副作用的 action 时更愿意使用此方法。但是这些按钮都存在于自身的表单中,同时也会引入一些限制,它们无法使用内联显示,也不能出现在其他表单内部。

    Rails 也提供条件化的链接方法,它的功能是如果条件符合就生成超链接,否则就返回链接文本。link_to_if()link_to_unless() 都可以接收条件参数,其他的参数与 link_to() 的一致。如果条件是 true (对于 link_to_if)或者 false(对于 link_to_unless)将使用余下的参数生成超链接,否则链接名称将以普通文本形式显示(无超链接效果)。

    link_to_unless_current() 可以在侧边栏创建菜单,并且将当前页面名字显示为普通文本,其他页面菜单项显示为超链接。

    <ul>
    <% %w{ create list edit save logout }.each do |action| %>
      <li>
        <%= link_to_unless_current(action.capitalize, action: action) %>
      </li>
    <% end %>
    </ul>
    

    link_to_unless_current() 也可以通过 block 判断提供的 action 是否为当前 action,可以高效地为链接提供变化。current_page() 也是一个辅助方法,它可以检测当前的 URI 与通过参数生成的地址是否一致。

    link_tourl_for 一样都支持绝对 URL。

    <%= link_to("Help", "http://my.site/help/index.html") %>

    image_tag() 能创建 <img> 标签。:size 参数(也就是表单的宽乘以高)或者分别用宽和高都可以定义图片的大小。

    <%= image_tag("/assets/dave.png", class: "bevel", size: "80x120") %>
    <%= iamge_tag("/assets/andy.png", class: "bevel", width: "80", height: "120") %>
    

    如果你没有使用 :alt 参数,Rails 将会合成图片文件名。如果文件不是以斜杠字符开头,Rails 默认它位于 app/assets/images 路径中。

    结合 link_to()image_tag() 可以生成带超链接的图片。

    <%= link_to(image_tag("delete.png", size: "50x20"),
                product_path(@product),
                data: { confirm: "Are you sure?" },
                method: :delete)
    %>
    

    mail_to() 会创建 mailto: 超链接,当点击此链接时会加载用户的邮箱应用。它会将邮箱地址作为链接名字,然后是一组 HTML 参数。在参数中也可以使用 :bcc:cc:body:subject 初始化邮件相应的字段。最后神秘参数 encode: "javascript" 可以掩藏客户端的链接,避免网络蜘蛛从你的网站中获取邮箱地址。不过它也就意味着如果不在浏览器中禁掉 JavaScript 用户将无法看到邮箱链接。

    <%= mail_to("support@pargprog.com", "Contact Support",
                subject: "Support question from #{@user.name}",
                encode: "javascript") %>`
    

    虽然这是表单的弱项,但你可以通过 :replace_at:replace_dot 替换艾特特号和名字中的点号。它对于愚蠢的邮箱地址收集器已经足够了。

    AssetTagHelper 模块也包含引用样式表和 JavaScript 代码的方法,并且可以创建自动发现的 Atom 订阅链接。我们在 Depot 的布局中创建了链接,分别在 <head> 中使用了 stylesheet_link_tag()javascript_link_tag() 方法。

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

    javascript_include_tag() 方法可接收一组 JavaScript 文件名(假设保存在 assets/javascripts 路径中),并且在创建的 HTML 中加载这些文件。对于 :all javascript_include_tag 使用 :default 作为参数值,Rails 便可以快捷地加载 jQuery.js。

    RSS 或 Atom 链接也在 <head> 中指向应用的 URL。当 URL 被访问时,应用应该返回相应的 RSS 或 Atom XML。

    <head>
      <%= auto_discovery_link_tag(:atom, products_url(format: 'atom')) %>
    </head>
    

    最后 JavaScriptHelper 模块还定义了一些辅助 JavaScript 的方法。它们可以生成运行浏览器的 JavaScript 片段,既能产生动态效果也可以与应用完成交互。

    默认情况下,图像与样式表资源分别在 assets 文件夹的 images 路径和 stylesheets 路径下。如果静态资源引用的标签是以斜杠开头,表示使用绝对路径。有时静态资源也会被转移出应用外或者转移至应用中的其他地方。可以配置 asset_host 修改静态资源基准路径:

    config.action_controller.asset_host = "http://media.my.url/assets"

    尽管辅助方法涉及广泛,但 Rails 已经提供了许多,到具体模块我们还会介绍相应的辅助方法,不过还是有一些已经失效或者转移到与 Rails 不同步的插件中。现在是个回顾在 256 页生成的在线文件的好时机,再看看 Rails 还提供哪些辅助方法。

    使用布局和 Partial 减少维护

    本章中我们已经学习过将一堆代码隔离后的模板。但在 Rails 后台中还有一个按 DRY 思想提出的方案,它的目标也是消除重复。尽管是常规的网站但依然存在许多重复。

    • 许多页面都共享头、尾和侧边栏。

    • 多个页面可以包含相同的 HTML 渲染片段(比如博客网站中的需要在多个地方展示文章)。

    • 同一个功能可能出现在多个地方。许多网站都有标准搜索组件,或者轮询组件。

    Rails 提供布局和 partial 降低在三种情形中的重复。

    布局

    Rails 可以渲染内嵌了其他页面的页面。根据这个特性我们可以将 action 的输出内容显示在一个全局的页面框架中(框架包括标题、脚注和侧边栏)。实际上,如果你已经使用脚手架 generate 生成的应用,初始时就可以使用布局。

    当 Rails 接收请求并需要渲染模板时实际上是渲染了两个模板。很明显,其中一个是你所要求渲染的(如果没有明确需要渲染的内容默认就是与 action 名字相应的模板)。除此之外 Rails 还需要渲染布局模板(下面我们会讨论它是如何找到布局模板的)。当它找到布局模板时便会将 action 输出的内容插入由布局模板生成的 HTML 中。

    接着看看布局模板:

    <html>
      <head>
        <title>Form: <%= controller.action_name %></title>
        <%= stylesheet_link_tag 'scaffold' %>
      </head>
      <body>
        <%= yield :layout %>
      </body>
    </html>
    

    布局模板会返回标准的 HTML 页面,其中包含 <head><body> 部分。它将当前 action 的名称作为页面标题并引入 CSS 文件。在 <body> 中调用了 yield,这就是神奇的地方。当 action 的模板被渲染时,Rails 会将内容存储起来并标记为 :layout。在布局模板中,调用 yield 会获取文本。实际上,:layout 是在渲染时返回的默认内容,所以可以使用 yield 替代 yield :layout。不过我们个人更喜欢明确一些的版本。

    如果 my_action.html.erb 模板包含下列内容:

    <h1><%= @msg %></h1>

    浏览器中可以看到下列 HTML:

    <html>
      <head>
        <title>Form: my_action</title>
        <link href="/stylesheet/scaffold.css" media="screen"
              rel="Stylesheet" type="text/css" />
      </head>
      <body>
        <h1>Hello, World!</h1>
      </body>
    </html>
    

    定位布局文件

    也许你已经开始期待,Rails 已经为布局文件定义了默认路径,但如果你需要修改也可以重新配置。

    布局文件是按 controller 区分的。如果当前请求由 store controller 处理,Rails 默认会在 app/views/layouts 中查找名为 store 的布局文件(扩展名为 .html.erb.xml.builder)。如果在 layouts 文件夹中创建了布局文件 application,它将影响所有没有定义布局的 controller。

    你可以在 controller 中使用 layout 声明重新定义。最简单的情况是向声明提供布局名称。下面的声明将为 store controller 中的 action 应用 standard.html.erbstandard.xml.builder 模板。在 app/views/layouts 路径下可找到布局文件。

    class StoreController < ApplicationController
      layout "standard"
      # ...
    end
    

    通过 :only:except 指定哪些 action 可应用布局文件。

    class StoreController < ApplicationController
      layout "standard", except: [ :rss, :atom ]
      # ...
    end
    

    对 layout 声明设置为 nil 将关闭 controller 的布局。

    有时我们想同时改变一组页面的显示效果。比如,博客网站在用户登录后需要显示不同观感的侧边栏菜单,或者在网站下线维护时需要使用另外的页面显示。Rails 利用动态布局满足此功能。如果 layout 声明的参数是标识,它表示 controller 中同名的实例方法将返回布局文件名字。

    class StoreController < ApplicationController
      layout :determine_layout
    
      #...
      private
      def determine_layout
        if Store.is_closed?
          "store_down"
        else
          "standard"
        end
      end
    end
    

    如果直接使用 layout 重新设置布局文件,controller 的子类也继承使用父类的此声明。最后,个别 action 会通过设置 render() 方法 :layout 参数指定需要的布局文件(或者不使用布局)。

    def rss
      render(layout: false) # never use a layout
    end
    
    def checkout
      render(layout: "layout/simple")
    end
    

    向布局文件传递参数

    布局模板也可以访问普通模板能够使用的数据。而且普通模板中的实例变量也可以在布局模板中使用(因为常规模板是在布局模板被调用前渲染)。这些信息都可以在布局模板中用于参数化头或菜单内容。比如,布局文件中包含下列内容:

    <html>
      <head>
        <title><%= @title %></title>
        <%= stylesheet_link_tag 'scaffold' %>
      </head>
      <body>
        <h1><%= @title %></h1>
        <%= yield :layout %>
      </body>
    </html>
    

    普通模板可以通过赋值给 @title 设置标题。

    <% @title = "My Wonderful Life" %>
    <p>
      Dear Diary:
    </p>
    <p>
      Yesterday I had pizza for dinner. It was nice.
    </p>
    

    实际上我们可以更长远地考虑一下。使用 yield :layout 将模板内嵌至布局中让你在模板中可以生成任意内容,也就是接着还可以内嵌至其他模板中。

    比如,不同的模板需要将自己的特异化信息添加至标准化页面的侧边栏中。在这些模板中可以使用 content_for 定义内容,然后通过 yield 在布局模板中将这些内容内嵌至侧边栏。

    在每个常规模板中,向 content_for 提供一个在 block 中渲染的内容的名称。这段内容将存储于 Rails 中,并且还会构成由模板生成的输出结果。

    <h1>Regular Template</h1>
    
    <% content_for(:sidebar) do %>
      <ul>
        <li>this text will be rendered</li>
        <li and saved for later</li>
        <li>it may contain <%= "dynamic" %> stuff</li>
      </ul>
    <% end %>
    
    <p>
      Here's the regular stuff that will appear on
      the page rendered by this template.
    </p>
    

    接着在布局模板中通过 yield :sidebar 向页面的侧边栏引入 block 中的内容。

    <!DOCTYPE ... >
    <html>
      <body>
        <div class="sidebar">
          <p>
            Regular sidebar stuff
          </p>
          <div class="page-specific-sidebar">
            <%= yield :sidebar %>
          </div>
        </div>
      </body>
    </html>
    

    同样的技术也可以用来向布局文件的 <head> 部分添加 JavaScript 方法,创建指定菜单栏等等。

    Partial 页面模板

    web 应用通常用于展示应用对象的信息或者在多个页面中展示对象。购物车可能在购物车页面显示购买商品,也可能在订单汇总界面显示购买商品。博客应用会在主页显示文章内容,以及在页面顶部征集评论时显示。也就是说在不同的模板中可能包含重复的代码片段。

    不过 Rails 为了消除这些重复代码使用了 partial 页面模板(简称 partials)。你可以将 partial 认为是一个子程序。在模板中可以多次调用它,不过需要将对象传送给它让其渲染。当 partial 模板完成渲染时,控制权将回到发起调用的模板手中。

    在 partial 模板内部看起来与其他模板没有什么区别。从外部来看还是有所不同。模板文件的名称必须以下划线开头,这种命名方式将 partial 模板与其他模板区别开来。

    例如,渲染博客内容的 partial 可能编写在 app/views/blog 的 _article.html.erb 文件中。

    <div class="article">
      <div class="articleheader">
        <h3><%= article.title %></h3>
      </div>
      <div class="articlebody">
        <%= article.body %>
      </div>
    </div>
    

    其他模板中通过 render(partial:) 调用。

    <%= render(partial: "article", object: @an_article) %>
    <h3>Add Comment</h3>
    ...
    

    render():partial 参数是被渲染的模板名称(不过不需要在开头使用下划线)。这个名称必须是有效的文件名以及有效的 Ruby 分隔符(所以 a-b20042501 并不是有效的 partial 名字)。:object 参数可以定义传入 partial 模板中的对象。被传入的对象在 partial 模板中作为局部变量使用,并且对象名与模板名一致。在本例中,@an_article 对象将被传送给 partial 模板,而 partial 模板通过本地变量 article 访问它。这就是我们可以在 partial 模板中编写 article.title 的原因。

    通过向 render() 传递 :locals 参数可以传递其他局部变量。变量使用哈希形式表达一组变量名和变量值的键值对。

    render(partial: 'article',
           object: @an_article,
           locals: { authorized_by: session[:user_name]
                     from_ip: request.remote_ip})
    

    Partial 和集合

    应用常常需要显示格式化后的集合。博客需要显示一系列文章,以及其中的内容、作者、日期等等。商城需要显示每条分类的信息,包括图片、描述和价格等。

    render() 中的 :collection 参数要与 :partial 参数结合使用。:partial 参数可以定义格式化每一条数据的 partial,:collection 参数会对集合中的每个元素使用此模板。

    如果要使用前面我们定义的 _article.html.erb 显示一组文章,我们可以如下编写:

    <%= render(partial: "article", collection: @article_list) %>
    

    在 partial 内部,article 变量将是集合中的当前文章,变量是在模板之后被命名的。而且 article_counter 变量表示当前文章在集合中的下标。

    :spacer_template 参数可指定在每个集合元素间渲染的模板。比如,view 可能包含下列内容:

    <%= render(partial: "animal"
               collection: %w{ ant bee cat dog elk },
               spacer_template: "spacer")
    %>
    

    在提供的列表中使用 _animal.html.erb 渲染每个动物,在每个动物中使用 _spacer.html.erb 渲染。如果 _animal.html.erb 包含下列内容:

    <p>The animal is <%= animal %><p>
    

    并且 _spacer.html.erb 包含下列内容:

    <hr/>
    

    你会在显示动物列表时每个动物之间都存在一条线。

    分享模板

    如果第一个参数或 :partial 参数是使用简单名字渲染,Rails 会认为目标模板在当前 controller 的 view 文件夹中。但是,如果名字中包含一至多个斜杠符,Rails 会认为最后的斜杠前的部分是文件夹名称,剩下的是模板名称。文件夹默认在 app/views 路径下。这种方式方便跨 controller 共享 partial 和子模板。

    Rails 中约定将共享 partial 存放于 app/views/shared 路径中。使用下面的声明共享 partial:

    <%= render("shared/header", locals: {title: @article.title) %>
    <%= render(partial: "shared/post", object: @article) %>
    ...
    

    在前面的例子中,@article 声明为模板中的 post 局部变量。

    布局的 Partial

    布局中的 partial 也可以被渲染,你可以在任何模板中通过 block 的方式应用布局模板。

    <%= render partial: "user, layout: "administrator" %>
    
    <%= render layout: "administrator do %>
      #...
    <% end %>
    

    partial 布局可以直接在 app/views 相应的 controller 路径下找到,并带有下划线前缀,比如 app/views/users/_administrator.html.erb。

    Partial 和 Controller

    使用 partial 不止是作为 view 模板。controller 也可以对其操作。partial 给 controller 提供了一项支持,它可以通过使用相同 partial 模板的界面生成片段。当你使用 Ajax 通过 controller 更新页面中的部分内容时这格外重要,你能够知道表格行或者更新用于适配相邻行的数据的格式。

    综合来看,partial 和布局提供了更有效率的方式保证应用的接口部分是可维护的。但可维护性只是其中一个特性,还要在使用这种方式时能够良好运行。

    总结

    view 是 Rails 应用的外观,而且 Rails 对构建健壮、可维护的接口提供了大量的支持。

    我们从学习模板开始,Rails 支持四种类型的模板,分别是 ERB、Builder、CoffeeScript 和 SCSS。模板使我们返回 HTML、XML、CSS 和 JavaScript 更加容易。我们可以在 413 页讨论其他部分。

    然后是表单,它是用户与应用交互的主要方式。同时还学习了上传文件。

    接下来是辅助方法,它将复杂的逻辑隔离,使 view 专注于表现方面的事情。同时我们也研究了一些 Rails 提供了辅助方法,其中一些是格式化超链接文本的,这是用户与 HTML 页面交互的最终方式。

    我们最后学习了通过两种方式重构模板中的内容,使其得到重用。通过布局提取 view 的外部层级,并提供了统一的风格和观感。通过 partial 提取共用的内部组件,比如表单或表格。

    同时也了解了用户如果通过浏览器访问我们的 Rails 应用。接下来,要学习如何定义和维护存储数据的数据库 schema。


    本文翻译自《Agile Web Development with Rails 4》,目的为学习所用,如有转载请注明出处。

    相关文章

      网友评论

          本文标题:Action View

          本文链接:https://www.haomeiwen.com/subject/sipotltx.html