Ruby on Rails 教程

Ruby on Rails Tutorial 原书第 2 版(涵盖 Rails 4)

第 5 章 完善布局

  1. 5.1 添加一些结构
  2. 5.1.1 网站导航
  3. 5.1.2 Bootstrap 和自定义的 CSS
  4. 5.1.3 局部视图
  5. 5.2 Sass 和 asset pipeline
  6. 5.2.1 Asset pipeline
  7. 5.2.2 句法强大的样式表
  8. 5.3 布局中的链接
  9. 5.3.1 路由测试
  10. 5.3.2 Rails 路由
  11. 5.3.3 具名路由
  12. 5.3.4 简化 RSpec 测试代码
  13. 5.4 用户注册:第一步
  14. 5.4.1 Users 控制器
  15. 5.4.2 “注册”页面的 URL 地址
  16. 5.5 小结
  17. 5.6 练习

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

home page mockup bootstrap

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

5.1 添加一些结构

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

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

和之前一样,如果你使用 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",
                                           "data-turbolinks-track" => true %>
    <%= javascript_include_tag "application", "data-turbolinks-track" => true %>
    <%= 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 navbar-inverse">
      <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>

我们从上往下看一下代码 5.1 中新添加的元素。3.3.2 节简单的介绍过,Rails 4 默认会使用 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 navbar-inverse">
  <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-topnavbar-inverse,用空格分开:

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

所有的 HTML 元素都可以指定 class 和 id,它们不仅是个标注,在 CSS 样式中也有用(5.1.2 节)。class 和 id 之间主要的区别是,class 可以在同一个网页中多次使用,而 id 只能使用一次。这里的三个 class 在 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 创建了一个占位链接,指向第 7 章中创建的用户注册页面

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

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

layout no logo or custom css bootstrap rails 4

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

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

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

(注意,src 中并没有包含 images,Rails 会自动关联 images 文件夹中对应的图片。把静态资源文件放在 assets 文件夹中可以加快网页响应时间。)alt 属性的内容会在图片无法加载时显示,也会在针对视觉障碍人士的屏幕阅读器中显示。人们有时懒得加上 alt 属性,可是在 HTML 标准中却是必须的。幸运的是,Rails 默认会加上 alt 标签,如果你没有在调用 image_tag 时指定的话,Rails 就会使用图片的文件名(不包括扩展名)。本例中,我们自己设定了 alt 文本,显示一个首字母大写的“Rails”。

在 Rails 之前的版本中,每个 Rails 应用程序中都有 rails.png 这个图片,但在最新版中这个图片不会在执行 rails new 命令时生成,所以要手动从 Ruby on Rails 官网下载,地址为 http://rubyonrails.org/images/rails.png,保存到 app/assets/images/ 文件夹中。(或许还要新建这个文件夹,可以使用 mkdir 命令或者图形化文件管理工具。)因为在代码 5.2 中我们使用了 image_tag 帮助方法,所以 Rails 会通过 Asset Pipeline(参见 5.2 节)自动在这个文件夹中寻找所需的图片。

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

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'
ruby '2.0.0'

gem 'rails', '4.0.4'
gem 'bootstrap-sass', '2.3.2.0'
.
.
.

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

$ bundle install

然后重启 Web 服务器,改动才能在应用程序中生效。(在大多数系统中可以使用 Ctrl-C 结束服务器,然后再执行 rails server 命令。)最后,对 Rails 4 来说,我们还要在 config/application.rb 中添加一行代码,让 bootstrap-sass 这个 gem 和 Asset Pipeline 兼容,如代码 5.4 所示。

代码 5.4:添加一行代码,兼容 Asset Pipeline

config/application.rb

require File.expand_path('../boot', __FILE__)
.
.
.
module SampleApp
  class Application < Rails::Application
    .
    .
    .
    config.assets.precompile += %w(*.png *.jpg *.jpeg *.gif)
  end
end

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

app/assets/stylesheets/custom.css.scss

文件存放的目录和文件名都很重要。其中目录

app/assets/stylesheets
sample app only bootstrap 40

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

是 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.5 所示。

代码 5.5:引入 Bootstrap

app/assets/stylesheets/custom.css.scss

@import "bootstrap";

这行代码会引入整个 Bootstrap CSS 框架,结果如图 5.3 所示。(或许你要通过 Ctrl-C 来重启服务器。还有一点要注意,截图中使用的是 Bootstrap 2.0,而本教程用的是 Bootstrap 2.3,所以外观可能有所不同,不过无需过分担心。)可以看到,文本的位置还不是很合适,LOGO 也没有任何样式,不过颜色搭配和注册按钮看起来还不错。

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

sample app universal 40

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

代码 5.6:添加全站使用的 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;
}

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

