Python计算机视觉编程

Programming Computer Vision with Python

第五章 完善布局

    第四章对 Ruby 做了简单的介绍,我们讲解了如何在应用程序中引入样式表,不过,就像在 4.3.4 节中说过的,这个样式表现在还是空的。本章我们会做些修改,把 Bootstrap 框架引入应用程序中,然后再添加一些自定义的样式。1 我们还会把已经创建的页面(例如“首页”和“关于”页面)添加到布局中(5.1 节)。在这个过程中,我们会介绍局部视图(partial)、Rails 路由和 asset pipeline,还会介绍 Sass(5.2 节)。我们还会用最新的 RSpec 技术重构第三章中的测试。最后,我们还会向前迈出很重要的一步:允许用户在我们的网站中注册。

    5.1 添加一些结构

    本书是关于 Web 开发而不是 Web 设计的,不过在一个看起来很垃圾的应用程序中开发会让人提不起劲,所以本书我们要向布局中添加一些结构,再加入一些 CSS 构建基本的样式。除了使用自定义的 CSS 之外,我们还会使用 Bootstrap,由 Twitter 开发的开源 Web 设计框架。我们还要按照一定的方式组织代码,即使用局部视图来保持布局文件的结构清晰,避免大量的代码混杂在布局文件中。

    开发 Web 应用程序时,尽早的对用户界面有个统筹安排往往会对你有所帮助。在本书后续内容中,我会经常插入网页的构思图(mockup)(在 Web 领域经常称之为“线框图(wireframe)”),这是对应用程序最终效果的草图设计。2 本章大部分内容都是在开发 3.1 节中介绍的静态页面,页面中包含一个网站 LOGO、导航条头部和网站底部。这些网页中最重要的一个是“首页”,它的构思图如图 5.1 所示。图 5.7 是最终实现的效果。你会发现二者之间的某些细节有所不同,例如,在最终实现的页面中我们加入了一个 Rails LOGO——这没什么关系,因为构思图没必要画出每个细节。

    home_page_mockup_bootstrap

    图 5.1:示例程序“首页”的构思图

    和之前一样,如果你使用 Git 做版本控制的话,现在最好创建一个新分支:

    $ git checkout -b filling-in-layout
    

    5.1.1 网站导航

    在示例程序中加入链接和样式的第一步,要修改布局文件 application.html.erb(上次使用是在代码 4.3 中),添加一些 HTML 结构。我们要添加一些区域,一些 CSS class,以及网站导航。布局文件的内容参见代码 5.1,对各部分代码的说明紧跟其后。如果你迫不及待的想看到结果,请查看图 5.2。(注意:结果(还)不是很让人满意。)

    代码 5.1 添加一些结构后的网站布局文件
    app/views/layouts/application.html.erb

    <!DOCTYPE html>
    <html>
      <head>
        <title><%= full_title(yield(:title)) %></title>
        <%= stylesheet_link_tag    "application", media: "all" %>
        <%= javascript_include_tag "application" %>
        <%= csrf_meta_tags %>
        <!--[if lt IE 9]>
        <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
        <![endif]-->
      </head>
      <body>
        <header class="navbar navbar-fixed-top">
          <div class="navbar-inner">
            <div class="container">
              <%= link_to "sample app", '#', id: "logo" %>
              <nav>
                <ul class="nav pull-right">
                  <li><%= link_to "Home",    '#' %></li>
                  <li><%= link_to "Help",    '#' %></li>
                  <li><%= link_to "Sign in", '#' %></li>
                </ul>
              </nav>
            </div>
          </div>
        </header>
        <div class="container">
          <%= yield %>
        </div>
      </body>
    </html>
    

    需要特别注意一下 Hash 风格从 Ruby 1.8 到 Ruby 1.9 的转变(参见 4.3.3 节)。即把

    <%= stylesheet_link_tag "application", :media => "all" %>
    

    换成

    <%= stylesheet_link_tag "application", media: "all" %>
    

    有一点很重要需要注意一下,因为旧的 Hash 风格使用范围还很广,所以两种用法你都要能够识别。

    我们从上往下看一下代码 5.1 中新添加的元素。3.1 节简单的介绍过,Rails 3 默认会使用 HTML5(如 <!DOCTYPE html> 所示),因为 HTML5 标准还很新,有些浏览器(特别是较旧版本的 IE 浏览器)还没有完全支持,所以我们加载了一些 JavaScript 代码(称作“HTML5 shim”)来解决这个问题:

    <!--[if lt IE 9]>
    <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
    <![endif]-->
    

    如下有点古怪的句法

    <!--[if lt IE 9]>
    

    只有当 IE 浏览器的版本小于 9 时(if lt IE 9)才会加载其中的代码。这个奇怪的 [if lt IE 9] 句法不是 Rails 提供的,其实它是 IE 浏览器为了解决兼容性问题而特别支持的条件注释(conditional comment)。这就带来了一个好处,因为这说明我们只会在 IE9 以前的版本中加载 HTML5 shim,而 Firefox、Chrome 和 Safari 等其他浏览器则不会受到影响。

    后面的区域是一个 header,包含网站的 LOGO(纯文本)、一些小区域(使用 div 标签)和一个导航列表元素:

    <header class="navbar navbar-fixed-top">
      <div class="navbar-inner">
        <div class="container">
          <%= link_to "sample app", '#', id: "logo" %>
          <nav>
            <ul class="nav pull-right">
              <li><%= link_to "Home",    '#' %></li>
              <li><%= link_to "Help",    '#' %></li>
              <li><%= link_to "Sign in", '#' %></li>
            </ul>
          </nav>
        </div>
      </div>
    </header>
    

    header 标签的意思是放在网页顶部的内容。我们为 header 标签指定了两个 CSS class3navbarnavbar-fixed-top,用空格分开:

    <header class="navbar navbar-fixed-top">
    

    所有的 HTML 元素都可以指定 class 和 id,它们不仅是个标注,在 CSS 样式中也有用(5.1.2 节)。class 和 id 之间主要的区别是,class 可以在同一个网页中多次使用,而 id 只能使用一次。这里的 navbarnavbar-fixed-top 在 Bootstrap 框架中有特殊的意义,我们会在 5.1.2 节中安装并使用 Bootstrap。header 标签内是一些 div 标签:

    <div class="navbar-inner">
      <div class="container">
    

    div 标签是常规的区域,除了把文档分成不同的部分之外,没有特殊的意义。在以前的 HTML 中,div 标签被用来划分网站中几乎所有的区域,但是 HTML5 增加了 headernavsection 元素,用来划分大多数网站中都有用到的区域。本例中,每个 div 也都指定了一个 CSS class。和 header 标签的 class 一样,这些 class 在 Bootstrap 中也有特殊的意义。

    在这些 div 之后,有一些 ERb 代码:

    <%= link_to "sample app", '#', id: "logo" %>
    <nav>
      <ul class="nav pull-right">
        <li><%= link_to "Home",    '#' %></li>
        <li><%= link_to "Help",    '#' %></li>
        <li><%= link_to "Sign in", '#' %></li>
      </ul>
    </nav>
    

    这里使用了 Rails 中的 link_to 帮助方法来创建链接(在 3.3.2 节中我们是直接创建 a 标签来实现的)。link_to 的第一个参数是链接文本,第二个参数是链接地址。在 5.3.3 节中我们会指定链接地址为设置好的路由,这里我们用的是 Web 设计中经常使用的占位符 #。第三个参数是可选的,为一个 Hash,本例使用这个参数为 LOGO 添加了一个 logo id。(其他三个链接没有使用这个 Hash 参数,没关系,因为这个参数是可选的。)Rails 帮助方法经常这样使用 Hash 参数,可以让我们仅使用 Rails 的帮助方法就能灵活的添加 HTML 属性。

    第二个 div 中是个导航链接列表,使用无序列表标签 ul,以及列表项目标签 li

    <nav>
      <ul class="nav pull-right">
        <li><%= link_to "Home",    '#' %></li>
        <li><%= link_to "Help",    '#' %></li>
        <li><%= link_to "Sign in", '#' %></li>
      </ul>
    </nav>
    

    上面代码中的 nav 标签以前是不需要的,它的目的是显示导航链接。ul 标签指定的 navpull-right class 在 Bootstrap 中有特殊的意义。 Rails 处理这个布局文件并执行其中的 ERb 代码后,生成的列表如下面的代码所示:

    <nav>
      <ul class="nav pull-right">
        <li><a href="#">Home</a></li>
        <li><a href="#">Help</a></li>
        <li><a href="#">Sign in</a></li>
      </ul>
    </nav>
    

    布局文件的最后一个 div 是主内容区域:

    <div class="container">
      <%= yield %>
    </div>
    

    和之前一样,container class 在 Bootstrap 中有特殊的意义。3.3.4 节已经介绍过,yield 会把各页面中的内容插入网站的布局中。

    除了网站的底部(在 5.1.3 节添加)之外,布局现在就完成了,访问一下“首页”就能看到结果了。为了利用后面添加的样式,我们要向 home.html.erb 视图中加入一些元素。(参见代码 5.2。)

    代码 5.2 “首页”的代码,包含一个到注册页面的链接
    app/views/static_pages/home.html.erb

    <div class="center hero-unit">
      <h1>Welcome to the Sample App</h1>
    
      <h2>
        This is the home page for the
        <a href="http://railstutorial.org/">Ruby on Rails Tutorial</a>
        sample application.
      </h2>
    
      <%= link_to "Sign up now!", '#', class: "btn btn-large btn-primary" %>
    </div>
    
    <%= link_to image_tag("rails.png", alt: "Rails"), 'http://rubyonrails.org/' %>
    

    上面代码中第一个 link_to 创建了一个占位链接,指向第七章中创建的用户注册页面

    <a href="#" class="btn btn-large btn-primary">Sign up now!</a>
    

    div 标签中的 hero-unit class 在 Bootstrap 中有特殊的意义,注册按钮的 btnbtn-largebtn-primary 也是一样。

    第二个 link_to 用到了 image_tag 帮助方法,第一个参数是图片的路径;第二个参数是可选的,一个 Hash,本例中这个 Hash 参数使用一个 Symbol 键设置了图片的 alt 属性。为了更好的理解,我们来看一下生成的 HTML:4

    <img alt="Rails" src="/assets/rails.png" />
    

    alt 属性的内容会在图片无法加载时显示,也会在针对视觉障碍人士的屏幕阅读器中显示。人们有时懒得加上 alt 属性,可是在 HTML 标准中却是必须的。幸运的是,Rails 默认会加上 alt 标签,如果你没有在调用 image_tag 时指定的话,Rails 就会使用图片的文件名(不包括扩展名)。本例中,我们自己设定了 alt 文本,显示一个首字母大写的“Rails”。

    现在我们终于可以看到劳动的果实了(如图 5.2)。你可能会说,这并不很美观啊。或许吧。不过也可以小小的高兴一下,我们已经为 HTML 结构指定了合适的 class,可以用来添加 CSS。

    顺便说一下,你可能会奇怪 rails.png 这个图片为什么可以显示出来,它是怎么来的呢?其实每个 Rails 应用程序中都有这个图片,存放在 app/assets/images/ 目录下。因为我们使用的是 image_tag 帮助方法,Rails 会通过 asset pipeline 找到这个图片。(5.2 节

    layout_no_logo_or_custom_css_bootstrap

    图 5.2:没有定义 CSS 的“首页”(/static_pages/home

    5.1.2 Bootstrap 和自定义的 CSS

    5.1.1 节我们为很多 HTML 元素指定了 CSS class,这样我们就可以使用 CSS 灵活的构建布局了。5.1.1 节中已经说过,很多 class 在 Bootstrap 中都有特殊的意义。Bootstrap 是 Twitter 开发的框架,可以方便的把精美的 Web 设计和用户界面元素添加到使用 HTML5 开发的应用程序中。本节,我们会结合 Bootstrap 和一些自定义的 CSS 为示例程序添加样式。

    首先要安装 Bootstrap,在 Rails 程序中可以使用 bootstrap-sass 这个 gem,参见代码 5.3。Bootstrap 框架本身使用 LESS 来动态的生成样式表,而 Rails 的 asset pipeline 默认支持的是(非常类似的)Sass,bootstrap-sass 会将 LESS 转换成 Sass 格式,而且 Bootstrap 中必要的文件都可以在当前的应用程序中使用。5

    代码 5.3 把 bootstrap-sass 加入 Gemfile

    source 'https://rubygems.org'
    
    gem 'rails', '3.2.13'
    gem 'bootstrap-sass', '2.0.4'
    .
    .
    .
    

    像往常一样,运行 bundle install 安装 Bootstrap:

    $ bundle install
    

    然后重启 Web 服务器,改动才能在应用程序中生效。(在大多数系统中可以使用 Ctrl-C 结束服务器,然后再执行 rails server 命令。)

    要向应用程序中添加自定义的 CSS,首先要创建一个 CSS 文件:

    app/assets/stylesheets/custom.css.scss
    

    (使用你喜欢的文本编辑器或者 IDE 创建这个文件。)文件存放的目录和文件名都很重要。其中目录

    app/assets/stylesheets
    

    是 asset pipeline 的一部分(5.2 节),这个目录中的所有样式表都会自动的包含在网站的 application.css 中。custom.css.scss 文件的第一个扩展名是 .css,说明这是个 CSS 文件;第二个扩展名是 .scss,说明这是个“Sassy CSS”文件。asset pipeline 会使用 Sass 处理这个文件。(在 5.2.2 节中才会使用 Sass,有了它 bootstrap-sass 才能运作。)创建了自定义 CSS 所需的文件后,我们可以使用 @import 引入 Bootstrap,如代码 5.4 所示。

    代码 5.4 引入 Bootstrap
    app/assets/stylesheets/custom.css.scss

    @import "bootstrap";
    

    这行代码会引入整个 Bootstrap CSS 框架,结果如图 5.3 所示。(或许你要通过 Ctrl-C 来重启服务器。)可以看到,文本的位置还不是很合适,LOGO 也没有任何样式,不过颜色搭配和注册按钮看起来还不错。

    sample_app_only_bootstrap

    图 5.3:使用 Bootstrap CSS 后的示例程序

    下面我们要加入一些整站都会用到的 CSS,用来样式化网站布局和各单独页面,如代码 5.5 所示。代码 5.5 中定义了很多样式规则。为了说明 CSS 规则的作用,我们经常会加入一些 CSS 注释,放在 /*...*/ 之中。代码 5.5 的 CSS 加载后的效果如图 5.4 所示。

    代码 5.5 添加全站使用的 CSS
    app/assets/stylesheets/custom.css.scss

    @import "bootstrap";
    
    /* universal */
    
    html {
      overflow-y: scroll;
    }
    
    body {
      padding-top: 60px;
    }
    
    section {
      overflow: auto;
    }
    
    textarea {
      resize: vertical;
    }
    
    .center {
      text-align: center;
    }
    
    .center h1 {
      margin-bottom: 10px;
    }
    

    sample_app_universal

    图 5.4:添加一些空白和其他的全局性样式

    注意代码 5.5 中的 CSS 格式是很统一的。一般来说,CSS 规则是通过 class、id、HTML 标签或者三者结合在一起来定义的,后面会跟着一些样式声明。例如:

    body {
      padding-top: 60px;
    }
    

    把页面的上内边距设为 60 像素。我们在 header 标签上指定了 navbar-fixed-top class,Bootstrap 就把这个导航条固定在页面的顶部。所以页面的上内边距会把主内容区和导航条隔开一段距离。下面的 CSS 规则:

    .center {
      text-align: center;
    }
    

    .center class 的样式定义为 text-align: center;.center 中的点号说明这个规则是样式化一个 class。(我们会在代码 5.7 中看到,# 是样式化一个 id。)这个规则的意思是,任何 class 为 .center 的标签(例如 div),其中包含的内容都会在页面中居中显示。(代码 5.2 中有用到这个 class。)

    虽然 Bootstrap 中包含了很精美的文字排版样式,我们还是要为网站添加一些自定义的规则,如代码 5.6 所示。(并不是所有的样式都会应用于“首页”,但所有规则都会在网站中的某个地方用到。)代码 5.6 的效果如图 5.5 所示。

    代码 5.6 添加一些精美的文字排版样式
    app/assets/stylesheets/custom.css.scss

    @import "bootstrap";
    .
    .
    .
    
    /* typography */
    
    h1, h2, h3, h4, h5, h6 {
      line-height: 1;
    }
    
    h1 {
      font-size: 3em;
      letter-spacing: -2px;
      margin-bottom: 30px;
      text-align: center;
    }
    
    h2 {
      font-size: 1.7em;
      letter-spacing: -1px;
      margin-bottom: 30px;
      text-align: center;
      font-weight: normal;
      color: #999;
    }
    
    p {
      font-size: 1.1em;
      line-height: 1.7em;
    }
    

    sample_app_typography

    图 5.5:添加了一些文字排版样式

    最后,我们还要为只包含文本“sample app”的网站 LOGO 添加一些样式。代码 5.7 中的 CSS 样式会把文字变成全大写字母,还修改了文字大小、颜色和位置。(我们使用的是 id,因为我们希望 LOGO 在页面中只出现一次,不过你也可以使用 class。)

    代码 5.7 添加网站 LOGO 的样式
    app/assets/stylesheets/custom.css.scss

    @import "bootstrap";
    .
    .
    .
    
    /* header */
    
    #logo {
      float: left;
      margin-right: 10px;
      font-size: 1.7em;
      color: #fff;
      text-transform: uppercase;
      letter-spacing: -1px;
      padding-top: 9px;
      font-weight: bold;
      line-height: 1;
    }
    
    #logo:hover {
      color: #fff;
      text-decoration: none;
    }
    

    其中 color: #fff; 会把 LOGO 文字的颜色变成白色。HTML 中的颜色代码是由 3 个 16 进制数组成的,分别代表了三原色中的红、绿、蓝。#ffffff 是 3 种颜色都为最大值的情况,代表了纯白色。#fff#ffffff 的简写形式。CSS 标准中为很多常用的 HTML 颜色定义了别名,例如 white 代表的是 #fff。代码 5.7 中的样式效果如图 5.6 所示。

    sample_app_logo

    图 5.6:样式化 LOGO 后的示例程序

    5.1.3 局部视图

    虽然代码 5.1 中的布局达到了目的,但它的内容看起来有点混乱。HTML shim 就占用了三行,而且使用了只针对 IE 的奇怪句法,所以如果能把它打包放在一个单独的地方就好了。头部的 HTML 自成一个逻辑单元,所以也可以把这部分打包放在某个地方。在 Rails 中我们可以使用局部视图来实现这种想法。先来看一下定义了局部视图之后的布局文件(参见代码 5.8)。

    代码 5.8 定义了 HTML shim 和头部局部视图之后的网站布局
    app/views/layouts/application.html.erb

    <!DOCTYPE html>
    <html>
      <head>
        <title><%= full_title(yield(:title)) %></title>
        <%= stylesheet_link_tag    "application", media: "all" %>
        <%= javascript_include_tag "application" %>
        <%= csrf_meta_tags %>
        <%= render 'layouts/shim' %>
      </head>
      <body>
        <%= render 'layouts/header' %>
        <div class="container">
          <%= yield %>
        </div>
      </body>
    </html>
    

    代码 5.8 中,我们把加载 HTML shim 的那几行代码换成了对 Rails 帮助函数 render 的调用:

    <%= render 'layouts/shim' %>
    

    这行代码会寻找一个名为 app/views/layouts/_shim.html.erb 的文件,执行文件中的代码,然后把结果插入视图。6(回顾一下,执行 Ruby 表达式并将结果插入模板中要使用 <%=...%>。)注意文件名 _shim.html.erb 的开头是个下划线,这个下划线是局部视图的命名约定,可以在目录中快速定位所有的局部视图。

    当然,若要局部视图起作用,我们要写入相应的内容。本例中的 HTML shim 局部视图只包含三行代码,如代码 5.9 所示。

    代码 5.9 HTML shim 局部视图
    app/views/layouts/_shim.html.erb

    <!--[if lt IE 9]>
    <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
    <![endif]-->
    

    类似的,我们可以把头部的内容移入局部视图,如代码 5.10 所示,然后再次调用 render 把这个局部视图插入布局中。

    代码 5.10 网站头部的局部视图
    app/views/layouts/_header.html.erb

    <header class="navbar navbar-fixed-top">
      <div class="navbar-inner">
        <div class="container">
          <%= link_to "sample app", '#', id: "logo" %>
          <nav>
            <ul class="nav pull-right">
              <li><%= link_to "Home",    '#' %></li>
              <li><%= link_to "Help",    '#' %></li>
              <li><%= link_to "Sign in", '#' %></li>
            </ul>
          </nav>
        </div>
      </div>
    </header>
    

    现在我们已经知道怎么创建局部视图了,让我们来加入和头部对应的网站底部吧。你或许已经猜到了,我们会把这个局部视图命名为 _footer.html.erb,放在布局目录中(参见代码 5.11)。7

    代码 5.11 网站底部的局部视图
    app/views/layouts/_footer.html.erb

    <footer class="footer">
      <small>
        <a href="http://railstutorial.org/">Rails Tutorial</a>
        by Michael Hartl
      </small>
      <nav>
        <ul>
          <li><%= link_to "About",   '#' %></li>
          <li><%= link_to "Contact", '#' %></li>
          <li><a href="http://news.railstutorial.org/">News</a></li>
        </ul>
      </nav>
    </footer>
    

    和头部类似,在底部我们使用 link_to 创建到“关于”页面和“联系”页面的链接,地址暂时使用占位符 #。(和 header 一样,footer 标签也是 HTML5 新增加的。)

    按照 HTML shim 和头部局部视图采用的方式,我们也可以在布局视图中渲染底部局部视图。(参见代码 5.12。)

    代码 5.12 网站的布局,包含底部局部视图
    app/views/layouts/application.html.erb

    <!DOCTYPE html>
    <html>
      <head>
        <title><%= full_title(yield(:title)) %></title>
        <%= stylesheet_link_tag    "application", media: "all" %>
        <%= javascript_include_tag "application" %>
        <%= csrf_meta_tags %>
        <%= render 'layouts/shim' %>
      </head>
      <body>
        <%= render 'layouts/header' %>
        <div class="container">
          <%= yield %>
          <%= render 'layouts/footer' %>
        </div>
      </body>
    </html>
    

    当然,如果没有样式的话,底部还是很丑的(样式参见代码 5.13)。添加样式后的效果如图 5.7 所示。

    代码 5.13 添加底部所需的 CSS
    app/assets/stylesheets/custom.css.scss

    .
    .
    .
    
    /* footer */
    
    footer {
      margin-top: 45px;
      padding-top: 5px;
      border-top: 1px solid #eaeaea;
      color: #999;
    }
    
    footer a {
      color: #555;
    }
    
    footer a:hover {
      color: #222;
    }
    
    footer small {
      float: left;
    }
    
    footer ul {
      float: right;
      list-style: none;
    }
    
    footer ul li {
      float: left;
      margin-left: 10px;
    }
    

    site_with_footer_bootstrap

    图 5.7:添加底部后的“首页”(/static_pages/home

    5.2 Sass 和 asset pipeline

    Rails 3.0 与之前版本的主要不同之一是 asset pipeline,这个功能可以明显提高如 CSS、JavaScript和图片等静态资源文件(asset)的生成效率,降低管理成本。本节我们会概览一下 asset pipeline,然后再介绍如何使用 Sass 这个生成 CSS 很强大的工具,Sass 现在是 asset pipeline 默认的一部分。

    5.1.1 Asset pipeline

    Asset pipeline 对 Rails做了很多改动,但对 Rails 开发者来说只有三个特性需要了解:资源目录,清单文件(manifest file),还有预处理器引擎(preprocessor engine)。8 我们会一个一个的介绍。

    资源目录

    在 Rails 3.0 之前(包括 3.0),静态文件分别放在如下的 public/ 目录中:

    • public/stylesheets
    • public/javascripts
    • public/images

    这些文件夹中的文件通过请求 http://example.com/stylesheets 等地址直接发送给浏览器。(Rails 3.0 之后的版本也可以这么做。)

    从 Rails 3.1 开始,静态文件可以存放在三个标准的目录中,各有各的用途:

    • app/assets:存放当前应用程序用到的资源文件
    • lib/assets:存放开发团队自己开发的代码库用到的资源文件
    • vendor/assets:存放第三方代码库用到的资源文件

    你可能猜到了,上面的目录中都会有针对不同资源类型的子目录。例如:

    $ ls app/assets/
    images javascripts stylesheets
    

    现在我们就可以知道 5.1.2 节custom.css.scss 存放位置的用意:因为 custom.css.scss 是应用程序本身用到的,所以把它存放在 app/assets/stylesheets 中。

    清单文件

    当你把资源文件存放在适当的目录后,要通过清单文件告诉 Rails怎么把它们合并成一个文件(使用 Sprockets gem。只适用于 CSS 和 JavaScript,而不会处理图片。)举个例子,让我们看一下应用程序默认的样式表清单文件(参见代码 5.14)。

    代码 5.14 应用程序的样式表清单文件
    app/assets/stylesheets/application.css

    /*
     * This is a manifest file that'll automatically include all the stylesheets
     * available in this directory and any sub-directories. You're free to add
     * application-wide styles to this file and they'll appear at the top of the
     * compiled file, but it's generally better to create a new file per style
     * scope.
     *= require_self
     *= require_tree .
    */
    

    这里的关键代码是几行 CSS 注释,Sprockets 会通过这些注释加载相应的文件:

    /*
     * .
     * .
     * .
     *= require_self
     *= require_tree .
    */
    

    上面代码中的

    *= require_tree .
    

    会把 app/assets/stylesheets 目录中的所有 CSS 文件都引入应用程序的样式表中。

    下面这行:

    *= require_self
    

    会把 application.css 这个文件中的 CSS 也加载进来。

    Rails 提供的默认清单文件可以满足我们的要求,所以本书不会对其做任何修改。Rails 指南中有一篇专门介绍 asset pipeline 的文章,该文有你需要知道的更为详细的内容。

    预处理器引擎

    准备好资源文件后,Rails 会使用一些预处理器引擎来处理它们,通过清单文件将其合并,然后发送给浏览器。我们通过扩展名告诉 Rails 要使用哪个预处理器。三个最常用的扩展名是:Sass 文件的 .scss,CoffeeScript 文件的 .coffee,ERb 文件的 .erb。我们在 3.3.3 节介绍过 ERb,5.2.2 节会介绍 Sass。本教程不需要使用 CoffeeScript,这是一个很小巧的语言,可以编译成 JavaScript。(RailsCast 中关于 CoffeeScript 的视频是个很好的入门教程。)

    预处理器引擎可以连接在一起使用,因此

    foobar.js.coffee
    

    只会使用 CoffeeScript 处理器,而

    foobar.js.erb.coffee
    

    会使用 CoffeeScript 和 ERb 处理器(按照扩展名的顺序从右向左处理,所以 CoffeeScript 处理器会先执行)。

    在生产环境中的效率问题

    Asset pipeline 带来的好处之一是,它会自动优化资源文件,在生产环境中使用效果极佳。CSS 和 JavaScript 的传统组织方式是将不同功能的代码放在不同的文件中,而且代码的格式是对人类友好的(有很多缩进)。虽然这对编程人员很友好,但在生产环境中使用却效率低下,加载大量的文件会明显增加页面加载时间(这是影响用户体验最主要的因素之一)。使用 asset pipeline,生产环境中应用程序所有的样式都会集中到一个 CSS 文件中(application.css),所有 JavaScript 代码都会集中到一个 JavaScript 文件中(javascript.js),而且还会压缩这些文件(包括 lib/assetsvendor/assets 中的相关文件),把不必要的空格删除,减小文件大小。这样我们就最好的平衡了两方面的需求:编程人员使用格式友好的多个文件,生产环境中使用优化后的单个文件。

    5.2.2 句法强大的样式表

    Sass 是一种编写 CSS 的语言,从多方面增强了 CSS 的功能。本节我们会介绍两个最主要的功能,嵌套和变量。(还有一个是 mixin,会在 7.1.1 节中介绍。)

    5.1.2 节中的简单介绍,Sass 支持一种名为 SCSS 的格式(扩展名为 .scss),这是 CSS 句法的一个扩展集。SCSS 只是为 CSS 添加了一些功能,而没有定义全新的句法。9 也就是说,所有合法的 CSS 文件都是合法的 SCSS 文件,这对已经定义了样式的项目来说是件好事。在我们的程序中,因为要使用 Bootstrap,从一开始就使用了 SCSS。Rails 的 asset pipeline 会自动使用 Sass 预处理器处理扩展名为 .scss 的文件,所以 custom.css.scss 文件会首先经由 Sass 预处理器处理,然后引入程序的样式表中,再发送给浏览器。

    嵌套

    样式表中经常会定义嵌套元素的样式,例如,在代码 5.1 中,定义了 .center.centr h1 两个样式:

    .center {
      text-align: center;
    }
    
    .center h1 {
      margin-bottom: 10px;
    }
    

    使用 Sass 可将其改写成

    .center {
      text-align: center;
      h1 {
        margin-bottom: 10px;
      }
    }
    

    上面代码中的 h1 会自动嵌入 .center 中。

    嵌套还有另一种形式,句法稍有不同。在代码 5.7 中,有如下的代码

    #logo {
      float: left;
      margin-right: 10px;
      font-size: 1.7em;
      color: #fff;
      text-transform: uppercase;
      letter-spacing: -1px;
      padding-top: 9px;
      font-weight: bold;
      line-height: 1;
    }
    
    #logo:hover {
      color: #fff;
      text-decoration: none;
    }
    

    其中 LOGO 的 id #logo 出现了两次,一次是单独出现的,另一次是和 hover 伪类一起出现的(鼠标悬停其上时的样式)。如果要嵌套第二个样式,我们需要引用父级元素 #logo,在 SCSS 中,使用 & 符号实现:

    #logo {
      float: left;
      margin-right: 10px;
      font-size: 1.7em;
      color: #fff;
      text-transform: uppercase;
      letter-spacing: -1px;
      padding-top: 9px;
      font-weight: bold;
      line-height: 1;
      &:hover {
        color: #fff;
        text-decoration: none;
      }
    }
    

    把 SCSS 转换成 CSS 时,Sass 会把 &:hover 编译成 #logo:hover

    这两种嵌套方式都可以用于代码 5.13 中的底部样式上,转换后的样式如下:

    footer {
      margin-top: 45px;
      padding-top: 5px;
      border-top: 1px solid #eaeaea;
      color: #999;
      a {
        color: #555;
        &:hover {
          color: #222;
        }
      }
      small {
        float: left;
      }
      ul {
        float: right;
        list-style: none;
        li {
          float: left;
          margin-left: 10px;
        }
      }
    }
    

    自己动手转换一下代码 5.13 是个不错的练习,转换之后你应该验证一下 CSS 是否还能正常使用。

    变量

    Sass 允许我们自定义变量来避免重复,这样也可以写出更具表现力的代码。例如,代码 5.6 和代码 5.13 中都重复使用了同一个颜色代码:

    h2 {
      .
      .
      .
      color: #999;
    }
    .
    .
    .
    footer {
      .
      .
      .
      color: #999;
    }
    

    上面代码中的 #999 是淡灰色(ligh gray),我们可以为它定义一个变量:

    $lightGray: #999;
    

    然后我们就可以这样写 SCSS:

    $lightGray: #999;
    .
    .
    .
    h2 {
      .
      .
      .
      color: $lightGray;
    }
    .
    .
    .
    footer {
      .
      .
      .
      color: $lightGray;
    }
    

    因为像 $lightGray 这样的变量名比 #999 更具说明性,所以为没有重复使用的值定义变量往往也是很有用的。Bootstrap 框架定义了很多颜色变量,Bootstrap 页面中有这些变量的 LESS 形式。这个页面中的变量使用的是 LESS 句法,而不是 Sass 句法,不过 bootstrap-sass gem 为我们提供了对应的 Sass 形式。二者之间的对应关系也不难猜出,LESS 使用 @ 符号定义变量,而 Sass 使用 $ 符号。在 Bootstrap 的变量页面我们可以看到为淡灰色定义的变量:

    @grayLight: #999;
    

    也就是说,在 bootstrap-sass gem 中会有一个对应的 SCSS 变量 $grayLight。我们可以用它换掉自己定义的 $lightGray 变量:

    h2 {
      .
      .
      .
      color: $grayLight;
    }
    .
    .
    .
    footer {
      .
      .
      .
      color: $grayLight;
    }
    

    使用 Sass 提供的嵌套和变量功能后得到的完整 SCSS 文件如代码 5.15 所示。这段代码中使用了 Sass 形式的颜色变量(参照 Bootstrap 变量页面中定义的 LESS 形式的颜色变量)和内置的颜色名称(例如,white 代表 #fff)。请特别注意一下 footer 标签样式明显的改进。

    译者注:一般不建议在 CSS 中使用颜色名称,因为不同的浏览器和不同的系统对同一个颜色的渲染有所不同,没有使用十六进制的颜色代码准确。

    代码 5.15 使用嵌套和变量转换后的 SCSS 文件
    app/assets/stylesheets/custom.css.scss

    @import "bootstrap";
    
    /* mixins, variables, etc. */
    
    $grayMediumLight: #eaeaea;
    
    /* universal */
    
    html {
      overflow-y: scroll;
    }
    
    body {
      padding-top: 60px;
    }
    
    section {
      overflow: auto;
    }
    
    textarea {
      resize: vertical;
    }
    
    .center {
      text-align: center;
      h1 {
        margin-bottom: 10px;
      }
    }
    
    /* typography */
    
    h1, h2, h3, h4, h5, h6 {
      line-height: 1;
    }
    
    h1 {
      font-size: 3em;
      letter-spacing: -2px;
      margin-bottom: 30px;
      text-align: center;
    }
    
    h2 {
      font-size: 1.7em;
      letter-spacing: -1px;
      margin-bottom: 30px;
      text-align: center;
      font-weight: normal;
      color: $grayLight;
    }
    
    p {
      font-size: 1.1em;
      line-height: 1.7em;
    }
    
    
    /* header */
    
    #logo {
      float: left;
      margin-right: 10px;
      font-size: 1.7em;
      color: white;
      text-transform: uppercase;
      letter-spacing: -1px;
      padding-top: 9px;
      font-weight: bold;
      line-height: 1;
      &:hover {
        color: white;
        text-decoration: none;
      }
    }
    
    /* footer */
    
    footer {
      margin-top: 45px;
      padding-top: 5px;
      border-top: 1px solid $grayMediumLight;
      color: $grayLight;
      a {
        color: $gray;
        &:hover {
          color: $grayDarker;
        }
      }
      small {
        float: left;
      }
      ul {
        float: right;
        list-style: none;
        li {
          float: left;
          margin-left: 10px;
        }
      }
    }
    

    Sass 提供了很多功能,可以用来简化样式表,不过代码 5.15 只用到了最主要的功能,这是个好的开端。更多功能请查看 Sass 网站

    5.3 布局中的链接

    我们已经为网站的布局定义了看起来还不错的样式,下面要把链接中暂时使用的占位符 # 换成真正的链接地址。当然,我们可以像下面这样手动加入链接:

    <a href="/static_pages/about">About</a>
    

    不过这样不太符合 Rails 风格。一者,“关于”页面的地址如果是 /about 而不是 /static_pages/about 就好了;再者,Rails 习惯使用具名路由(named route)来指定链接地址,相应的代码如下:

    <%= link_to "About", about_path %>
    

    使用这种方式能更好的表达链接与 URI 和路由的对应关系,如表格 5.1 所示。本章完结之前除了最后一个链接之外,其他的链接都会设定好。(第八章会添加最后一个。)

    页面 URI 对应的路由
    “首页” / root_path
    “关于” /about about_path
    “关于” /help help_path
    “联系” /contact contact_path
    “注册” /signup signup_path
    “登录” /signin signin_path

    表格 5.1:网站中链接的路由和 URI 地址的映射关系

    继续之前,让我们先添加一个“联系”页面(第三章的一个练习题),测试如代码 5.16 所示,形式和代码 3.18 差不多。注意,和应用程序的代码一样,代码 5.16 中 Hash 使用的也是 Ruby 1.9 风格。

    代码 5.16 “联系”页面的测试
    spec/requests/static_pages_spec.rb

    require 'spec_helper'
    
    describe "Static pages" do
      .
      .
      .
      describe "Contact page" do
    
        it "should have the h1 'Contact'" do
          visit '/static_pages/contact'
          page.should have_selector('h1', text: 'Contact')
        end
    
        it "should have the title 'Contact'" do
          visit '/static_pages/contact'
          page.should have_selector('title',
                        text: "Ruby on Rails Tutorial Sample App | Contact")
        end
      end
    end
    

    你应该看一下这个测试是否是失败的:

    $ bundle exec rspec spec/requests/static_pages_spec.rb
    

    这里要采用的步骤和 3.2.2 节中添加“关于”页面的步骤是一致的:先更新路由设置(参见代码 5.17),然后在 StaticPages 控制器中添加 contact 动作(参见代码 5.18),最后再编写“联系”页面的视图(参见代码 5.19)。

    代码 5.17 添加“联系”页面的路由设置
    config/routes.rb

    SampleApp::Application.routes.draw do
      get "static_pages/home"
      get "static_pages/help"
      get "static_pages/about"
      get "static_pages/contact"
      .
      .
      .
    end
    

    代码 5.18 添加“联系”页面对应的动作
    app/controllers/static_pages_controller.rb

    class StaticPagesController < ApplicationController
      .
      .
      .
      def contact
      end
    end
    

    代码 5.19 “联系”页面的视图
    app/views/static_pages/contact.html.erb

    <% provide(:title, 'Contact') %>
    <h1>Contact</h1>
    <p>
      Contact Ruby on Rails Tutorial about the sample app at the
      <a href="http://railstutorial.org/contact">contact page</a>.
    </p>
    

    再看一下测试是否可以通过:

    $ bundle exec rspec spec/requests/static_pages_spec.rb
    

    5.3.1 路由测试

    静态页面的集成测试编写完之后,再编写路由测试就简单了:只需把硬编码的地址换成表格 5.1中相应的具名路由就可以了。也就是说,要把

    visit '/static_pages/about'
    

    修改为

    visit about_path
    

    其他的页面也这样做,修改后的结果如代码 5.20 所示。

    代码 5.20 具名路由测试
    spec/requests/static_pages_spec.rb

    require 'spec_helper'
    
    describe "Static pages" do
    
      describe "Home page" do
    
        it "should have the h1 'Sample App'" do
          visit root_path
          page.should have_selector('h1', text: 'Sample App')
        end
    
        it "should have the base title" do
          visit root_path
          page.should have_selector('title',
                            text: "Ruby on Rails Tutorial Sample App")
        end
    
        it "should not have a custom page title" do
          visit root_path
          page.should_not have_selector('title', text: '| Home')
        end
      end
    
      describe "Help page" do
    
        it "should have the h1 'Help'" do
          visit help_path
          page.should have_selector('h1', text: 'Help')
        end
    
        it "should have the title 'Help'" do
          visit help_path
          page.should have_selector('title',
                            text: "Ruby on Rails Tutorial Sample App | Help")
        end
      end
    
      describe "About page" do
    
        it "should have the h1 'About'" do
          visit about_path
          page.should have_selector('h1', text: 'About Us')
        end
    
        it "should have the title 'About Us'" do
          visit about_path
          page.should have_selector('title',
                        text: "Ruby on Rails Tutorial Sample App | About Us")
        end
      end
    
      describe "Contact page" do
    
        it "should have the h1 'Contact'" do
          visit contact_path
          page.should have_selector('h1', text: 'Contact')
        end
    
        it "should have the title 'Contact'" do
          visit contact_path
          page.should have_selector('title',
                        text: "Ruby on Rails Tutorial Sample App | Contact")
        end
      end
    end
    

    和往常一样,现在应该看一下测试是否是失败的(红色):

    $ bundle exec rspec spec/requests/static_pages_spec.rb
    

    顺便说一下,很多人都会觉得代码 5.20 有很多重复,也很啰嗦,我们会在 5.3.4 节进行重构。

    5.3.2 Rails 路由

    我们已经编写了针对所有 URI 地址的测试,现在就要实现这些地址了。如 3.1.2 节所说,Rails 使用 config/routes.rb 文件设置 URI 地址的映射关系。如果你看一下默认的路由文件,会发现内容很杂乱,不过还是能提供些帮助的,因为有很多注释,说明了各路由的映射关系。我建议你找个时间通读一下路由文件,也建议你阅读一下 Rails 指南中《详解 Rails 路由》一文,更深入的了解一下路由。

    定义具名路由,要把

    get 'static_pages/help'
    

    修改为

    match '/help', to: 'static_pages#help'
    

    这样在 /help 地址上就有了一个可访问的页面,也定义了一个名为 help_path 的具名路由,该函数会返回相应页面的地址。(其实把 match 换成 get 效果是一样的,不过使用 match 更符合约定。)

    其他页面也要做类似修改,结果如代码 5.21 所示。不过“首页”有点特殊,参见代码 5.23。

    代码 5.21 静态页面的路由
    config/routes.rb

    SampleApp::Application.routes.draw do
      match '/help',    to: 'static_pages#help'
      match '/about',   to: 'static_pages#about'
      match '/contact', to: 'static_pages#contact'
      .
      .
      .
    end
    

    如果认真阅读代码 5.21,或许会发现它的作用。例如,你会发现

    match '/about', to: 'static_pages#about'
    

    会匹配 /about 地址,并将其分发到 StaticPages 控制器的 about 动作上。之前的设置意图更明显,我们用

    get 'static_pages/about'
    

    也可以得到相同的页面,不过 /about 的地址形式更简洁。而且,如前面提到的,match '/about' 会自动创建具名路由函数,可以在控制器和视图中使用:

    about_path => '/about'
    about_url  => 'http://localhost:3000/about'
    

    注意,about_url 返回的结果是完整的 URI 地址 http://localhost:3000/about(部署后,会用实际的域名替换 localhost:3000,例如 example.com)。如 5.3 节的用法,如果只想返回 /about,使用 about_path 就可以了。本书基本上都会使用惯用的 path 形式,不过在页面转向时会使用 url 形式,因为 HTTP 标准要求转向后的地址为完整的 URI,不过大多数浏览器都可以正常使用这两种形式。

    设置了这些路由之后,“帮助”页面、“关于”页面和“联系”页面的测试应该就可以通过了:

    $ bundle exec rspec spec/requests/static_pages_spec.rb
    

    不过“首页”的测试还是失败的。

    要设置“首页”的路由,可以使用如下的代码:

    match '/', to: 'static_pages#home'
    

    不过没必要这么做。Rails 在路由设置文件的下部为根地址 /(斜线)提供了特别的设置方式(参见代码 5.22)。

    代码 5.22 注释掉的根路由设置说明
    config/routes.rb

    SampleApp::Application.routes.draw do
      .
      .
      .
      # You can have the root of your site routed with "root"
      # just remember to delete public/index.html.
      # root :to => "welcome#index"
      .
      .
      .
    end
    

    按照上述说明,我们把根地址 / 映射到“首页”上(参见代码 5.23)。

    代码 5.23 添加根地址的路由设置
    config/routes.rb

    SampleApp::Application.routes.draw do
      root to: 'static_pages#home'
    
      match '/help',    to: 'static_pages#help'
      match '/about',   to: 'static_pages#about'
      match '/contact', to: 'static_pages#contact'
      .
      .
      .
    end
    

    上面的代码会把根地址 / 映射到 /static_pages/home 页面上,同时生成两个 URI 地址帮助方法,如下所示:

    root_path => '/'
    root_url  => 'http://localhost:3000/'
    

    我们应该按照代码 5.22 中注释的提示,删掉 public/index.html 文件,避免访问根目录时显示默认的首页(如图 1.3)。你当然可以直接把这个文件丢进垃圾桶,不过,如果使用 Git 做版本控制的话,可以使用 git rm 命令,删除文件的同时也告知 Git 系统做了这个删除操作:

    $ git rm public/index.html
    

    至此,所有静态页面的路由都设置好了,而且所有测试应该都可以通过了:

    $ bundle exec rspec spec/requests/static_pages_spec.rb
    

    下面,我们要在布局中插入这些链接。

    5.3.3 具名路由

    现在要在布局中使用上一小节设置的路由帮助方法,把 link_to 函数的第二个参数设为相应的具名路由。例如,要把

    <%= link_to "About", '#' %>
    

    改为

    <%= link_to "About", about_path %>
    

    其他链接以此类推。

    先从头部局部视图 _header.html.erb 开始,这个视图中包含了到“首页”和“帮助”页面的链接。既然要对头部视图做修改,顺便就按照网页的惯例为 LOGO 添加一个到“首页”的链接吧(参见代码 5.24)。

    代码 5.24 头部局部视图,包含一些链接
    app/views/layouts/_header.html.erb

    <header class="navbar navbar-fixed-top">
      <div class="navbar-inner">
        <div class="container">
          <%= link_to "sample app", root_path, id: "logo" %>
          <nav>
            <ul class="nav pull-right">
              <li><%= link_to "Home",    root_path %></li>
              <li><%= link_to "Help",    help_path %></li>
              <li><%= link_to "Sign in", '#' %></li>
            </ul>
          </nav>
        </div>
      </div>
    </header>
    

    第八章才会为“注册”页面设置具名路由,所以现在还是用占位符 # 代替该页面的地址。

    还有一个包含链接的文件是底部局部视图 _footer.html.erb,有到“关于”页面和“联系”页面的链接(参见代码 5.25)。

    代码 5.25 底部局部视图,包含一些链接
    app/views/layouts/_footer.html.erb

    <footer class="footer">
      <small>
        <a href="http://railstutorial.org/">Rails Tutorial</a>
        by Michael Hartl
      </small>
      <nav>
        <ul>
          <li><%= link_to "About",   about_path %></li>
          <li><%= link_to "Contact", contact_path %></li>
          <li><a href="http://news.railstutorial.org/">News</a></li>
        </ul>
      </nav>
    </footer>
    

    如此一来,第三章创建的所有静态页面的链接都加入布局了,以“关于”页面为例,输入 /about,就会进入网站的“关于”页面(如图 5.8)。

    顺便说一下,要注意,虽然我们没有编写测试检测布局中是否包含这些链接,不过如果没有设置路由的话,前面的测试也会失败,不信你可以把代码 5.21 中的路由注释掉再运行测试来验证一下。检查链接是否指向正确页面的测试代码参见 5.6 节

    about_page_styled

    图 5.8:“关于”页面 /about

    5.3.4 简化 RSpec 测试代码

    5.3.1 节中说过,静态页面的测试有点啰嗦,也有些重复(参见代码 5.20)。本节我们就会使用一些最新的 RSpec 特性,把测试变得简洁一些、优雅一些。

    先看一下如何改进下面的代码:

    describe "Home page" do
    
      it "should have the h1 'Sample App'" do
        visit root_path
        page.should have_selector('h1', text: 'Sample App')
      end
    
      it "should have the base title" do
        visit root_path
        page.should have_selector('title',
                          text: "Ruby on Rails Tutorial Sample App")
      end
    
      it "should not have a custom page title" do
        visit root_path
        page.should_not have_selector('title', text: '| Home')
      end
    end
    

    我们注意到,三个测试用例都访问了根地址,使用 before 块可以消除这个重复:

    describe "Home page" do
      before { visit root_path }
    
      it "should have the h1 'Sample App'" do
        page.should have_selector('h1', text: 'Sample App')
      end
    
      it "should have the base title" do
        page.should have_selector('title',
                          text: "Ruby on Rails Tutorial Sample App")
      end
    
      it "should not have a custom page title" do
        page.should_not have_selector('title', text: '| Home')
      end
    end
    

    上面的代码使用

    before { visit root_path }
    

    在每个测试用例运行之前访问根地址。(before 方法还可以使用别名 before(:each) 调用。)

    还有个代码在每个用例中都出现了,我们使用了

    it "should have the h1 'Sample App'" do
    

    同时还使用了

    page.should have_selector('h1', text: 'Sample App')
    

    二者虽然形式不同,要表达的意思却是相同的。而且两个用例都引用了 page 变量。我们可以告诉 RSpec,page 就是要测试的对象(subject),这样就可以避免多次使用 page

    subject { page }
    

    然后再使用 it 方法的另一种形式,把测试代码和描述文本合二为一:

    it { should have_selector('h1', text: 'Sample App') }
    

    因为指明了 subject { page },所以调用 should 时就会自动使用 Capybara 提供的 page 变量(参见 3.2.1 节)。

    使用这些技巧可以把“首页”的测试变得简洁一些:

      subject { page }
    
      describe "Home page" do
        before { visit root_path }
    
        it { should have_selector('h1', text: 'Sample App') }
        it { should have_selector 'title',
                            text: "Ruby on Rails Tutorial Sample App" }
        it { should_not have_selector 'title', text: '| Home' }
      end
    

    这样代码看起来就舒服多了,不过标题的测试还有点长。其实,代码 5.20 中大多数标题都是这样的长标题:

    "Ruby on Rails Tutorial Sample App | About"
    

    3.5 节的练习题建议定义一个 base_title 变量,再使用字符串插值来消除这个重复(参见代码 3.30)。我们可以更进一步,定义一个和代码 4.2 中 full_title 类似的方法。

    为此我们要新建 spec/support 文件夹,然后在其中新建 RSpec 通用函数文件 utilities.rb(参见代码 5.26)。

    代码 5.26 RSpec 通用函数文件,包含 full_title 方法
    spec/support/utilities.rb

    def full_title(page_title)
      base_title = "Ruby on Rails Tutorial Sample App"
      if page_title.empty?
        base_title
      else
        "#{base_title} | #{page_title}"
      end
    end
    

    其实这就是代码 4.2 中那个帮助方法的复制,不过,定义两个独立的方法可以捕获标题公共部分中的错误,其实这样也不太靠得住,更好的(也更强大的)方法是直接测试原来那个 full_title 帮助方法,参见 5.6 节中的练习。

    RSpec 会自动加载 spec/support 目录中的文件,所以我们就可以按照如下的方式编写“首页”的测试:

      subject { page }
    
      describe "Home page" do
        before { visit root_path }
    
        it { should have_selector('h1',    text: 'Sample App') }
        it { should have_selector('title', text: full_title('')) }
      end
    

    下面我们要用类似“首页”的方法来简化“帮助”页面、“关于”页面和“联系”页面的测试了,结果如代码 5.27 所示。

    代码 5.27 简化后的静态页面测试
    spec/requests/static_pages_spec.rb

    require 'spec_helper'
    
    describe "Static pages" do
    
      subject { page }
    
      describe "Home page" do
        before { visit root_path }
    
        it { should have_selector('h1',    text: 'Sample App') }
        it { should have_selector('title', text: full_title('')) }
        it { should_not have_selector 'title', text: '| Home' }
      end
    
      describe "Help page" do
        before { visit help_path }
    
        it { should have_selector('h1',    text: 'Help') }
        it { should have_selector('title', text: full_title('Help')) }
      end
    
      describe "About page" do
        before { visit about_path }
    
        it { should have_selector('h1',    text: 'About') }
        it { should have_selector('title', text: full_title('About Us')) }
      end
    
      describe "Contact page" do
        before { visit contact_path }
    
        it { should have_selector('h1',    text: 'Contact') }
        it { should have_selector('title', text: full_title('Contact')) }
      end
    end
    

    现在应该验证一下测试代码是否还可以通过:

    $ bundle exec rspec spec/requests/static_pages_spec.rb
    

    代码 5.27 中的 RSpec 测试比代码 5.20 简化多了,其实,还可以变得更简洁,详见 5.6 节。在示例程序接下来的开发过程中,只要可以,我们都会使用这种简洁的方式。

    5.4 用户注册:第一步

    为了完结本章的目标,本节我们要设置“注册”页面的路由,为此要创建第二个控制器。这是允许用户注册最重要的一步,用户模型会在第六章构建,整个注册功能则会在第七章完成。

    5.4.1 Users 控制器

    创建第一个控制器 StaticPages 是很久以前的事了,还是在 3.1.2 节中。现在我们要创建第二个了,Users 控制器。和之前一样,我们使用 generate 命令创建所需的控制器骨架,包含用户注册页面所需的动作。遵照 Rails 的 REST 架构约定,我们把这个动作命名为 new,将其传递给 generate controller 就可以自动创建这个动作了(参见代码 5.28)。

    代码 5.28 生成 Users 控制器(包含 new 动作)

    $ rails generate controller Users new --no-test-framework
          create  app/controllers/users_controller.rb
           route  get "users/new"
          invoke  erb
          create    app/views/users
          create    app/views/users/new.html.erb
          invoke  helper
          create    app/helpers/users_helper.rb
          invoke  assets
          invoke    coffee
          create      app/assets/javascripts/users.js.coffee
          invoke    scss
          create      app/assets/stylesheets/users.css.scss
    

    这个命令会创建 Users 控制器,还有其中的 new 动作(参见代码 5.29)和一个占位用的视图文件(参见代码 5.30)。

    代码 5.29 默认生成的 Users 控制器,包含 new 动作
    app/controllers/users_controller.rb

    class UsersController < ApplicationController
      def new
      end
    
    end
    

    代码 5.30 默认生成的 new 动作视图
    app/views/users/new.html.erb

    <h1>Users#new</h1>
    <p>Find me in app/views/users/new.html.erb</p>
    

    5.4.2 “注册”页面的 URI 地址

    5.4.1 节中生成的代码会在 /users/new 地址上对应一个页面,不过如表格 5.1所示,我们希望“注册”页面的地址是 /signup。为此,和 5.3 节一样,首先要编写集成测试,可以通过下面的命令生成:

    $ rails generate integration_test user_pages
    

    然后,按照代码 5.27 中静态页面测试代码的形式,我们要编写测试检测“注册”页面中是否有 h1title 标签,如代码 5.31 所示。

    代码 5.31 Users 控制器的测试代码,包含“注册”页面的测试用例
    spec/requests/user_pages_spec.rb

    require 'spec_helper'
    
    describe "User pages" do
    
      subject { page }
    
      describe "signup page" do
        before { visit signup_path }
    
        it { should have_selector('h1',    text: 'Sign up') }
        it { should have_selector('title', text: full_title('Sign up')) }
      end
    end
    

    和之前一样,可以执行 rspec 命令运行测试:

    $ bundle exec rspec spec/requests/user_pages_spec.rb
    

    不过有一点要知道,你还可以指定整个目录来运行所有的 request 测试:

    $ bundle exec rspec spec/requests/
    

    同理,你可能还想知道怎么运行全部测试:

    $ bundle exec rspec spec/
    

    为了测试全面,在本书后续内容中,我们一般都会使用这个命令运行所有的测试。顺便说一下,你要知道,你也可以使用 Rake 的 spec 任务运行测试(你可能见过其他人这样使用):

    $ bundle exec rake spec
    

    (事实上,你可以只输入 rake,因为 rake 的默认任务就是运行测试。)

    我们已经为 Users 控制器生成了 new 动作,如要测试通过,需要正确设置路由,还要有相应内容的视图文件。我们按照代码 5.21 的方式,为“注册”页面加入 match '/signup' 路由设置(参见代码 5.32)。

    代码 5.32 “注册”页面的路由设置
    config/routes.rb

    SampleApp::Application.routes.draw do
      get "users/new"
    
      root to: 'static_pages#home'
    
      match '/signup',  to: 'users#new'
    
      match '/help',    to: 'static_pages#help'
      match '/about',   to: 'static_pages#about'
      match '/contact', to: 'static_pages#contact'
      .
      .
      .
    end
    

    注意,我们保留了 get "users/new" 设置,这是控制器生成命令(代码 5.28)自动添加的路由,如要路由可用,这个设置还不能删除,不过这不符合 REST 约定(参见表格 2.2),会在 7.1.2 节删除。

    要让测试通过,视图中还要有相应的 h1title(参见代码 5.33)。

    代码 5.33 “注册”页面视图
    app/views/users/new.html.erb

    <% provide(:title, 'Sign up') %>
    <h1>Sign up</h1>
    <p>Find me in app/views/users/new.html.erb</p>
    

    现在,代码 5.31 中“注册”页面的测试应该可以通过了。下面要做的就是为“首页”中的注册按钮加上链接。和其他的具名路由一样,match '/signup' 会生成 signup_path 方法,用来链接到“注册”页面(参见代码 5.34)。

    代码 5.34 把按钮链接到“注册”页面
    app/views/static_pages/home.html.erb

    <div class="center hero-unit">
      <h1>Welcome to the Sample App</h1>
    
      <h2>
        This is the home page for the
        <a href="http://railstutorial.org/">Ruby on Rails Tutorial</a>
        sample application.
      </h2>
    
      <%= link_to "Sign up now!", signup_path, class: "btn btn-large btn-primary" %>
    </div>
    
    <%= link_to image_tag("rails.png", alt: "Rails"), 'http://rubyonrails.org/' %>
    

    至此,除了没有设置“登录/退出”路由之外(第八章会实现),我们已经完成了添加链接和设置路由的任务。注册用户的页面(/signup)如图 5.9 所示。

    new_signup_page_bootstrap

    图 5.9:“注册”页面 /signup

    现在测试应该可以通过了:

    $ bundle exec rspec spec/
    

    5.5 小结

    本章,我们为应用程序定义了一些样式,也设置了一些路由。本书剩下的内容会不断的充实这个示例程序:先添加用户注册、登录和退出的功能,然后实现微博功能,最后是关注用户功能。

    如果使用 Git 的话,现在你应该把本章所做的改动合并到主分支中:

    $ git add .
    $ git commit -m "Finish layout and routes"
    $ git checkout master
    $ git merge filling-in-layout
    

    还可以把代码推送到 GitHub 上:

    $ git push
    

    最后,你可以把应用程序部署到 Heroku:

    $ git push heroku
    

    然后在“生产环境”中就得到了一个可以运行的示例程序:

    $ heroku open
    

    如果遇到问题,运行

    $ heroku logs
    

    试着使用 Heroku 的日志文件排错。

    5.6 练习

    1. 测试静态页面的代码 5.27 已经简化了,但还有一些重复的地方。RSpec 提供了一种名为“共享用例(shared example)”的辅助功能,可以消除这些重复。按照代码 5.35 的形式,添加没有编写的“帮助”页面、“关于”页面和“联系”页面的测试。注意,代码 3.30 中用到的 let 方法只要需要就会用指定的值创建一个局部变量(例如,引用这个变量时),相比之下,实例变量只在赋值时才被创建。
    2. 或许你已经注意到了,对布局中链接的测试,只测试了路由设置,而没有测试链接是否指向了正确的页面。实现这个测试的方法之一是使用 RSpec 集成测试中的 visitclick_link 函数。加入代码 5.36 中的测试来验证一下链接的地址是否正确。
    3. 不要使用代码 5.26 中的 full_title 方法,另外编写一个用例,测试原来那个帮助方法,可参照代码 5.37。(需要新建 spec/helpers 目录和 application_helper_spec.rb 文件。)然后用代码 5.38 中的代码将其引入(require)测试。运行测试验证一下新代码是否可以正常使用。注意:代码 5.37 用到了正则表达式(regular expression),6.2.4 节会做介绍。(感谢 Alex Chaffee 的建议,并提供了本题用到的代码。)

    代码 5.35 用 RSpec “共享用例”来消除重复
    spec/requests/static_pages_spec.rb

    require 'spec_helper'
    
    describe "Static pages" do
    
      subject { page }
    
      shared_examples_for "all static pages" do
        it { should have_selector('h1',    text: heading) }
        it { should have_selector('title', text: full_title(page_title)) }
      end
    
      describe "Home page" do
        before { visit root_path }
        let(:heading)    { 'Sample App' }
        let(:page_title) { '' }
    
        it_should_behave_like "all static pages"
        it { should_not have_selector 'title', text: '| Home' }
      end
    
      describe "Help page" do
        .
        .
        .
      end
    
      describe "About page" do
        .
        .
        .
      end
    
      describe "Contact page" do
        .
        .
        .
      end
    end
    

    代码 5.36 测试布局中的链接
    spec/requests/static_pages_spec.rb

    require 'spec_helper'
    
    describe "Static pages" do
      .
      .
      .
      it "should have the right links on the layout" do
        visit root_path
        click_link "About"
        page.should have_selector 'title', text: full_title('About Us')
        click_link "Help"
        page.should # fill in
        click_link "Contact"
        page.should # fill in
        click_link "Home"
        click_link "Sign up now!"
        page.should # fill in
        click_link "sample app"
        page.should # fill in
      end
    end
    

    代码 5.37full_title 帮助方法的测试
    spec/helpers/application_helper_spec.rb

    require 'spec_helper'
    
    describe ApplicationHelper do
    
      describe "full_title" do
        it "should include the page title" do
          full_title("foo").should =~ /foo/
        end
    
        it "should include the base title" do
          full_title("foo").should =~ /^Ruby on Rails Tutorial Sample App/
        end
    
        it "should not include a bar for the home page" do
          full_title("").should_not =~ /\|/
        end
      end
    end
    

    代码 5.38 使用一个简单的引用代替测试中的 full_title 方法
    spec/support/utilities.rb

    include ApplicationHelper
    
    1. 感谢读者 Colm Tuite 使用 Bootstrap 重写了本书原来的示例程序;
    2. 本书中的所有构思图都是通过 Mockingbird这个在线应用制作的;
    3. 这些 class 和 Ruby 的类一点关系都没有;
    4. 你大概注意到了 img 标签的格式,不是 <img>...</img> 而是 <img ... />。这样的标签叫做自关闭标签;
    5. 在 asset pipeline 中当然也可以使用 LESS,详见 less-rails-bootstrap gem
    6. 很多 Rails 程序员都会使用 shared 目录存放需要在不同视图中共用的局部视图。我倾向于在 shared 目录中存放辅助的局部视图,而把每个页面中都会用到的局部视图放在layouts 目录中。 (我们会在第七章创建 shared 目录。)在我看来,这样用比较符合逻辑,不过,都放在 shared 目录里对运作没有影响;
    7. 你可能想知道为什么要使用 footer 标签和 .footer class。理由就是,这样的标签对于人类来说更容易理解,而那个 class 是因为 Bootstrap 里在用,所以不得不用。如果愿意的话,用 div 标签代替 footer 也没什么问题;
    8. 本节架构的依据是 Michael Erasmus 的《The Rails 3 Asset Pipeline in (about) 5 Minutes》一文。更多内容,请阅读 Rails 指南中的《Asset Pipeline》;
    9. Sass 仍然支持较早的 .sass 格式,这个格式相对来说更简洁,花括号更少,但是对现存项目不太友好,已经熟悉 CSS 的人学习难度也相对更大。