我们已经学习过路由组件如何操作 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 对象也有 name
、country
和 password
这些属性。模板可以通过辅助方法构建 HTML 表单,使用户能够编辑 model 中的数据。要注意表单字段的命名方式。比如,country
属性匹配 HTML 中命名为 user[country]
的输入域。
当用户提交表单时,原生的 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_type
和 original_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。在下图中,我们可以看出 get
和 show
action 是其中的核心。
<!-- 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::AssetTagHelper
和 ActionView::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_to
与 url_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.erb
或 standard.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-b
和 20042501
并不是有效的 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》,目的为学习所用,如有转载请注明出处。
网友评论