body {
  padding-top: 60px;
}

把页面的上内边距设为 60 像素。我们在 header 标签上指定了 navbar-fixed-top class,Bootstrap 就把这个导航条固定在页面的顶部。所以页面的上内边距会把主内容区和导航条隔开一段距离。(导航条的颜色在 Bootstrap 2.0 和 2.1 之间是不一样的,所以我们要加入 navbar-inverse class,把亮色变暗。)下面的 CSS 规则:

.center {
  text-align: center;
}

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

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

sample app typography 40

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

代码 5.7:添加一些精美的文字排版样式

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.2em;
  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”的网站 LOGO 添加一些样式。代码 5.8 中的 CSS 样式会把文字变成全大写字母,还修改了文字大小、颜色和位置。(我们使用的是 id,因为我们希望 LOGO 在页面中只出现一次,不过你也可以使用 class。)

sample app logo 40

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

代码 5.8:添加网站 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.8 中的样式效果如图 5.6 所示。

5.1.3 局部视图

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

代码 5.9:定义了 HTML shim 和头部局部视图之后的网站布局

app/views/layouts/application.html.erb

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

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

<%= render 'layouts/shim' %>

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

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

代码 5.10: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.11 所示,然后再次调用 render 把这个局部视图插入布局中。

代码 5.11:网站头部的局部视图

app/views/layouts/_header.html.erb

<header class="navbar navbar-fixed-top navbar-inverse">
  <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.12)。7

代码 5.12:网站底部的局部视图

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.13。)

代码 5.13:网站的布局,包含底部局部视图

app/views/layouts/application.html.erb

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

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

代码 5.14:添加底部所需的 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;
}

5.2 Sass 和 asset pipeline

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

5.2.1 Asset pipeline

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

site with footer bootstrap 40

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

资源目录

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

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

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

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

  • 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.15)。

代码 5.15:应用程序的样式表清单文件

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.6 中,定义了 .center.center h1 两个样式:

.center {
  text-align: center;
}

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

使用 Sass 可将其改写成

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

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

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

#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.14 中的底部样式上,转换后的样式如下:

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.14 是个不错的练习,转换之后你应该验证一下 CSS 是否还能正常使用。

变量

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

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.16 所示。这段代码中使用了 Sass 形式的颜色变量(参照 Bootstrap 变量页面中定义的 LESS 形式的颜色变量)和内置的颜色名称(例如,white 代表 #fff10。请特别注意一下 footer 标签样式明显的改进。

代码 5.16:使用嵌套和变量转换后的 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.2em;
  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.16 只用到了最主要的功能,这是个好的开端。更多功能请查看 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 所示。本章完结之前除了最后一个链接之外,其他的链接都会设定好。(第 8 章会添加最后一个。)

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

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

继续之前,让我们先添加一个“联系”页面(第 3 章的一个练习题),测试如代码 5.17 所示,形式和代码 3.19 差不多。

代码 5.17:“联系”页面的测试

spec/requests/static_pages_spec.rb

require 'spec_helper'

describe "Static pages" do
  .
  .
  .
  describe "Contact page" do

    it "should have the content 'Contact'" do
      visit '/static_pages/contact'
      expect(page).to have_content('Contact')
    end

    it "should have the title 'Contact'" do
      visit '/static_pages/contact'
      expect(page).to have_title("Ruby on Rails Tutorial Sample App | Contact")
    end
  end
end

为保证代码 5.17 中测试失败,我们要把页面底部的“联系”页面链接注释掉,如代码 5.18 所示。

代码 5.18:注释掉页面底部的“联系”页面链接

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>

从代码 5.18 中可以得知在 ERb 中注释代码的方法,即在百分号后面加上 Ruby 中的注释符号。

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

现在,代码 5.17 中的测试应该是失败的:

$ bundle exec rspec spec/requests/static_pages_spec.rb

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

代码 5.19:添加“联系”页面的路由设置

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.20:添加“联系”页面对应的动作

app/controllers/static_pages_controller.rb

class StaticPagesController < ApplicationController
  .
  .
  .
  def contact
  end
end

代码 5.21:“联系”页面的视图

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.22)。不过这样做还不完美,如果不小心删除了代码 5.21 中的 h1 标签,测试是无法检测到的。要解决这个问题,就要编写一个针对 h1 标签内容的测试,测试的编写留作练习(参见 5.6 节)。

代码 5.22:去掉底部“联系”页面链接的注释

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>

5.3.1 路由测试

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

visit '/static_pages/about'

修改为

visit about_path

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

代码 5.23:具名路由测试

spec/requests/static_pages_spec.rb

require 'spec_helper'

