5.1 添加一些结构

本书介绍 Web 开发而不是 Web 设计,不过在一个看起来很简陋的应用中开发会让人提不起劲,所以本节要向布局中添加一些结构,再加入一些 CSS 实现基本的样式。除了使用自定义的 CSS 之外,我们还会使用由 Twitter 开发的开源 Web 设计框架 Bootstrap。我们会按照一定的方式组织代码——当布局文件中的内容变多以后,使用局部视图清理。

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

home page mockup 3rd edition图 5.1:演示应用首页的构思图

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

$ git checkout master
$ git checkout -b filling-in-layout

5.1.1 网站导航

在应用中添加链接和样式之前,我们先来修改网站的布局文件 application.html.erb(上一次见到是在代码清单 4.3 中),添加一些 HTML 结构。我们要添加一些区域,一些 CSS 类,以及导航条。布局文件的完整内容参见代码清单 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="//cdnjs.cloudflare.com/ajax/libs/html5shiv/r29/html5.min.js">
 </script>
 <![endif]-->
 </head>
 <body>
 <header class="navbar navbar-fixed-top navbar-inverse">
 <div class="container">
  <%= link_to "sample app", '#', id: "logo" %>
 <nav>
 <ul class="nav navbar-nav navbar-right">
 <li><%= link_to "Home",   '#' %></li>
 <li><%= link_to "Help",   '#' %></li>
 <li><%= link_to "Log in", '#' %></li>
 </ul>
 </nav>
 </div>
 </header>
 <div class="container">
  <%= yield %>
 </div>
 </body>
</html>

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

<!--[if lt IE 9]>
 <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/r29/html5.min.js">
 </script>
<![endif]-->

如下有点古怪的句法

<!--[if lt IE 9]>

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

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

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

header 标签表明这个元素应该放在页面的顶部。我们为 header 标签指定了三个 CSS 类,[3]分别为 navbarnavbar-fixed-topnavbar-inverse,类之间用空格分开:

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

所有 HTML 元素都可以指定类和 ID,它们不仅是标记,使用 CSS 编写样式时也有用(5.1.2 节)。类和 ID 之间主要的区别是,类可以在同一个网页中多次使用,而 ID 只能使用一次。这里的三个类在 Bootstrap 框架中都有特殊的意义。我们会在 5.1.2 节安装并使用 Bootstrap。

header 标签内部,有一个 div 标签:

<div class="container">

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

在这个 div 标签中有一些 ERb 代码:

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

这里使用 Rails 提供的 link_to 辅助方法创建链接(3.2.2 节直接使用 a 标签创建)。link_to 的第一个参数是链接文本,第二个参数是链接地址。在 5.3 节我们会使用“具名路由”(named route)指定链接地址,现在暂且使用 Web 开发中经常使用的占位符 #。第三个参数可选,是一个哈希,本例使用这个参数为 LOGO 添加一个 CSS ID——logo。(其他三个链接没有使用这个哈希参数,没关系,因为这个参数是可选的。)Rails 辅助方法的参数经常这样使用哈希,让我们仅使用 Rails 的辅助方法就能灵活添加 HTML 属性。

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

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

nav 标签以前是不需要的,它的目的是明确表明这些链接是导航。ul 标签中的 navnavbar-navnavbar-right 三个类在 Bootstrap 中有特殊的意义,5.1.2 节引入 Bootstrap 的 CSS 之后会自动实现特殊的样式。在浏览器中审查导航元素,你会发现 Rails 处理布局文件并执行其中的 ERb 代码后,生成的列表如下所示:[4]

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

这就是返回给浏览器的文本。

布局文件的最后一部分是一个 div 标签,用于显示主内容:

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

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

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

代码清单 5.2:“首页”视图,包含一个到注册页面的链接

app/views/static_pages/home.html.erb

<div class="center jumbotron">
 <h1>Welcome to the Sample App</h1>

 <h2>
 This is the home page for the
 <a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
 sample application.
 </h2>

  <%= link_to "Sign up now!", '#', class: "btn btn-lg btn-primary" %>
</div>

<%= link_to image_tag("rails.png", alt: "Rails logo"),
            'http://rubyonrails.org/' %>

其中第一个 link_to 创建一个占位链接,指向第 7 章创建的用户注册页面:

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

div 标签的 CSS 类 jumbotron 在 Bootstrap 中有特殊的意义,注册按钮的 btnbtn-lgbtn-primary 也是一样。

第二个 link_to 用到了 image_tag 辅助方法,第一个参数是图片的路径;第二个参数可选,是一个哈希,本例中这个哈希参数使用一个符号键设置图片的 alt 属性。为了能正确显示图片,应用中必须有个名为 rails.png 的图片。这个图片可以从本书的网站中下载,地址是 http://railstutorial-china.org/assets/http://railstutorial-china.org/book/images/rails.png。下载后把这个图片放在 app/assets/http://railstutorial-china.org/book/images/ 文件夹中。如果使用云端 IDE 或 Unix 类系统,可以使用 curl 完成这个操作,如下所示:[5]

$ curl -O http://railstutorial-china.org/assets/http://railstutorial-china.org/book/images/rails.png
$ mv rails.png app/assets/http://railstutorial-china.org/book/images/

因为我们使用了 image_tag 辅助方法,所以 Rails 会使用 Asset Pipeline(5.2 节)自动在 app/assets/http://railstutorial-china.org/book/images/ 文件夹中寻找图片。

为了更好地理解 image_tag,我们来看一下生成的 HTML:[6]

