07-Task-C-Catalog-Display

优质
小牛编辑
121浏览
2023-12-01

总而言之,之前确实是一次成功的迭代。我们已经收集了客户的需求,绘制了基本流程,也实现了我们优先需要的数据,并且将 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 这个价格应该展示为 $12.34,13 应该展示为 $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

再强调一次,此时你需要重启服务,快速保存模板以验证模板的缓存已经关闭。

小结

我们将最基础的商店分类进行了显示,经过了如下步骤:

  1. 创建一个新的 controller 处理以客户为中心的操作

  2. 实现默认的 index() action

  3. 在 Store controller 中调用 order() 方法控制网站中的数据按顺序排列

  4. 实现一个 view 和一个布局

  5. 使用辅助方法将价格格式化为我们期望的样子

  6. 使用 CSS 样式表

  7. 编写 controller 的功能测试

  8. 实现页面部分模块的缓存

是时候复查一下所有的功能,并且准备继续下一个任务了,也就是说,我们是时候制作购物车了!

自习天地

有一些知识需要你自己尝试:

  • 在侧边栏添加一个日期和时间。它不需要是动态的,只展示页面显示时的时间即可
  • 试试几种不同的 number_to_currency 辅助方法选项,并且看看它们对分类列表的影响
  • 通过 assert_select 编写一些商品维护应用的功能测试。这些测试需要在 test/controllers/product_controller_test.rb 中替换。
  • 需要提醒一下,一个迭代结束时应该用 Git 保存劳动成果。如果你一直都是这样做的,你就已经有了良好的基础。在 242 页时你还需要回到当前的工作成果中,这时要使用更多的 Git 功能。