describe "Static pages" do

  describe "Home page" do

    it "should have the content 'Sample App'" do
      visit root_path
      expect(page).to have_content('Sample App')
    end

    it "should have the base title" do
      visit root_path
      expect(page).to have_title("Ruby on Rails Tutorial Sample App")
    end

    it "should not have a custom page title" do
      visit root_path
      expect(page).not_to have_title('| Home')
    end
  end

  describe "Help page" do

    it "should have the content 'Help'" do
      visit help_path
      expect(page).to have_content('Help')
    end

    it "should have the title 'Help'" do
      visit help_path
      expect(page).to have_title("Ruby on Rails Tutorial Sample App | Help")
    end
  end

  describe "About page" do

    it "should have the content 'About Us'" do
      visit about_path
      expect(page).to have_content('About Us')
    end

    it "should have the title 'About Us'" do
      visit about_path
      expect(page).to have_title("Ruby on Rails Tutorial Sample App | About Us")
    end
  end

  describe "Contact page" do

    it "should have the content 'Contact'" do
      visit contact_path
      expect(page).to have_content('Contact')
    end

    it "should have the title 'Contact'" do
      visit contact_path
      expect(page).to have_title("Ruby on Rails Tutorial Sample App | Contact")
    end
  end
end

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

$ bundle exec rspec spec/requests/static_pages_spec.rb

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

5.3.2 Rails 路由

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

定义具名路由,要把

get 'static_pages/help'

修改为

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

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

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

代码 5.24:静态页面的路由

config/routes.rb

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

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

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

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

get 'static_pages/about'

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

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

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

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

$ bundle exec rspec spec/requests/static_pages_spec.rb

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

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

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

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

代码 5.25:注释掉的根路由设置说明

config/routes.rb

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

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

代码 5.26:添加根地址的路由设置

config/routes.rb

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

上面的代码会把根地址 / 映射到 /static_pages/home 页面上,也就是说 http://localhost:3000/ 中的内容和图 3.1 中的 Rails 默认页面不一样了,同时还生成了两个 URL 地址帮助方法,如下所示:

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

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

$ 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.27)。

代码 5.27:头部局部视图,包含一些链接

app/views/layouts/_header.html.erb

<header class="navbar navbar-fixed-top navbar-inverse">
  <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>

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

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

代码 5.28:底部局部视图,包含一些链接

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>

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

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

about page styled

图 5.8:“关于”页面 /about

5.3.4 简化 RSpec 测试代码

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

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

describe "Home page" do

  it "should have the content 'Sample App'" do
    visit root_path
    expect(page).to have_content('Sample App')
  end

  it "should have the base title" do
    visit root_path
    expect(page).to have_title("Ruby on Rails Tutorial Sample App")
  end

  it "should not have a custom page title" do
    visit root_path
    expect(page).not_to have_title('| Home')
  end
end

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

describe "Home page" do
  before { visit root_path }

  it "should have the content 'Sample App'" do
    expect(page).to have_content('Sample App')
  end

  it "should have the base title" do
    expect(page).to have_title("Ruby on Rails Tutorial Sample App")
  end

  it "should not have a custom page title" do
    expect(page).not_to have_title('| Home')
  end
end

上面的代码使用

before { visit root_path }

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

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

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

同时还使用了

expect(page).to have_content('Sample App')

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

subject { page }

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

it { should have_content('Sample App') }

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

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

  subject { page }

  describe "Home page" do
    before { visit root_path }

    it { should have_content('Sample App') }
    it { should have_title("Ruby on Rails Tutorial Sample App") }
    it { should_not have_title('| Home') }
  end

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

"Ruby on Rails Tutorial Sample App | About"

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

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

代码 5.29: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_content('Sample App') }
    it { should have_title(full_title('')) }
  end

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

代码 5.30:简化后的静态页面测试

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_content('Sample App') }
    it { should have_title(full_title('')) }
    it { should_not have_title('| Home') }
  end

  describe "Help page" do
    before { visit help_path }

    it { should have_content('Help') }
    it { should have_title(full_title('Help')) }
  end

  describe "About page" do
    before { visit about_path }

    it { should have_content('About') }
    it { should have_title(full_title('About Us')) }
  end

  describe "Contact page" do
    before { visit contact_path }

    it { should have_content('Contact') }
    it { should have_title(full_title('Contact')) }
  end
end

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

$ bundle exec rspec spec/requests/static_pages_spec.rb

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

5.4 用户注册:第一步

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

5.4.1 Users 控制器

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

代码 5.31:生成 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.32)和一个占位用的视图文件(参见代码 5.33)。

代码 5.32:默认生成的 Users 控制器,包含 new 动作

app/controllers/users_controller.rb

class UsersController < ApplicationController
  def new
  end

end

代码 5.33:默认生成的 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 “注册”页面的 URL 地址

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