<img alt="Rails logo" src="/assets/rails-9308b8f92fea4c19a3a0d8385b494526.png" />

其中,字符串 9308b8f92fea4c19a3a0d8385b494526(在你的系统中得到的字符串可能不一样)由 Rails 添加,目的是确保文件名的唯一性,如果文件变化了,让浏览器重新加载文件(而不是从缓存中读取)。注意,src 属性中并没有 images,使用的是静态文件(图片,JavaScript,CSS 等)共用的 assets 文件夹。在服务器中,Rails 会把 assets 文件夹中的图片和 app/assets/images 文件夹中的文件对应起来。这么做是为了让浏览器觉得所有静态文件都在同一个文件夹中,有利于快速伺服。alt 属性的内容会在图片无法加载时显示,例如在针对视觉障碍人士的屏幕阅读器中。

layout no logo or custom css bootstrap 3rd edition图 5.2:还没添加 CSS 的首页

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

5.1.2 Bootstrap 和自定义的 CSS

前一节我们为很多 HTML 元素指定了 CSS 类,这样我们就可以使用 CSS 灵活的构建布局了。如前所述,其中很多类在 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 中所有必要的文件都可以在当前应用中使用。[7]

代码清单 5.3:把 bootstrap-sass 添加到 Gemfile
source 'https://rubygems.org'

gem 'rails',                '4.2.2'
gem 'bootstrap-sass',       '3.2.0.0'
.
.
.

和之前一样,运行 bundle install 安装 Bootstrap:

$ bundle install

rails generate 命令会自动为控制器生成一个单独的 CSS 文件,但很难使用正确的顺序引入这些样式,所以简单起见,本书会把所有 CSS 都放在一个文件夹中。为此,我们要先新建这个 CSS 文件:

$ touch app/assets/stylesheets/custom.css.scss

(使用 3.3.3 节用过的 touch 命令,你也可以使用其他方式。)其中文件夹的名字和文件扩展名都很重要。app/assets/stylesheets/ 文件夹是 Asset Pipeline 的一部分,其中所有的样式表都会引入 application.css 文件。文件名 custom.css.scss 中包含 .css,说明这是 CSS 文件,.scss 扩展名则说明这是“Sassy CSS”文件,Asset Pipeline 会使用 Sass 处理其中的内容。(5.2.2 节才会使用 Sass,不过加入这个扩展名才能发挥 bootstrap-sass gem 的作用。)

在这个 CSS 文件中,我们可以使用 @import 函数引入 Bootstrap(以及相关的 Sprockets 工具),如代码清单 5.4 所示。[8]

代码清单 5.4:添加 Bootstrap 的 CSS

app/assets/stylesheets/custom.css.scss

@import "bootstrap-sprockets";
@import "bootstrap";

这两行代码会引入整个 Bootstrap CSS 框架。然后重启 Web 服务器(先按 Ctrl-C 键,然后执行 rails server 命令),让这些改动生效,效果如图 5.3 所示。文本的位置还不合适,LOGO 也没有任何样式,不过颜色搭配和注册按钮看起来都不错。

sample app only bootstrap 3rd edition图 5.3:使用 Bootstrap CSS 后的演示应用

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

代码清单 5.5:添加全站使用的 CSS

app/assets/stylesheets/custom.css.scss

@import "bootstrap-sprockets";
@import "bootstrap";

/* universal */

body {
  padding-top: 60px;
}

section {
  overflow: auto;
}

textarea {
  resize: vertical;
}

.center {
  text-align: center;
}

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

sample app universal 3rd edition图 5.4:添加一些留白以及其他全局样式

注意,代码清单 5.5 中的 CSS 格式都是统一的。一般来说,CSS 规则通过类、ID、HTML 标签或者三者结合在一起来指代目标,然后在后面跟着一些样式声明。例如:

body {
  padding-top: 60px;
}

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

.center {
  text-align: center;
}

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

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

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

app/assets/stylesheets/custom.css.scss

@import "bootstrap-sprockets";
@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: #777;
}

p {
  font-size: 1.1em;
  line-height: 1.7em;
}

sample app typography 3rd edition图 5.5:添加一些排版样式

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

代码清单 5.7:添加网站 LOGO 的样式

app/assets/stylesheets/custom.css.scss

@import "bootstrap-sprockets";
@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;
}

#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 3rd edition图 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',
                                           '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>

在这段代码中,我们把 HTML shim 删掉,换成了一行代码,调用 Rails 的辅助方法 render

<%= render 'layouts/shim' %>

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

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

代码清单 5.9:HTML shim 局部视图

app/views/layouts/_shim.html.erb

<!--[if lt IE 9]>
 <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/r29/html5.min.js">
 </script>
<![endif]-->

类似地,我们可以把头部的内容移入局部视图,如代码清单 5.10 所示,然后再次调用 render 把这个局部视图插入布局中。(一般都要在文本编辑器中手动创建局部视图对应的文件。)

代码清单 5.10:网站头部的局部视图

app/views/layouts/_header.html.erb

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

现在我们已经知道怎么创建局部视图了,让我们来加入和头部对应的网站底部吧。你或许已经猜到了,我们会把这个局部视图命名为 _footer.html.erb,放在布局文件夹中,如代码清单 5.11 所示。[10]

代码清单 5.11:网站底部的局部视图

app/views/layouts/_footer.html.erb

<footer class="footer">
 <small>
 The <a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
 by <a href="http://www.michaelhartl.com/">Michael Hartl</a>
 </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",
                                           "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.13,效果如图 5.7 所示。

site with footer bootstrap 3rd edition图 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: #777;
}

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: 15px;
}