# 3.4 有點動態內容的頁面
我們已經為一些靜態頁面創建了動作和視圖,現在要稍微添加一些動態內容,根據所在的頁面不同而變化:我們要讓標題根據頁面的內容變化。改變標題到底算不算真正動態還有爭議,這么做能為[第 7 章](chapter7.html#sign-up)實現的真正動態內容打下基礎。
我們的計劃是修改首頁、“幫助”頁面和“關于”頁面,讓每頁顯示的標題都不一樣。為此,我們要在頁面的視圖中使用 `<title>` 標簽。大多數瀏覽器都會在瀏覽器窗口的頂部顯示標題中的內容,而且標題對“搜索引擎優化”(Search-Engine Optimization,簡稱 SEO)也有好處。我們要使用完整的“遇紅-變綠-重構”循環:先為頁面的標題編寫一些簡單的測試(**遇紅**),然后分別在三個頁面中添加標題(**變綠**),最后使用布局文件去除重復內容(重構)。本節結束時,三個靜態頁面的標題都會變成“<頁面的名字> | Ruby on Rails Tutorial Sample App”這種形式([表 3.2](#table-static-pages))。
`rails new` 命令會創建一個布局文件,不過現在最好不用。我們重命名這個文件:
```
$ mv app/views/layouts/application.html.erb layout_file # 臨時移動
```
在真實的應用中你不需要這么做,不過沒有這個文件能讓你更好地理解它的作用。
表 3.2:演示應用中基本上是靜態內容的頁面
| 頁面 | URL | 基本標題 | 變動部分 |
| --- | --- | --- | --- |
| 首頁 | /static_pages/home | `"Ruby on Rails Tutorial Sample App"` | `"Home"` |
| 幫助 | /static_pages/help | `"Ruby on Rails Tutorial Sample App"` | `"Help"` |
| 關于 | /static_pages/about | `"Ruby on Rails Tutorial Sample App"` | `"About"` |
## 3.4.1 測試標題(遇紅)
添加標題之前,我們要學習網頁的一般結構,如[代碼清單 3.21](#listing-html-structure) 所示。
##### 代碼清單 3.21:網頁一般的 HTML 結構
```
<!DOCTYPE html>
<html>
<head>
<title>Greeting</title>
</head>
<body>
<p>Hello, world!</p>
</body>
</html>
```
這段代碼的最頂部是“文檔類型聲明”(document type declaration,簡稱 doctype),告訴瀏覽器使用哪個 HTML 版本(本例使用 [HTML5](http://en.wikipedia.org/wiki/HTML5))[[8](#fn-8)]。隨后是 `head` 部分,包含一個 `title` 標簽,其中的內容是“Greeting”。然后是 `body` 部分,包含一個 `p` 標簽(段落),其中的內容是“Hello, world!”。(內容的縮進是可選的,HTML 不會特別對待空白,制表符和空格都會被忽略,但縮進可以讓文檔結構更清晰。)
我們要使用 `assert_select` 方法分別為[表 3.2](#table-static-pages) 中的每個標題編寫簡單的測試,合并到[代碼清單 3.13](#listing-about-test) 的測試中。`assert_select` 方法的作用是檢查有沒有指定的 HTML 標簽。這種方法有時也叫“選擇符”,從方法名可以看出這一點。[[9](#fn-9)]
```
assert_select "title", "Home | Ruby on Rails Tutorial Sample App"
```
這行代碼的作用是檢查有沒有 `<title>` 標簽,以及其中的內容是不是字符串“Home | Ruby on Rails Tutorial Sample App”。把這樣的代碼分別放到三個頁面的測試中,得到的結果如[代碼清單 3.22](#listing-title-tests) 所示。
##### 代碼清單 3.22:加入標題測試后的靜態頁面控制器測試 RED
test/controllers/static_pages_controller_test.rb
```
require 'test_helper'
class StaticPagesControllerTest < ActionController::TestCase
test "should get home" do
get :home
assert_response :success
assert_select "title", "Home | Ruby on Rails Tutorial Sample App" end
test "should get help" do
get :help
assert_response :success
assert_select "title", "Help | Ruby on Rails Tutorial Sample App" end
test "should get about" do
get :about
assert_response :success
assert_select "title", "About | Ruby on Rails Tutorial Sample App" end
end
```
(如果你覺得在標題中重復使用“Ruby on Rails Tutorial Sample App”不妥,可以看一下 [3.6 節](#mostly-static-pages-exercises)的練習。)
寫好測試之后,應該確認一下現在測試組件是失敗的(**RED**):
##### 代碼清單 3.23:**RED**
```
$ bundle exec rake test
3 tests, 6 assertions, 3 failures, 0 errors, 0 skips
```
## 3.4.2 添加頁面標題(變綠)
現在,我們要為每個頁面添加標題,讓前一節的測試通過。參照[代碼清單 3.21](#listing-html-structure) 中的 HTML 結構,把[代碼清單 3.9](#listing-custom-home-page) 中的首頁內容換成[代碼清單 3.24](#listing-home-view-full-html) 中的內容。
##### 代碼清單 3.24:具有完整 HTML 結構的首頁 RED
app/views/static_pages/home.html.erb
```
<!DOCTYPE html>
<html>
<head>
<title>Home | Ruby on Rails Tutorial Sample App</title>
</head>
<body>
<h1>Sample App</h1>
<p>
This is the home page for the
<a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
sample application.
</p>
</body>
</html>
```
修改之后,首頁如[圖 3.6](#fig-home-view-full-html) 所示。[[10](#fn-10)]
圖 3.6:添加標題后的首頁
然后使用類似的方式修改“幫助”頁面和“關于”頁面,得到的代碼如[代碼清單 3.25](#listing-help-view-full-html) 和[代碼清單 3.26](#listing-about-view-full-html) 所示。
##### 代碼清單 3.25:具有完整 HTML 結構的“幫助”頁面 RED
app/views/static_pages/help.html.erb
```
<!DOCTYPE html>
<html>
<head>
<title>Help | Ruby on Rails Tutorial Sample App</title>
</head>
<body>
<h1>Help</h1>
<p>
Get help on the Ruby on Rails Tutorial at the
<a href="http://www.railstutorial.org/#help">Rails Tutorial help
section</a>.
To get help on this sample app, see the
<a href="http://www.railstutorial.org/book"><em>Ruby on Rails
Tutorial</em> book</a>.
</p>
</body>
</html>
```
##### 代碼清單 3.26:具有完整 HTML 結構的“關于”頁面 GREEN
app/views/static_pages/about.html.erb
```
<!DOCTYPE html>
<html>
<head>
<title>About | Ruby on Rails Tutorial Sample App</title>
</head>
<body>
<h1>About</h1>
<p>
The <a href="http://www.railstutorial.org/"><em>Ruby on Rails
Tutorial</em></a> is a
<a href="http://www.railstutorial.org/book">book</a> and
<a href="http://screencasts.railstutorial.org/">screencast series</a>
to teach web development with
<a href="http://rubyonrails.org/">Ruby on Rails</a>.
This is the sample application for the tutorial.
</p>
</body>
</html>
```
現在,測試組件能通過了(**GREEN**):
##### 代碼清單 3.27:**GREEN**
```
$ bundle exec rake test
3 tests, 6 assertions, 0 failures, 0 errors, 0 skips
```
## 3.4.3 布局和嵌入式 Ruby(重構)
到目前為止,本節已經做了很多事情,我們使用 Rails 控制器和動作生成了三個可用的頁面,不過這些頁面中的內容都是純靜態的 HTML,沒有體現出 Rails 的強大之處。而且,代碼中有著大量重復:
* 頁面的標題幾乎(但不完全)是一模一樣的;
* 每個標題中都有“Ruby on Rails Tutorial Sample App”;
* 整個 HTML 結構在每個頁面都重復地出現了。
重復的代碼違反了很重要的“不要自我重復”(Don’t Repeat Yourself,簡稱 DRY)原則。本節要遵照 DRY 原則,去掉重復的代碼。最后,我們要運行前一節編寫的測試,確認顯示的標題仍然正確。
不過,去除重復的第一步卻是要增加一些代碼,讓頁面的標題看起來是一樣的。這樣我們就能更容易地去掉重復的代碼了。
在這個過程中,要在視圖中使用嵌入式 Ruby(Embedded Ruby)。既然首頁、“幫助”頁面和“關于”頁面的標題中有一個變動的部分,那我們就使用 Rails 提供的一個特別的函數 `provide`,在每個頁面中設定不同的標題。通過把 `home.html.erb` 視圖中標題的“Home”換成[代碼清單 3.28](#listing-home-view-erb-title) 所示的代碼,我們可以看一下這個函數的作用。
##### 代碼清單 3.28:標題中使用了嵌入式 Ruby 代碼的首頁視圖 GREEN
app/views/static_pages/home.html.erb
```
<% provide(:title, "Home") %> <!DOCTYPE html>
<html>
<head>
<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
</head>
<body>
<h1>Sample App</h1>
<p>
This is the home page for the
<a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
sample application.
</p>
</body>
</html>
```
在這段代碼中我們第一次使用了嵌入式 Ruby,或者簡稱 ERb。(現在你應該知道為什么 HTML 視圖文件的擴展名是 `.html.erb` 了。)ERb 是為網頁添加動態內容主要使用的模板系統。[[11](#fn-11)]下面的代碼
```
<% provide(:title, 'Home') %>
```
通過 `<% …? %>` 調用 Rails 中的 `provide` 函數,把字符串 `"Home"` 賦給 `:title`。[[12](#fn-12)]然后,在標題中,我們使用類似的符號 `<%= …? %>`,通過 Ruby 的 `yield` 函數把標題插入模板中:[[13](#fn-13)]
```
<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
```
(這兩種嵌入 Ruby 代碼的方式區別在于,`<% …? %>` 只**執行**其中的代碼;`<%= …? %>` 也會執行其中的代碼,而且會把執行的結果**插入**模板中。)最終得到的頁面和以前一樣,不過,現在標題中變動的部分通過 ERb 動態生成。
我們可以運行前一節編寫的測試確認一下——測試還能通過(**GREEN**):
##### 代碼清單 3.29:**GREEN**
```
$ bundle exec rake test
3 tests, 6 assertions, 0 failures, 0 errors, 0 skips
```
然后,按照相同的方式修改“幫助”([代碼清單 3.30](#listing-help-view-erb-title))和“關于”頁面([代碼清單 3.31](#listing-about-view-erb-title))。
##### 代碼清單 3.30:標題中使用了嵌入式 Ruby 代碼的“幫助”頁面視圖 GREEN
app/views/static_pages/help.html.erb
```
<% provide(:title, "Help") %> <!DOCTYPE html>
<html>
<head>
<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
</head>
<body>
<h1>Help</h1>
<p>
Get help on the Ruby on Rails Tutorial at the
<a href="http://www.railstutorial.org/#help">Rails Tutorial help
section</a>.
To get help on this sample app, see the
<a href="http://www.railstutorial.org/book"><em>Ruby on Rails
Tutorial</em> book</a>.
</p>
</body>
</html>
```
##### 代碼清單 3.31:標題中使用了嵌入式 Ruby 代碼的“關于”頁面視圖 GREEN
app/views/static_pages/about.html.erb
```
<% provide(:title, "About") %> <!DOCTYPE html>
<html>
<head>
<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
</head>
<body>
<h1>About</h1>
<p>
The <a href="http://www.railstutorial.org/"><em>Ruby on Rails
Tutorial</em></a> is a
<a href="http://www.railstutorial.org/book">book</a> and
<a href="http://screencasts.railstutorial.org/">screencast series</a>
to teach web development with
<a href="http://rubyonrails.org/">Ruby on Rails</a>.
This is the sample application for the tutorial.
</p>
</body>
</html>
```
至此,我們把頁面標題中的變動部分都換成了 ERb。現在,各個頁面的內容類似下面這樣:
```
<% provide(:title, "The Title") %>
<!DOCTYPE html>
<html>
<head>
<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
</head>
<body>
Contents
</body>
</html>
```
也就是說,所有的頁面結構都是一致的,包括 `title` 標簽中的內容,只有 `body` 標簽中的內容有些差別。
為了提取出共用的結構,Rails 提供了一個特別的布局文件,名為 `application.html.erb`。我們在 [3.4 節](#slightly-dynamic-pages)重命名了這個文件,現在改回來:
```
$ mv layout_file app/views/layouts/application.html.erb
```
若想使用這個布局,我們要把默認的標題換成前面幾段代碼中使用的嵌入式 Ruby:
```
<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
```
修改后得到的布局文件如[代碼清單 3.32](#listing-application-layout) 所示。
##### 代碼清單 3.32:這個演示應用的網站布局 GREEN
app/views/layouts/application.html.erb
```
<!DOCTYPE html>
<html>
<head>
<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
<%= stylesheet_link_tag 'application', media: 'all',
'data-turbolinks-track' => true %>
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
<%= csrf_meta_tags %>
</head>
<body>
<%= yield %>
</body>
</html>
```
注意,其中有一行比較特殊:
```
<%= yield %>
```
這行代碼的作用是,把每個頁面的內容插入布局中。沒必要了解它的具體實現過程,我們只需知道,在布局中使用這行代碼后,訪問 /static_pages/home 時會把 `home.html.erb` 中的內容轉換成 HTML,然后插入 `<%= yield %>` 所在的位置。
還要注意,默認的 Rails 布局文件中還有下面這幾行代碼:
```
<%= stylesheet_link_tag ... %>
<%= javascript_include_tag "application", ... %>
<%= csrf_meta_tags %>
```
這幾行代碼的作用是,引入應用的樣式表和 JavaScript 文件(Asset Pipeline 的一部分,[5.2.1 節](chapter5.html#the-asset-pipeline)會介紹);Rails 中的 `csrf_meta_tags` 方法,作用是避免“跨站請求偽造”(Cross-Site Request Forgery,簡稱 CSRF,一種惡意網絡攻擊)。
現在,[代碼清單 3.28](#listing-home-view-erb-title)、[代碼清單 3.30](#listing-help-view-erb-title) 和 [代碼清單 3.31](#listing-about-view-erb-title) 的內容還是和布局文件中類似的 HTML 結構,所以我們要把完整的結構刪除,只保留需要的內容。清理后的視圖如[代碼清單 3.33](#listing-home-view-interior)、[代碼清單 3.34](#listing-help-view-interior) 和 [代碼清單 3.35](#listing-about-view-interior) 所示。
##### 代碼清單 3.33:去除完整的 HTML 結構后的首頁 GREEN
app/views/static_pages/home.html.erb
```
<% provide(:title, "Home") %>
<h1>Sample App</h1>
<p>
This is the home page for the
<a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
sample application.
</p>
```
##### 代碼清單 3.34:去除完整的 HTML 結構后的“幫助”頁面 GREEN
app/views/static_pages/help.html.erb
```
<% provide(:title, "Help") %>
<h1>Help</h1>
<p>
Get help on the Ruby on Rails Tutorial at the
<a href="http://www.railstutorial.org/#help">Rails Tutorial help section</a>.
To get help on this sample app, see the
<a href="http://www.railstutorial.org/book"><em>Ruby on Rails Tutorial</em>
book</a>.
</p>
```
##### 代碼清單 3.35:去除完整的 HTML 結構后的“關于”頁面 GREEN
app/views/static_pages/about.html.erb
```
<% provide(:title, "About") %>
<h1>About</h1>
<p>
The <a href="http://www.railstutorial.org/"><em>Ruby on Rails
Tutorial</em></a> is a
<a href="http://www.railstutorial.org/book">book</a> and
<a href="http://screencasts.railstutorial.org/">screencast series</a>
to teach web development with
<a href="http://rubyonrails.org/">Ruby on Rails</a>.
This is the sample application for the tutorial.
</p>
```
修改這幾個視圖后,首頁、“幫助”頁面和“關于”頁面顯示的內容還和之前一樣,但是沒有多少重復內容了。
經驗告訴我們,即便是十分簡單的重構,也容易出錯,所以才要認真編寫測試組件。有了測試,我們就無需手動檢查每個頁面,看有沒有錯誤。初期階段手動檢查還不算難,但是當應用不斷變大之后,情況就不同了。我們只需驗證測試組件是否還能通過即可:
##### 代碼清單 3.36:**GREEN**
```
$ bundle exec rake test
3 tests, 6 assertions, 0 failures, 0 errors, 0 skips
```
測試不能證明代碼完全正確,但至少能提高正確的可能性,而且還提供了安全防護措施,避免以后出現問題。
## 3.4.4 設置根路由
我們修改了網站中的頁面,也順利開始編寫測試了,在繼續之前,我們要設置應用的根路由。與 [1.3.4 節](chapter1.html#hello-world)和 [2.2.2 節](chapter2.html#mvc-in-action)的做法一樣,我們要修改 `routes.rb` 文件,把根路徑 `/` 指向我們選擇的頁面。這里我們要指向前面創建的首頁。(我還建議把 [3.1 節](#sample-app-setup)添加的 `hello` 動作從應用的控制器中刪除。)如[代碼清單 3.37](#listing-home-root-route) 所示,我們要把自動生成的 `get` 規則([代碼清單 3.5](#listing-pages-routes))改成:
```
root 'static_pages#home'
```
我們把 `static_pages/home` 改成 `static_pages#home`,確保通過 `GET` 請求訪問 `/` 時,會交給靜態頁面路由器中的 `home` 動作處理。修改路由后,首頁如[圖 3.7](#fig-home-root-route) 所示。(注意,修改路由之后,/static_pages/home 就無法訪問了。)
##### 代碼清單 3.37:把根路由指向首頁
config/routes.rb
```
Rails.application.routes.draw do
root 'static_pages#home' get 'static_pages/help'
get 'static_pages/about'
end
```
圖 3.7:在根路由上顯示的首頁
- Ruby on Rails 教程
- 致中國讀者
- 序
- 致謝
- 作者譯者簡介
- 版權和代碼授權協議
- 第 1 章 從零開始,完成一次部署
- 1.1 簡介
- 1.2 搭建環境
- 1.3 第一個應用
- 1.4 使用 Git 做版本控制
- 1.5 部署
- 1.6 小結
- 1.7 練習
- 第 2 章 玩具應用
- 2.1 規劃應用
- 2.2 用戶資源
- 2.3 微博資源
- 2.4 小結
- 2.5 練習
- 第 3 章 基本靜態的頁面
- 3.1 創建演示應用
- 3.2 靜態頁面
- 3.3 開始測試
- 3.4 有點動態內容的頁面
- 3.5 小結
- 3.6 練習
- 3.7 高級測試技術
- 第 4 章 Rails 背后的 Ruby
- 4.1 導言
- 4.2 字符串和方法
- 4.3 其他數據類型
- 4.4 Ruby 類
- 4.5 小結
- 4.6 練習
- 第 5 章 完善布局
- 5.1 添加一些結構
- 5.2 Sass 和 Asset Pipeline
- 5.3 布局中的鏈接
- 5.4 用戶注冊:第一步
- 5.5 小結
- 5.6 練習
- 第 6 章 用戶模型
- 6.1 用戶模型
- 6.2 用戶數據驗證
- 6.3 添加安全密碼
- 6.4 小結
- 6.5 練習
- 第 7 章 注冊
- 7.1 顯示用戶的信息
- 7.2 注冊表單
- 7.3 注冊失敗
- 7.4 注冊成功
- 7.5 專業部署方案
- 7.6 小結
- 7.7 練習
- 第 8 章 登錄和退出
- 8.1 會話
- 8.2 登錄
- 8.3 退出
- 8.4 記住我
- 8.5 小結
- 8.6 練習
- 第 9 章 更新,顯示和刪除用戶
- 9.1 更新用戶
- 9.2 權限系統
- 9.3 列出所有用戶
- 9.4 刪除用戶
- 9.5 小結
- 9.6 練習
- 第 10 章 賬戶激活和密碼重設
- 10.1 賬戶激活
- 10.2 密碼重設
- 10.3 在生產環境中發送郵件
- 10.4 小結
- 10.5 練習
- 10.6 證明超時失效的比較算式
- 第 11 章 用戶的微博
- 11.1 微博模型
- 11.2 顯示微博
- 11.3 微博相關的操作
- 11.4 微博中的圖片
- 11.5 小結
- 11.6 練習
- 第 12 章 關注用戶
- 12.1 “關系”模型
- 12.2 關注用戶的網頁界面
- 12.3 動態流
- 12.4 小結
- 12.5 練習