$ rails generate integration_test user_pages

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

代码 5.34: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_content('Sign up') }
    it { should have_title(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 的默认任务就是运行测试。)现在唯一的问题是,运行测试会报错,因为还没有准备好“测试数据库”,我们会在 6.2.1 节 中详细介绍。

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

代码 5.35:“注册”页面的路由设置

config/routes.rb

SampleApp::Application.routes.draw do
  get "users/new"

  root to: 'static_pages#home'
  match '/signup',  to: 'users#new',            via: 'get'
  match '/help',    to: 'static_pages#help',    via: 'get'
  match '/about',   to: 'static_pages#about',   via: 'get'
  match '/contact', to: 'static_pages#contact', via: 'get'
  .
  .
  .
end

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

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

代码 5.36:“注册”页面视图

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

代码 5.37:把按钮链接到“注册”页面

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/' %>

至此,除了没有设置“登录/退出”路由之外(第 8 章会实现),我们已经完成了添加链接和设置路由的任务。注册用户的页面(/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 的日志文件排错。同时确保你已经做了代码 2.17 所示的改动。

5.6 练习

  1. 5.3 节我们得知,RSpec 的 have_content 匹配器有时匹配的范围太宽了,所以我们必须注释掉底部的“联系”页面链接才能避免误伤(参见代码 5.18)。为了解决这个问题,代码 5.38 使用了 Capybara 提供的 have_selector 方法,可以直接用来测试页面中是否存在指定的 HTML 标签。我们可以先把代码 5.21 中的 h1 标签删掉,然后再加进来,验证一下代码 5.38 中的测试即使不注释掉底部的链接测试也会按预期失败。

  2. 测试静态页面的代码 5.30 已经简化了,但还有一些重复的地方。RSpec 提供了一种名为“共享用例(shared example)”的辅助功能,可以消除这些重复。按照代码 5.39 的形式,添加没有编写的“帮助”页面、“关于”页面和“联系”页面的测试。注意,代码 3.31 中用到的 let 方法只要需要就会用指定的值创建一个局部变量(例如,引用这个变量时),相比之下,实例变量只在赋值时才被创建。(代码 5.39 也用到了 代码 5.38 中的 have_selector。)

  3. 或许你已经注意到了,对布局中链接的测试,只测试了路由设置,而没有测试链接是否指向了正确的页面。实现这个测试的方法之一是使用 RSpec 集成测试中的 visitclick_link 函数。加入代码 5.40 中的测试来验证一下链接的地址是否正确。

  4. 不要使用代码 5.29 中的 full_title 方法,另外编写一个用例,测试原来那个帮助方法,可参照代码 5.41。(需要新建 spec/helpers 目录和 application_helper_spec.rb 文件。)然后用代码 5.42 中的代码将其引入(require)测试。运行测试验证一下新代码是否可以正常使用。注意:代码 5.41 用到了正则表达式(regular expression),6.2.4 节会做介绍。(感谢 Alex Chaffee 的建议,并提供了本题用到的代码。)

代码 5.38:针对“联系”页面更有针对性的测试

spec/requests/static_pages_spec.rb

require 'spec_helper'

describe "Static pages" do
  .
  .
  .
  describe "Contact page" do
    before { visit contact_path }

    it { should have_selector('h1', text: 'Contact') }
    it { should have_title(full_title('Contact')) }
  end
end

代码 5.39:用 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_content(heading) }
    it { should have_title(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_title('| Home') }
  end

  describe "Help page" do
    before { visit help_path }
    let(:heading)    { 'Help' }
    let(:page_title) { 'Help' }
  end

  describe "About page" do
    .
    .
    .
  end

  describe "Contact page" do
    .
    .
    .
  end
end

代码 5.40:测试布局中的链接

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"
    expect(page).to have_title(full_title('About Us'))
    click_link "Help"
    expect(page).to # fill in
    click_link "Contact"
    expect(page).to # fill in
    click_link "Home"
    click_link "Sign up now!"
    expect(page).to # fill in
    click_link "sample app"
    expect(page).to # fill in
  end
end

代码 5.41:full_title 帮助方法的测试

spec/helpers/application_helper_spec.rb

require 'spec_helper'

describe ApplicationHelper do

  describe "full_title" do
    it "should include the page title" do
      expect(full_title("foo")).to match(/foo/)
    end

    it "should include the base title" do
      expect(full_title("foo")).to match(/^Ruby on Rails Tutorial Sample App/)
    end

    it "should not include a bar for the home page" do
      expect(full_title("")).not_to match(/\|/)
    end
  end
end

代码 5.42:使用一个简单的引用代替测试中的 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 目录中。 (我们会在第 7 章创建 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 的人学习难度也相对更大。

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