Task C Catalog Display
总而言之,之前确实是一次成功的迭代。我们已经收集了客户的需求,绘制了基本流程,也实现了我们优先需要的数据,并且将 Depot 应用商品的维护界面整合了起来。这些代码也已经有一定数量了。我们甚至还有一些虽然小型但依然不断增长的测试套件。
所以对于我们接下来的任务,我们更加地有信心了。当我们在谈论客户需求优先级的时候,她说她希望可以优先看看购买者在应用中如何操作。那下一步我们将创建一个基本的分类显示界面。
从我们的角度出发这个任务也是情理之中。一旦我们将商品完全地存放于数据库后,就要简洁地展示它。这个任务也将会给我们提供后续开发购物车的基础。
而且我们还可以借鉴之前商品维护任务的工作,毕竟分类展示也就是美化后的商品列表。
最后,我们也需要通过一些基本的 controller 测试完善 model 的单元测试。
迭代 C1:创建分类列表
我们已经创建了 products controller,现在它由管理 Depot 应用的售货员使用。也是时候创建第二个 controller 了,它将由消费者使用。那我们就叫它 Store 吧。
rails generate controller Store index
就像上一章一样,我们也是通过 generate 工具创建了一个 controller 并且并联了商品管理员的脚手架,现在我们只是创建了 controller 而已(StoreController 类只包含一个操作方法,就是 index()
)。
当万事俱备时就可以通过启动应用,并通过 http://localhost:3000/store/index 进行相应的操作了,不过我们还可以做到更好。让我们简化一下用户的操作,将它作为整个网站的根 URL。我们可以编辑 config/routes.rb 达到目的。
Rails.application.routes.draw do
get 'store/index'
resources :products
# The priority is based upon order of creation: first created -> highest priority.
# See how all your routes lay out with "rake routes".
# You can have the root of your site routed with "root"
root 'store#index', as: 'store'
#...
end
在这个文件的顶部,你可以看见有几行代码,它们是用来支持 store 和 products controller 的功能的。我们要另起一行。我们在文件的注释外定义了网站的根。可以将相应行去除注释,也可以在注释外添加。我们在这行修改的只是 controller 的名字(从 welcome 修改为 store),像 as:'store'
这样写。稍后 Rails 就会被命令创建 store_path
访问方法。我们在之前的第 26 页也见到过。
让我们尝试一下。在浏览器输入 http://localhost:3000/,就会显示我们的网站界面。
template-not-found.png界面可能还不够丰富,不过我们至少知道所有的东西都可以直接访问到。界面已经告诉了我们要在哪些找到绘制这个界面的模板。
让我们由展示数据库中简单的商品列表开始。我们知道最终界面都要变得复杂,我们要将商品进行分类,不过基础的商品列表可以保证我们持续下去。
我们需要获取数据库中的商品列表,并且将它整理为可以显示在表格中的代码。这意味着必须修改 store_controller.rb 中的 index()
方法。我们希望以适当的抽象水平进行编程,所以让我们通过 model 获取我们可以售卖的商品列表。
class StoreController < ApplicationController
def index
@products = Product.order(:title)
end
end
我们问客户她是否有商品列表排序的依据,我们好确定展示商品列表时如何排序。我们是通过调用 Product model 的 order(:title)
方法实现的。
现在我们需要编写 view 模板了。要做这件事,就要编辑 app/views/store 中的 index.html.erb 文件。(要记住 view 路径名称的组成是由 controller 名字和 action 的名字构成的。.html.erb 表示它是 ERB 模板,并且最终生成 HTML 结果)。
<% if notice %>
<p id="notice"><%= notice %></p>
<% end %>
<h1>Your Pragmatic Catalog</h1>
<% @products.each do |product| %>
<div class="entry">
<%= image_tag(product.image_url) %>
<h3><%= product.title %></h3>
<%= sanitize(product.description) %>
<div class="price_line">
<span class="price"><%= product.price %></span>
</div>
</div>
<% end %>
要注意对描述使用的 sanitize()
方法。它允许我们安全地添加 HTML 风格,使描述对客户来说更加有趣。需要注意的是,这个决定可能会开启一个潜在的安全漏洞,不过由于商品描述是由为我们公司工作的员工维护,我们认为风险是比较小的。
我们也使用了 image_tag()
辅助方法。这将生成一个 <img>
标签并将参数作为图片源使用。
接着我们添加了一个样式表,我们如同迭代 A2 一样使用,StoreController 创建的界面的 HTML 类名都为 store。
// Place all the styles related to the Store controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
.store {
h1 {
margin: 0;
padding-bottom: 0.5em;
font: 150% sans-serif;
color: #226;
border-bottom: 3px dotted #77d;
}
/* An entry in the store catalog */
.entry {
overflow: auto;
margin-top: 1em;
border-bottom: 1px dotted #77d;
min-height: 100px;
img {
width: 80px;
margin-right: 5px;
margin-bottom: 5px;
position: absolute;
}
h3 {
font-size: 120%;
font-family: sans-serif;
margin-left: 100px;
margin-top: 0;
margin-bottom: 2px;
color: #227;
}
p, div.price_line {
margin-left: 100px;
margin-top: 0.5em;
margin-bottom: 0.8em;
}
.price {
color: #44a;
font-weight: bold;
margin-right: 3em;
}
}
}
点击刷新将会给我们展示如下图的界面。看起来还不错,不过好像缺少了些什么。当我们思考的时候,客户恰巧经过,她指出她想要看到一个得体的条幅以及面向公众的侧边栏。
our-first-catalog-page.png就像现实世界一样,我们可能要咨询设计师了,我们已经看过太多由程序员设计的网站让人感到不舒服了。但网站设计师还在海滩休假并且明年才会回来,所以现在让我们先放置一个占位符,到时再用另一个迭代处理。
迭代 C2:添加页面布局
一般同一个网站中的界面都会使用相同的布局,设计师可能会创建一个标准的模板通过替换内容进行使用。我们的下一步工作就是要修改界面,并向每一个 store 页面添加装饰。
直到现在,我们也只是给 application.html.erb 添加了少量的修改,类似迭代 A2 一样添加了相应的类。布局文件本身是被使用在所有 controller 的所有 view 上的,我们只要修改一个文件就可以改变整个网站的所观所感。现在我们能够提供一个布局界面会很棒,当设计师从岛上回来时我们可以继续更新它。
让我们修改布局文件,给它添加一个横幅和侧边栏。
<!DOCTYPE html>
<html>
<head>
<title>Depot</title>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
<%= csrf_meta_tags %>
</head>
<body class='<%= controller.controller_name %>'>
<div id="banner">
<%= image_tag("logo.png") %>
<%= @page_title || "Pragmatic Bookshelf" %>
</div>
<div id="columns">
<div id="side">
<ul>
<li><a href="http://www....">Home</a></li>
<li><a href="http://www..../faq">Questions</a></li>
<li><a href="http://www..../news">News</a></li>
<li><a href="http://www..../contact">Contact</a></li>
</ul>
<div id="date_time"><%= Time.now.strftime("%Y-%m-%d %H:%M:%S") %></div>
</div>
<div id="main">
<%= yield %>
</div>
</div>
</body>
</html>
与通常的 HTML 组件不同,布局文件有三个 Rails 条目。第 5 行使用了 Rails 的 stylesheet_link_tag()
辅助方法生成关于应用的样式表和开启的 turbolink 相关的 <link>
标签,这些都是在提升应用中界面修改的工作。相似的还有第 7 行,生成了应用脚本的 <link>
。
最后,第 8 行装配了防止跨域伪装攻击的幕后数据,这对于我们在 12 章添加表单是十分重要的。
在第 13 行,我们使用实例变量 @page_title
的值设置为页面顶部的值。不过真正神奇的地方是第 25 行。当我们调用 yield
时,Rails 自动将指定界面的内容进行了替换,指定界面是来自于由 view 请求返回的内容生成的结果。此时,这里将是 index.html.erb 生成的分类商品页面。
要让所有的东西都正常运转首先要重命名 application.css 为 application.css.scss。如果你没有选择像自习天地中一样操作 Git 的话,现在该是这样做的时候了。通过 Git 命令 git mv
重命名文件。一旦你已经重命名了文件,无论你是通过 Git 还是操作系统重命名的,都将下面的代码添加到文件中:
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any styles
* defined in the other CSS/SCSS files in this directory. It is generally better to create a new
* file per style scope.
*
*= require_tree .
*= require_self
*/
#banner {
background: #9c9;
padding: 10px;
border-bottom: 2px solid;
font: small-caps 40px/40px "Times New Roman", serif;
color: #282;
text-align: center;
img {
float: left;
width: 90px;
height: 45px;
}
}
#notice {
color: #000 !important;
border: 2px solid red;
padding: 1em;
margin-bottom: 2em;
background-color: #f0f0f0;
font: bold smaller sans-serif;
}
#columns {
background: #141;
#main {
margin-left: 17em;
padding: 1em;
background: white;
}
#side {
float: left;
padding: 1em 2em;
width: 13em;
background: #141;
ul {
padding: 0;
li {
list-style: none;
a {
color: #bfb;
font-size: small;
}
}
}
#date_time {
color: white;
font-weight: bold;
}
}
}
就像注释中解释的那样,清单文件将自动包含所有此文件夹及子文件夹中的样式表。这都是通过 require_tree
直接完成的。
我们可以替换在 stylesheet_link_tag()
中关联的独立的样式表,但因为我们是在操作整个应用的布局文件,并且布局文件也已经加载了所有样式表,我们就先不要管它。
这个页面设计是由三个区域组成,一个是顶部的横幅,一个是位于右下方的主区域,还有一个位于左侧的侧边栏区域。而且还有一些相应的注意事项。它们会使用通常在 CSS 中看见的 margin,padding,fonts 和 colors。横幅需要居中,左侧栏暂时用图片替换。在侧边栏区域内,有一个特殊样式的列表,它去除了填充,并且使用不同的字体和颜色。
再说明一下,我们使用 Sass 的比重很大,我们只有修改文件名才可以这样做。例如,在 #banner
选择器内有一个 img
选择器,在 #side
中也有一个选择器。
点击刷新,浏览器看到的应该如下图。它不太可能获取任何设计奖项,不过它已经可以向客户大致展示最终的界面会是什么样子。
Catalog with layout added看到这个页面,我们发现了一个小问题,价格应该怎么展示。数据库是将价格作为数字存储,不过我们希望将它展示为美元和美分。12.34 这个价格应该展示为 13.00。我们下一步将解决这个问题。
迭代 C3:使用辅助方法格式化价格
Ruby 提供了 sprintf()
方法用来格式化价格。我们可以直接在 view 中用这个函数替换逻辑。例如,我们可以看看下面的内容:
<span class="price"><%= sprintf("$%0.02f", product.price) %></span>
这样就能正常显示,但它将货币的知识嵌入了 view 中。我们需要在多个地方显示商品价格,甚至后面还会进行国际化,这些都会产生维护的问题。
转换一下,我们通过一个辅助方法将价格作为货币格式化。Rails 有一个已经具备的适当方法,它叫做 number_to_currency()
。
在 view 中使用这个辅助方法十分简单,在 index 模板中我们要修改下面这行代码:
<span class="price"><%= product.price %></span>
修改为下面这样:
<span class="price"><%= number_to_currency(product.price, unit: "¥") %></span>
就是这样,我们再点击刷新,我们可以看到格式化后的价格很棒,就像下图中的一样。
catalog-with-price-formatted.png尽管页面看起来已经不错了,我们还是要挑剔一下,我们确实应该为这些新功能编写和运行一下测试,特别是我们向 model 添加了逻辑后。
迭代 C4:Controller 的基本测试
现在是关于真实的环节。在我们专注于编写新测试之前,我们需要确定我们是否破坏了什么。我们向 model 添加校验逻辑的事情还历历在目,现在运行测试时我们还有些担忧。
rake test
还不错。我们添加了不少的东西,所幸没有破坏任何东西。这对于我们来说是个安慰,毕竟我们还有工作没有完成,我们还需要测试新添加的功能。
之前我们给 model 做的单元测试比较直接。我们调用一个方法,并比较方法返回的结果是否如同我们的期望一样。但现在我们处理一个向服务器发起的请求流程,用户是在浏览器查看响应的。我们此时需要功能测试验证 model,view 和 controller 都正常运转。不需要担心,Rails 会让一切都轻松搞定。
首先,让我们看看 Rails 为我们生成了什么。
require 'test_helper'
class StoreControllerTest < ActionController::TestCase
test "should get index" do
get :index
assert_response :success
end
end
should get index
请求了 index,并且期望是一个成功的响应。这样看起来确实挺直接的。这个起点还挺合理的,不过我们希望验证能够包含布局,商品信息和数字格式化。让我们继续看看在代码里面要怎么处理。
require 'test_helper'
class StoreControllerTest < ActionController::TestCase
test "should get index" do
get :index
assert_response :success
assert_select '#columns #side a', minimum: 4
assert_select '#main .entry', 3
assert_select 'h3', 'Programming Ruby 1.9'
assert_select '.price', /\$[,\d]+\.\d\d/
end
end
我们添加的 4 行代码是查看返回的 HTML 内容,并且使用了 CSS 选择器。需要复习一下,选择器以 #
号开始表示的是 id 属性,以 .
号开始是匹配 class 属性,如果没有前缀时匹配相应的元素名。
因此,第一个选择器测试的是查找 id 为 columns 的元素中的,id 为 side 的元素中的 a 标签元素。并且验证其中小于 4 个元素。assert_select()
方法确实是个有力的工具吧。
下面三行代码验证了商品要展示的所有信息。首先验证了页面主体中 class 为 entry 的元素中有三个元素。下一行验证了 h3 元素中是我们之前存入的 Ruby 书籍的标题。第三行验证了价格是被正确格式化的。这些断言都是基于我们之前放置于夹具中的测试数据的。
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
title: MyString
description: MyText
image_url: MyString
price: 9.99
two:
title: MyString
description: MyText
image_url: MyString
price: 9.99
ruby:
title: Programming Ruby 1.9
description:
Ruby is the fastest growing and most exciting dynamic
language out there. If you need to get working programs
delivered fast, you should add Ruby to your toolbox.
image_url: ruby.png
price: 49.50
如果你细心的话,你应该会注意到 assert_select
判断的依据主要是基于第二个参数。如果是一个数字参数,它将被视作一个数量。如果是一个字符串,它将被视为一个期望的结果。还有正则表达式也是有效类型,我们在最后一个断言中使用了它。我们验证价格是依次包含美元符号,接着是任意数字(但最少是一个),逗号或数字,接着是小数点,最后是两位数字。
在我们断续之前的最后一点是,无论是验证还是功能测试都只是测试 controller 的行为,它们并不能追溯到任何已经存在数据库或者夹具的对象的影响。在前一个例子中,两个商品包含着相同的标题。这些数据并没有引起问题,一些已经被修改和被保存的记录并没有被发现。
我们只演示了 assert_select()
的几个功能。更多的信息可以查看在线文档。
这么多的验证也只用了几行代码。我们可以看见功能测试的运行(毕竟我们已经修改了所有东西)。
rake test:controllers
现在,我们已经制作了一些可以辨认的界面作为门店,我们也通过测试确认了 model,view 和 controller 的代码能够正常运转且产生有意义的结果。尽管已经听过太多次,但我还是要说 Rails 让其更加轻松。实际上,编写比较多的还是 HTML 和 CSS 代码,而不是业务代码或测试代码。在我们继续之前,让我们确认应用可以承受用户的热烈使用。
迭代 C5:缓存部分结果
如果每件事情都如同计划一样进行,页面绝对将是网站的高流量区域。为了响应页面的请求,我们从数据库获取所有商品并且渲染它们。我们可以做得更好。毕竟,分类并不会频繁变化,所以不需要每次请求都重新处理。
就因为如此,我们要看看我们都要做什么,首先,我们要修改开发环境的配置让其能返回缓存。我们要修改开发环境的配置让其能返回缓存。
config.action_controller.perform_caching = true
修改完这个配置后,你需要重启服务。
接着,我们计划一下我们要怎么进行处理。想一想,我们只需要在商品更新时才重新渲染,而且我们也只需要重新渲染被更新的商品即可。要关注的前一部分的问题是,我们需要添加代码用以返回最近修改的商品。
# app/models/product.rb
def self.latest
Product.order(:updated_at).last
end
接着在商品发生变动时我们需要在更新的模板中进行标记,而且在标记的模块中还需要标记子模块,因为我们需要更新发生变动的商品的详情。
<% if notice %>
<p id="notice"><%= notice %></p>
<% end %>
<h1>Your Pragmatic Catalog</h1>
<% cache ['store', Product.latest] do %>
<% @products.each do |product| %>
<% cache ['entry', product] do %>
<div class="entry">
<%= image_tag(product.image_url) %>
<h3><%= product.title %></h3>
<%= sanitize(product.description) %>
<div class="price_line">
<span class="price"><%= number_to_currency(product.price, unit: "¥") %></span>
</div>
</div>
<% end %>
<% end %>
<% end %>
并且在中括号部分,我们通过每一个缓存条目的名字区分了组件。我们选择将所有的缓存条目都命名为 store,并将每个缓存条目命名为 entry。我们也将单个商品与每个商品关联,也就是说,最新的商品与整个 store 相关联,独立的商品与 entry 相关联。
括号内的部分可以内嵌任意深度,这也是 Rails 社区将它称为俄罗斯套娃式缓存的原因。
我们已经完成了自己的部分。剩下的都是 Rails 需要处理的,包括管理存储和决定旧数据失效的时间。如果你对缓存十分感兴趣,它还有很多的机关让你选择你可以将哪个存储用于缓存。现在你不用再担心任何事情了,而且《Caching with Rails in the RailsGuides》更有收藏价值。
至于要如何验证缓存的工作,不太幸运的是还没有太多的方法。如果你回到页面,你应该看不到有任何的变动,实际上也是这样的。最好的方式是,你可以修改模板中缓存部分内的任意内容,但不要变动商品,并且验证你没有看到任何变动,因为页面的缓存版本并没有发生变化。
一旦你已经对缓存的处理结果感到满意时,你需要在开发环境下关闭缓存,避免模板的修改不能立即显现。
config.action_controller.perform_caching = false
再强调一次,此时你需要重启服务,快速保存模板以验证模板的缓存已经关闭。
小结
我们将最基础的商店分类进行了显示,经过了如下步骤:
-
创建一个新的 controller 处理以客户为中心的操作
-
实现默认的
index()
action -
在 Store controller 中调用
order()
方法控制网站中的数据按顺序排列 -
实现一个 view 和一个布局
-
使用辅助方法将价格格式化为我们期望的样子
-
使用 CSS 样式表
-
编写 controller 的功能测试
-
实现页面部分模块的缓存
是时候复查一下所有的功能,并且准备继续下一个任务了,也就是说,我们是时候制作购物车了!
自习天地
有一些知识需要你自己尝试:
-
在侧边栏添加一个日期和时间。它不需要是动态的,只展示页面显示时的时间即可
-
试试几种不同的
number_to_currency
辅助方法选项,并且看看它们对分类列表的影响 -
通过
assert_select
编写一些商品维护应用的功能测试。这些测试需要在 test/controllers/product_controller_test.rb 中替换。 -
需要提醒一下,一个迭代结束时应该用 Git 保存劳动成果。如果你一直都是这样做的,你就已经有了良好的基础。在 242 页时你还需要回到当前的工作成果中,这时要使用更多的 Git 功能。
本文翻译自《Agile Web Development with Rails 4》,目的为学习所用,如有转载请注明出处。