美文网首页
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