# Rails 入門
本文介紹如何開始使用 Ruby on Rails。
讀完本文,你將學到:
* 如何安裝 Rails,新建 Rails 程序,如何連接數據庫;
* Rails 程序的基本文件結構;
* MVC(模型,視圖,控制器)和 REST 架構的基本原理;
* 如何快速生成 Rails 程序骨架;
### Chapters
1. [前提條件](#%E5%89%8D%E6%8F%90%E6%9D%A1%E4%BB%B6)
2. [Rails 是什么?](#rails-%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F)
3. [新建 Rails 程序](#%E6%96%B0%E5%BB%BA-rails-%E7%A8%8B%E5%BA%8F)
* [安裝 Rails](#%E5%AE%89%E8%A3%85-rails)
* [創建 Blog 程序](#%E5%88%9B%E5%BB%BA-blog-%E7%A8%8B%E5%BA%8F)
4. [Hello, Rails!](#hello,-rails-bang)
* [啟動服務器](#%E5%90%AF%E5%8A%A8%E6%9C%8D%E5%8A%A1%E5%99%A8)
* [顯示“Hello, Rails!”](#%E6%98%BE%E7%A4%BA%E2%80%9Chello,-rails-bang%E2%80%9D)
* [設置程序的首頁](#%E8%AE%BE%E7%BD%AE%E7%A8%8B%E5%BA%8F%E7%9A%84%E9%A6%96%E9%A1%B5)
5. [開始使用](#%E5%BC%80%E5%A7%8B%E4%BD%BF%E7%94%A8)
* [挖地基](#%E6%8C%96%E5%9C%B0%E5%9F%BA)
* [首個表單](#%E9%A6%96%E4%B8%AA%E8%A1%A8%E5%8D%95)
* [創建文章](#%E5%88%9B%E5%BB%BA%E6%96%87%E7%AB%A0)
* [創建 Article 模型](#%E5%88%9B%E5%BB%BA-article-%E6%A8%A1%E5%9E%8B)
* [運行遷移](#%E8%BF%90%E8%A1%8C%E8%BF%81%E7%A7%BB)
* [在控制器中保存數據](#%E5%9C%A8%E6%8E%A7%E5%88%B6%E5%99%A8%E4%B8%AD%E4%BF%9D%E5%AD%98%E6%95%B0%E6%8D%AE)
* [顯示文章](#%E6%98%BE%E7%A4%BA%E6%96%87%E7%AB%A0)
* [列出所有文章](#%E5%88%97%E5%87%BA%E6%89%80%E6%9C%89%E6%96%87%E7%AB%A0)
* [添加鏈接](#%E6%B7%BB%E5%8A%A0%E9%93%BE%E6%8E%A5)
* [添加數據驗證](#%E6%B7%BB%E5%8A%A0%E6%95%B0%E6%8D%AE%E9%AA%8C%E8%AF%81)
* [更新文章](#%E6%9B%B4%E6%96%B0%E6%96%87%E7%AB%A0)
* [使用局部視圖去掉視圖中的重復代碼](#%E4%BD%BF%E7%94%A8%E5%B1%80%E9%83%A8%E8%A7%86%E5%9B%BE%E5%8E%BB%E6%8E%89%E8%A7%86%E5%9B%BE%E4%B8%AD%E7%9A%84%E9%87%8D%E5%A4%8D%E4%BB%A3%E7%A0%81)
* [刪除文章](#%E5%88%A0%E9%99%A4%E6%96%87%E7%AB%A0)
6. [添加第二個模型](#%E6%B7%BB%E5%8A%A0%E7%AC%AC%E4%BA%8C%E4%B8%AA%E6%A8%A1%E5%9E%8B)
* [生成模型](#%E7%94%9F%E6%88%90%E6%A8%A1%E5%9E%8B)
* [模型關聯](#%E6%A8%A1%E5%9E%8B%E5%85%B3%E8%81%94)
* [添加評論的路由](#%E6%B7%BB%E5%8A%A0%E8%AF%84%E8%AE%BA%E7%9A%84%E8%B7%AF%E7%94%B1)
* [生成控制器](#%E7%94%9F%E6%88%90%E6%8E%A7%E5%88%B6%E5%99%A8)
7. [重構](#%E9%87%8D%E6%9E%84)
* [渲染局部視圖中的集合](#%E6%B8%B2%E6%9F%93%E5%B1%80%E9%83%A8%E8%A7%86%E5%9B%BE%E4%B8%AD%E7%9A%84%E9%9B%86%E5%90%88)
* [渲染局部視圖中的表單](#%E6%B8%B2%E6%9F%93%E5%B1%80%E9%83%A8%E8%A7%86%E5%9B%BE%E4%B8%AD%E7%9A%84%E8%A1%A8%E5%8D%95)
8. [刪除評論](#%E5%88%A0%E9%99%A4%E8%AF%84%E8%AE%BA)
* [刪除關聯對象](#%E5%88%A0%E9%99%A4%E5%85%B3%E8%81%94%E5%AF%B9%E8%B1%A1)
9. [安全](#%E5%AE%89%E5%85%A8)
* [基本認證](#%E5%9F%BA%E6%9C%AC%E8%AE%A4%E8%AF%81)
* [其他安全注意事項](#%E5%85%B6%E4%BB%96%E5%AE%89%E5%85%A8%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9)
10. [接下來做什么](#%E6%8E%A5%E4%B8%8B%E6%9D%A5%E5%81%9A%E4%BB%80%E4%B9%88)
11. [常見問題](#%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98)
### 1 前提條件
本文針對想從零開始開發 Rails 程序的初學者,不需要預先具備任何的 Rails 使用經驗。不過,為了能順利閱讀,還是需要事先安裝好一些軟件:
* [Ruby](https://www.ruby-lang.org/en/downloads) 1.9.3 及以上版本
* 包管理工具 [RubyGems](https://rubygems.org),隨 Ruby 1.9+ 安裝。想深入了解 RubyGems,請閱讀 [RubyGems 指南](http://guides.rubygems.org)
* [SQLite3](https://www.sqlite.org) 數據庫
Rails 是使用 Ruby 語言開發的網頁程序框架。如果之前沒接觸過 Ruby,學習 Rails 可要深下一番功夫。網上有很多資源可以學習 Ruby:
* [Ruby 語言官方網站](https://www.ruby-lang.org/zh_cn/documentation/)
* [reSRC 列出的免費編程書籍](http://resrc.io/list/10/list-of-free-programming-books/#ruby)
記住,某些資源雖然很好,但是針對 Ruby 1.8,甚至 1.6 編寫的,所以沒有介紹一些 Rails 日常開發會用到的句法。
### 2 Rails 是什么?
Rails 是使用 Ruby 語言編寫的網頁程序開發框架,目的是為開發者提供常用組件,簡化網頁程序的開發。只需編寫較少的代碼,就能實現其他編程語言或框架難以企及的功能。經驗豐富的 Rails 程序員會發現,Rails 讓程序開發變得更有樂趣。
Rails 有自己的一套規則,認為問題總有最好的解決方法,而且建議使用最好的方法,有些情況下甚至不推薦使用其他替代方案。學會如何按照 Rails 的思維開發,能極大提高開發效率。如果堅持在 Rails 開發中使用其他語言中的舊思想,嘗試使用別處學來的編程模式,開發過程就不那么有趣了。
Rails 哲學包含兩大指導思想:
* **不要自我重復(DRY):** DRY 是軟件開發中的一個原則,“系統中的每個功能都要具有單一、準確、可信的實現。”。不重復表述同一件事,寫出的代碼才能更易維護,更具擴展性,也更不容易出問題。
* **多約定,少配置:** Rails 為網頁程序的大多數需求都提供了最好的解決方法,而且默認使用這些約定,不用在長長的配置文件中設置每個細節。
### 3 新建 Rails 程序
閱讀本文時,最佳方式是跟著一步一步操作,如果錯過某段代碼或某個步驟,程序就可能出錯,所以請一步一步跟著做。
本文會新建一個名為 `blog` 的 Rails 程序,這是一個非常簡單的博客。在開始開發程序之前,要確保已經安裝了 Rails。
文中的示例代碼使用 `$` 表示命令行提示符,你的提示符可能修改過,所以會不一樣。在 Windows 中,提示符可能是 `c:\source_code>`。
#### 3.1 安裝 Rails
打開命令行:在 Mac OS X 中打開 Terminal.app,在 Windows 中選擇“運行”,然后輸入“cmd.exe”。下文中所有以 `$` 開頭的代碼,都要在命令行中運行。先確認是否安裝了 Ruby 最新版:
有很多工具可以幫助你快速在系統中安裝 Ruby 和 Ruby on Rails。Windows 用戶可以使用 [Rails Installer](http://railsinstaller.org),Mac OS X 用戶可以使用 [Tokaido](https://github.com/tokaido/tokaidoapp)。
```
$ ruby -v
ruby 2.1.2p95
```
如果你還沒安裝 Ruby,請訪問 [ruby-lang.org](https://www.ruby-lang.org/en/downloads/),找到針對所用系統的安裝方法。
很多類 Unix 系統都自帶了版本尚新的 SQLite3。Windows 等其他操作系統的用戶可以在 [SQLite3 的網站](https://www.sqlite.org)上找到安裝說明。然后,確認是否在 PATH 中:
```
$ sqlite3 --version
```
命令行應該回顯版本才對。
安裝 Rails,請使用 RubyGems 提供的 `gem install` 命令:
```
$ gem install rails
```
要檢查所有軟件是否都正確安裝了,可以執行下面的命令:
```
$ rails --version
```
如果顯示的結果類似“Rails 4.2.0”,那么就可以繼續往下讀了。
#### 3.2 創建 Blog 程序
Rails 提供了多個被稱為“生成器”的腳本,可以簡化開發,生成某項操作需要的所有文件。其中一個是新程序生成器,生成一個 Rails 程序骨架,不用自己一個一個新建文件。
打開終端,進入有寫權限的文件夾,執行以下命令生成一個新程序:
```
$ rails new blog
```
這個命令會在文件夾 `blog` 中新建一個 Rails 程序,然后執行 `bundle install` 命令安裝 `Gemfile` 中列出的 gem。
執行 `rails new -h` 可以查看新程序生成器的所有命令行選項。
生成 `blog` 程序后,進入該文件夾:
```
$ cd blog
```
`blog` 文件夾中有很多自動生成的文件和文件夾,組成一個 Rails 程序。本文大部分時間都花在 `app` 文件夾上。下面簡單介紹默認生成的文件和文件夾的作用:
| 文件/文件夾 | 作用 |
| --- | --- |
| app/ | 存放程序的控制器、模型、視圖、幫助方法、郵件和靜態資源文件。本文主要關注的是這個文件夾。 |
| bin/ | 存放運行程序的 `rails` 腳本,以及其他用來部署或運行程序的腳本。 |
| config/ | 設置程序的路由,數據庫等。詳情參閱“[設置 Rails 程序](/configuring.html)”一文。 |
| config.ru | 基于 Rack 服務器的程序設置,用來啟動程序。 |
| db/ | 存放當前數據庫的模式,以及數據庫遷移文件。 |
| Gemfile, Gemfile.lock | 這兩個文件用來指定程序所需的 gem 依賴件,用于 Bundler gem。關于 Bundler 的詳細介紹,請訪問 [Bundler 官網](http://bundler.io)。 |
| lib/ | 程序的擴展模塊。 |
| log/ | 程序的日志文件。 |
| public/ | 唯一對外開放的文件夾,存放靜態文件和編譯后的資源文件。 |
| Rakefile | 保存并加載可在命令行中執行的任務。任務在 Rails 的各組件中定義。如果想添加自己的任務,不要修改這個文件,把任務保存在 `lib/tasks` 文件夾中。 |
| README.rdoc | 程序的簡單說明。你應該修改這個文件,告訴其他人這個程序的作用,如何安裝等。 |
| test/ | 單元測試,固件等測試用文件。詳情參閱“[測試 Rails 程序](/testing.html)”一文。 |
| tmp/ | 臨時文件,例如緩存,PID,會話文件。 |
| vendor/ | 存放第三方代碼。經常用來放第三方 gem。 |
### 4 Hello, Rails!
首先,我們來添加一些文字,在頁面中顯示。為了能訪問網頁,要啟動程序服務器。
#### 4.1 啟動服務器
現在,新建的 Rails 程序已經可以正常運行。要訪問網站,需要在開發電腦上啟動服務器。請在 `blog` 文件夾中執行下面的命令:
```
$ rails server
```
把 CoffeeScript 編譯成 JavaScript 需要 JavaScript 運行時,如果沒有運行時,會報錯,提示沒有 `execjs`。Mac OS X 和 Windows 一般都提供了 JavaScript 運行時。Rails 生成的 `Gemfile` 中,安裝 `therubyracer` gem 的代碼被注釋掉了,如果需要使用這個 gem,請把前面的注釋去掉。在 JRuby 中推薦使用 `therubyracer`。在 JRuby 中生成的 `Gemfile` 已經包含了這個 gem。所有支持的運行時參見 [ExecJS](https://github.com/sstephenson/execjs#readme)。
上述命令會啟動 WEBrick,這是 Ruby 內置的服務器。要查看程序,請打開一個瀏覽器窗口,訪問 [http://localhost:3000](http://localhost:3000)。應該會看到默認的 Rails 信息頁面:

要想停止服務器,請在命令行中按 Ctrl+C 鍵。服務器成功停止后回重新看到命令行提示符。在大多數類 Unix 系統中,包括 Mac OS X,命令行提示符是 `$` 符號。在開發模式中,一般情況下無需重啟服務器,修改文件后,服務器會自動重新加載。
“歡迎使用”頁面是新建 Rails 程序后的“冒煙測試”:確保程序設置正確,能順利運行。你可以點擊“About your application's environment”鏈接查看程序所處環境的信息。
#### 4.2 顯示“Hello, Rails!”
要在 Rails 中顯示“Hello, Rails!”,需要新建一個控制器和視圖。
控制器用來接受向程序發起的請求。路由決定哪個控制器會接受到這個請求。一般情況下,每個控制器都有多個路由,對應不同的動作。動作用來提供視圖中需要的數據。
視圖的作用是,以人類能看懂的格式顯示數據。有一點要特別注意,數據是在控制器中獲取的,而不是在視圖中。視圖只是把數據顯示出來。默認情況下,視圖使用 eRuby(嵌入式 Ruby)語言編寫,經由 Rails 解析后,再發送給用戶。
控制器可用控制器生成器創建,你要告訴生成器,我想要個名為“welcome”的控制器和一個名為“index”的動作,如下所示:
```
$ rails generate controller welcome index
```
運行上述命令后,Rails 會生成很多文件,以及一個路由。
```
create app/controllers/welcome_controller.rb
route get 'welcome/index'
invoke erb
create app/views/welcome
create app/views/welcome/index.html.erb
invoke test_unit
create test/controllers/welcome_controller_test.rb
invoke helper
create app/helpers/welcome_helper.rb
invoke assets
invoke coffee
create app/assets/javascripts/welcome.js.coffee
invoke scss
create app/assets/stylesheets/welcome.css.scss
```
在這些文件中,最重要的當然是控制器,位于 `app/controllers/welcome_controller.rb`,以及視圖,位于 `app/views/welcome/index.html.erb`。
使用文本編輯器打開 `app/views/welcome/index.html.erb` 文件,刪除全部內容,寫入下面這行代碼:
```
<h1>Hello, Rails!</h1>
```
#### 4.3 設置程序的首頁
我們已經創建了控制器和視圖,現在要告訴 Rails 在哪個地址上顯示“Hello, Rails!”。這里,我們希望訪問根地址 [http://localhost:3000](http://localhost:3000) 時顯示。但是現在顯示的還是歡迎頁面。
我們要告訴 Rails 真正的首頁是什么。
在編輯器中打開 `config/routes.rb` 文件。
```
Rails.application.routes.draw do
get 'welcome/index'
# The priority is based upon order of creation:
# first created -> highest priority.
#
# You can have the root of your site routed with "root"
# root 'welcome#index'
#
# ...
```
這是程序的路由文件,使用特殊的 DSL(domain-specific language,領域專屬語言)編寫,告知 Rails 請求應該發往哪個控制器和動作。文件中有很多注釋,舉例說明如何定義路由。其中有一行說明了如何指定控制器和動作設置網站的根路由。找到以 `root` 開頭的代碼行,去掉注釋,變成這樣:
```
root 'welcome#index'
```
`root 'welcome#index'` 告知 Rails,訪問程序的根路徑時,交給 `welcome` 控制器中的 `index` 動作處理。`get 'welcome/index'` 告知 Rails,訪問 [http://localhost:3000/welcome/index](http://localhost:3000/welcome/index) 時,交給 `welcome` 控制器中的 `index` 動作處理。`get 'welcome/index'` 是運行 `rails generate controller welcome index` 時生成的。
如果生成控制器時停止了服務器,請再次啟動(`rails server`),然后在瀏覽器中訪問 [http://localhost:3000](http://localhost:3000)。你會看到之前寫入 `app/views/welcome/index.html.erb` 文件的“Hello, Rails!”,說明新定義的路由把根目錄交給 `WelcomeController` 的 `index` 動作處理了,而且也正確的渲染了視圖。
關于路由的詳細介紹,請閱讀“[Rails 路由全解](/routing.html)”一文。
### 5 開始使用
前文已經介紹如何創建控制器、動作和視圖,下面我們來創建一些更實質的功能。
在博客程序中,我們要創建一個新“資源”。資源是指一系列類似的對象,比如文章,人和動物。
資源可以被創建、讀取、更新和刪除,這些操作簡稱 CRUD。
Rails 提供了一個 `resources` 方法,可以聲明一個符合 REST 架構的資源。創建文章資源后,`config/routes.rb` 文件的內容如下:
```
Rails.application.routes.draw do
resources :articles
root 'welcome#index'
end
```
執行 `rake routes` 任務,會看到定義了所有標準的 REST 動作。輸出結果中各列的意義稍后會說明,現在只要留意 `article` 的單復數形式,這在 Rails 中有特殊的含義。
```
$ bin/rake routes
Prefix Verb URI Pattern Controller#Action
articles GET /articles(.:format) articles#index
POST /articles(.:format) articles#create
new_article GET /articles/new(.:format) articles#new
edit_article GET /articles/:id/edit(.:format) articles#edit
article GET /articles/:id(.:format) articles#show
PATCH /articles/:id(.:format) articles#update
PUT /articles/:id(.:format) articles#update
DELETE /articles/:id(.:format) articles#destroy
root GET / welcome#index
```
下一節,我們會加入新建文章和查看文章的功能。這兩個操作分別對應于 CRUD 的 C 和 R,即創建和讀取。新建文章的表單如下所示:

表單看起來很簡陋,不過沒關系,后文會加入更多的樣式。
#### 5.1 挖地基
首先,程序中要有個頁面用來新建文章。一個比較好的選擇是 `/articles/new`。這個路由前面已經定義了,可以訪問。打開 [http://localhost:3000/articles/new](http://localhost:3000/articles/new) ,會看到如下的路由錯誤:

產生這個錯誤的原因是,沒有定義用來處理該請求的控制器。解決這個問題的方法很簡單,執行下面的命令創建名為 `ArticlesController` 的控制器即可:
```
$ bin/rails g controller articles
```
打開剛生成的 `app/controllers/articles_controller.rb` 文件,會看到一個幾乎沒什么內容的控制器:
```
class ArticlesController < ApplicationController
end
```
控制器就是一個類,繼承自 `ApplicationController`。在這個類中定義的方法就是控制器的動作。動作的作用是處理文章的 CRUD 操作。
在 Ruby 中,方法分為 `public`、`private` 和 `protected` 三種,只有 `public` 方法才能作為控制器的動作。詳情參閱 [Programming Ruby](http://www.ruby-doc.org/docs/ProgrammingRuby/) 一書。
現在刷新 [http://localhost:3000/articles/new](http://localhost:3000/articles/new),會看到一個新錯誤:

這個錯誤的意思是,在剛生成的 `ArticlesController` 控制器中找不到 `new` 動作。因為在生成控制器時,除非指定要哪些動作,否則不會生成,控制器是空的。
手動創建動作只需在控制器中定義一個新方法。打開 `app/controllers/articles_controller.rb` 文件,在 `ArticlesController` 類中,定義 `new` 方法,如下所示:
```
class ArticlesController < ApplicationController
def new
end
end
```
在 `ArticlesController` 中定義 `new` 方法后,再刷新 [http://localhost:3000/articles/new](http://localhost:3000/articles/new),看到的還是個錯誤:

產生這個錯誤的原因是,Rails 希望這樣的常規動作有對應的視圖,用來顯示內容。沒有視圖可用,Rails 就報錯了。
在上圖中,最后一行被截斷了,我們來看一下完整的信息:
```
Missing template articles/new, application/new with {locale:[:en], formats:[:html], handlers:[:erb, :builder, :coffee]}. Searched in: * "/path/to/blog/app/views"
```
這行信息還挺長,我們來看一下到底是什么意思。
第一部分說明找不到哪個模板,這里,丟失的是 `articles/new` 模板。Rails 首先會尋找這個模板,如果找不到,再找名為 `application/new` 的模板。之所以這么找,是因為 `ArticlesController` 繼承自 `ApplicationController`。
后面一部分是個 Hash。`:locale` 表示要找哪國語言模板,默認是英語(`"en"`)。`:format` 表示響應使用的模板格式,默認為 `:html`,所以 Rails 要尋找一個 HTML 模板。`:handlers` 表示用來處理模板的程序,HTML 模板一般使用 `:erb`,XML 模板使用 `:builder`,`:coffee` 用來把 CoffeeScript 轉換成 JavaScript。
最后一部分說明 Rails 在哪里尋找模板。在這個簡單的程序里,模板都存放在一個地方,復雜的程序可能存放在多個位置。
讓這個程序正常運行,最簡單的一種模板是 `app/views/articles/new.html.erb`。模板文件的擴展名是關鍵所在:第一個擴展名是模板的類型,第二個擴展名是模板的處理程序。Rails 會嘗試在 `app/views` 文件夾中尋找名為 `articles/new` 的模板。這個模板的類型只能是 `html`,處理程序可以是 `erb`、`builder` 或 `coffee`。因為我們要編寫一個 HTML 表單,所以使用 `erb`。所以這個模板文件應該命名為 `articles/new.html.erb`,還要放在 `app/views` 文件夾中。
新建文件 `app/views/articles/new.html.erb`,寫入如下代碼:
```
<h1>New Article</h1>
```
再次刷新 [http://localhost:3000/articles/new](http://localhost:3000/articles/new),可以看到頁面中顯示了一個標頭。現在路由、控制器、動作和視圖都能正常運行了。接下來要編寫新建文章的表單了。
#### 5.2 首個表單
要在模板中編寫表單,可以使用“表單構造器”。Rails 中常用的表單構造器是 `form_for`。在 `app/views/articles/new.html.erb` 文件中加入以下代碼:
```
<%= form_for :article do |f| %>
<p>
<%= f.label :title %><br>
<%= f.text_field :title %>
</p>
<p>
<%= f.label :text %><br>
<%= f.text_area :text %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
```
現在刷新頁面,會看到上述代碼生成的表單。在 Rails 中編寫表單就是這么簡單!
調用 `form_for` 方法時,要指定一個對象。在上面的表單中,指定的是 `:article`。這個對象告訴 `form_for`,這個表單是用來處理哪個資源的。在 `form_for` 方法的塊中,`FormBuilder` 對象(用 `f` 表示)創建了兩個標簽和兩個文本字段,一個用于文章標題,一個用于文章內容。最后,在 `f` 對象上調用 `submit` 方法,創建一個提交按鈕。
不過這個表單還有個問題。如果查看這個頁面的源碼,會發現表單 `action` 屬性的值是 `/articles/new`。這就是問題所在,因為其指向的地址就是現在這個頁面,而這個頁面是用來顯示新建文章表單的。
要想轉到其他地址,就要使用其他的地址。這個問題可使用 `form_for` 方法的 `:url` 選項解決。在 Rails 中,用來處理新建資源表單提交數據的動作是 `create`,所以表單應該轉向這個動作。
修改 `app/views/articles/new.html.erb` 文件中的 `form_for`,改成這樣:
```
<%= form_for :article, url: articles_path do |f| %>
```
這里,我們把 `:url` 選項的值設為 `articles_path` 幫助方法。要想知道這個方法有什么作用,我們要回過頭再看一下 `rake routes` 的輸出:
```
$ bin/rake routes
Prefix Verb URI Pattern Controller#Action
articles GET /articles(.:format) articles#index
POST /articles(.:format) articles#create
new_article GET /articles/new(.:format) articles#new
edit_article GET /articles/:id/edit(.:format) articles#edit
article GET /articles/:id(.:format) articles#show
PATCH /articles/:id(.:format) articles#update
PUT /articles/:id(.:format) articles#update
DELETE /articles/:id(.:format) articles#destroy
root GET / welcome#index
```
`articles_path` 幫助方法告訴 Rails,對應的地址是 `/articles`,默認情況下,這個表單會向這個路由發起 `POST` 請求。這個路由對應于 `ArticlesController` 控制器的 `create` 動作。
表單寫好了,路由也定義了,現在可以填寫表單,然后點擊提交按鈕新建文章了。請實際操作一下。提交表單后,會看到一個熟悉的錯誤:

解決這個錯誤,要在 `ArticlesController` 控制器中定義 `create` 動作。
#### 5.3 創建文章
要解決前一節出現的錯誤,可以在 `ArticlesController` 類中定義 `create` 方法。在 `app/controllers/articles_controller.rb` 文件中 `new` 方法后面添加以下代碼:
```
class ArticlesController < ApplicationController
def new
end
def create
end
end
```
然后再次提交表單,會看到另一個熟悉的錯誤:找不到模板。現在暫且不管這個錯誤。`create` 動作的作用是把新文章保存到數據庫中。
提交表單后,其中的字段以參數的形式傳遞給 Rails。這些參數可以在控制器的動作中使用,完成指定的操作。要想查看這些參數的內容,可以把 `create` 動作改成:
```
def create
render plain: params[:article].inspect
end
```
`render` 方法接受一個簡單的 Hash 為參數,這個 Hash 的鍵是 `plain`,對應的值為 `params[:article].inspect`。`params` 方法表示通過表單提交的參數,返回 `ActiveSupport::HashWithIndifferentAccess` 對象,可以使用字符串或者 Symbol 獲取鍵對應的值。現在,我們只關注通過表單提交的參數。
如果現在再次提交表單,不會再看到找不到模板錯誤,而是會看到類似下面的文字:
```
{"title"=>"First article!", "text"=>"This is my first article."}
```
`create` 動作把表單提交的參數顯示出來了。不過這么做沒什么用,看到了參數又怎樣,什么都沒發生。
#### 5.4 創建 Article 模型
在 Rails 中,模型的名字使用單數,對應的數據表名使用復數。Rails 提供了一個生成器用來創建模型,大多數 Rails 開發者創建模型時都會使用。創建模型,請在終端里執行下面的命令:
```
$ bin/rails generate model Article title:string text:text
```
這個命令告知 Rails,我們要創建 `Article` 模型,以及一個字符串屬性 `title` 和文本屬性 `text`。這兩個屬性會自動添加到 `articles` 數據表中,映射到 `Article` 模型。
執行這個命令后,Rails 會生成一堆文件。現在我們只關注 `app/models/article.rb` 和 `db/migrate/20140120191729_create_articles.rb`(你得到的文件名可能有點不一樣)這兩個文件。后者用來創建數據庫結構,下一節會詳細說明。
Active Record 很智能,能自動把數據表中的字段映射到模型的屬性上。所以無需在 Rails 的模型中聲明屬性,因為 Active Record 會自動映射。
#### 5.5 運行遷移
如前文所述,`rails generate model` 命令會在 `db/migrate` 文件夾中生成一個數據庫遷移文件。遷移是一個 Ruby 類,能簡化創建和修改數據庫結構的操作。Rails 使用 rake 任務運行遷移,修改數據庫結構后還能撤銷操作。遷移的文件名中有個時間戳,這樣能保證遷移按照創建的時間順序運行。
`db/migrate/20140120191729_create_articles.rb`(還記得嗎,你的遷移文件名可能有點不一樣)文件的內容如下所示:
```
class CreateArticles < ActiveRecord::Migration
def change
create_table :articles do |t|
t.string :title
t.text :text
t.timestamps
end
end
end
```
在這個遷移中定義了一個名為 `change` 的方法,在運行遷移時執行。`change` 方法中定義的操作都是可逆的,Rails 知道如何撤銷這次遷移操作。運行遷移后,會創建 `articles` 表,以及一個字符串字段和文本字段。同時還會創建兩個時間戳字段,用來跟蹤記錄的創建時間和更新時間。
關于遷移的詳細說明,請參閱“[Active Record 數據庫遷移](/migrations.html)”一文。
然后,使用 rake 命令運行遷移:
```
$ bin/rake db:migrate
```
Rails 會執行遷移操作,告訴你創建了 `articles` 表。
```
== CreateArticles: migrating ==================================================
-- create_table(:articles)
-> 0.0019s
== CreateArticles: migrated (0.0020s) =========================================
```
因為默認情況下,程序運行在開發環境中,所以相關的操作應用于 `config/database.yml` 文件中 `development` 區域設置的數據庫上。如果想在其他環境中運行遷移,必須在命令中指明:`rake db:migrate RAILS_ENV=production`。
#### 5.6 在控制器中保存數據
再回到 `ArticlesController` 控制器,我們要修改 `create` 動作,使用 `Article` 模型把數據保存到數據庫中。打開 `app/controllers/articles_controller.rb` 文件,把 `create` 動作修改成這樣:
```
def create
@article = Article.new(params[:article])
@article.save
redirect_to @article
end
```
在 Rails 中,每個模型可以使用各自的屬性初始化,自動映射到數據庫字段上。`create` 動作中的第一行就是這個目的(還記得嗎,`params[:article]` 就是我們要獲取的屬性)。`@article.save` 的作用是把模型保存到數據庫中。保存完后轉向 `show` 動作。稍后再編寫 `show` 動作。
后文會看到,`@article.save` 返回一個布爾值,表示保存是否成功。
再次訪問 [http://localhost:3000/articles/new](http://localhost:3000/articles/new),填寫表單,還差一步就能創建文章了,會看到一個錯誤頁面:

Rails 提供了很多安全防范措施保證程序的安全,你所看到的錯誤就是因為違反了其中一個措施。這個防范措施叫做“健壯參數”,我們要明確地告知 Rails 哪些參數可在控制器中使用。這里,我們想使用 `title` 和 `text` 參數。請把 `create` 動作修改成:
```
def create
@article = Article.new(article_params)
@article.save
redirect_to @article
end
private
def article_params
params.require(:article).permit(:title, :text)
end
```
看到 `permit` 方法了嗎?這個方法允許在動作中使用 `title` 和 `text` 屬性。
注意,`article_params` 是私有方法。這種用法可以防止攻擊者把修改后的屬性傳遞給模型。關于健壯參數的更多介紹,請閱讀[這篇文章](http://weblog.rubyonrails.org/2012/3/21/strong-parameters/)。
#### 5.7 顯示文章
現在再次提交表單,Rails 會提示找不到 `show` 動作。這個提示沒多大用,我們還是先添加 `show` 動作吧。
我們在 `rake routes` 的輸出中看到,`show` 動作的路由是:
```
article GET /articles/:id(.:format) articles#show
```
`:id` 的意思是,路由期望接收一個名為 `id` 的參數,在這個例子中,就是文章的 ID。
和前面一樣,我們要在 `app/controllers/articles_controller.rb` 文件中添加 `show` 動作,以及相應的視圖文件。
```
def show
@article = Article.find(params[:id])
end
```
有幾點要注意。我們調用 `Article.find` 方法查找想查看的文章,傳入的參數 `params[:id]` 會從請求中獲取 `:id` 參數。我們還把文章對象存儲在一個實例變量中(以 `@` 開頭的變量),只有這樣,變量才能在視圖中使用。
然后,新建 `app/views/articles/show.html.erb` 文件,寫入下面的代碼:
```
<p>
<strong>Title:</strong>
<%= @article.title %>
</p>
<p>
<strong>Text:</strong>
<%= @article.text %>
</p>
```
做了以上修改后,就能真正的新建文章了。訪問 [http://localhost:3000/articles/new](http://localhost:3000/articles/new),自己試試。

#### 5.8 列出所有文章
我們還要列出所有文章,對應的路由是:
```
articles GET /articles(.:format) articles#index
```
在 `app/controllers/articles_controller.rb` 文件中,為 `ArticlesController` 控制器添加 `index` 動作:
```
def index
@articles = Article.all
end
```
然后編寫這個動作的視圖,保存為 `app/views/articles/index.html.erb`:
```
<h1>Listing articles</h1>
<table>
<tr>
<th>Title</th>
<th>Text</th>
</tr>
<% @articles.each do |article| %>
<tr>
<td><%= article.title %></td>
<td><%= article.text %></td>
</tr>
<% end %>
</table>
```
現在訪問 [http://localhost:3000/articles](http://localhost:3000/articles),會看到已經發布的文章列表。
#### 5.9 添加鏈接
至此,我們可以新建、顯示、列出文章了。下面我們添加一些鏈接,指向這些頁面。
打開 `app/views/welcome/index.html.erb` 文件,改成這樣:
```
<h1>Hello, Rails!</h1>
<%= link_to 'My Blog', controller: 'articles' %>
```
`link_to` 是 Rails 內置的視圖幫助方法之一,根據提供的文本和地址創建超鏈接。這上面這段代碼中,地址是文章列表頁面。
接下來添加到其他頁面的鏈接。先在 `app/views/articles/index.html.erb` 中添加“New Article”鏈接,放在 `<table>` 標簽之前:
```
<%= link_to 'New article', new_article_path %>
```
點擊這個鏈接后,會轉向新建文章的表單頁面。
然后在 `app/views/articles/new.html.erb` 中添加一個鏈接,位于表單下面,返回到 `index` 動作:
```
<%= form_for :article do |f| %>
...
<% end %>
<%= link_to 'Back', articles_path %>
```
最后,在 `app/views/articles/show.html.erb` 模板中添加一個鏈接,返回 `index` 動作,這樣用戶查看某篇文章后就可以返回文章列表頁面了:
```
<p>
<strong>Title:</strong>
<%= @article.title %>
</p>
<p>
<strong>Text:</strong>
<%= @article.text %>
</p>
<%= link_to 'Back', articles_path %>
```
如果要鏈接到同一個控制器中的動作,不用指定 `:controller` 選項,因為默認情況下使用的就是當前控制器。
在開發模式下(默認),每次請求 Rails 都會重新加載程序,因此修改之后無需重啟服務器。
#### 5.10 添加數據驗證
模型文件,比如 `app/models/article.rb`,可以簡單到只有這兩行代碼:
```
class Article < ActiveRecord::Base
end
```
文件中沒有多少代碼,不過請注意,`Article` 類繼承自 `ActiveRecord::Base`。Active Record 提供了很多功能,包括:基本的數據庫 CRUD 操作,數據驗證,復雜的搜索功能,以及多個模型之間的關聯。
Rails 為模型提供了很多方法,用來驗證傳入的數據。打開 `app/models/article.rb` 文件,修改成:
```
class Article < ActiveRecord::Base
validates :title, presence: true,
length: { minimum: 5 }
end
```
添加的這段代碼可以確保每篇文章都有一個標題,而且至少有五個字符。在模型中可以驗證數據是否滿足多種條件,包括:字段是否存在、是否唯一,數據類型,以及關聯對象是否存在。“[Active Record 數據驗證](/active_record_validations.html)”一文會詳細介紹數據驗證。
添加數據驗證后,如果把不滿足驗證條件的文章傳遞給 `@article.save`,會返回 `false`。打開 `app/controllers/articles_controller.rb` 文件,會發現,我們還沒在 `create` 動作中檢查 `@article.save` 的返回結果。如果保存失敗,應該再次顯示表單。為了實現這種功能,請打開 `app/controllers/articles_controller.rb` 文件,把 `new` 和 `create` 動作改成:
```
def new
@article = Article.new
end
def create
@article = Article.new(article_params)
if @article.save
redirect_to @article
else
render 'new'
end
end
private
def article_params
params.require(:article).permit(:title, :text)
end
```
在 `new` 動作中添加了一個實例變量 `@article`。稍后你會知道為什么要這么做。
注意,在 `create` 動作中,如果保存失敗,調用的是 `render` 方法而不是 `redirect_to` 方法。用 `render` 方法才能在保存失敗后把 `@article` 對象傳給 `new` 動作的視圖。渲染操作和表單提交在同一次請求中完成;而 `redirect_to` 會讓瀏覽器發起一次新請求。
刷新 [http://localhost:3000/articles/new](http://localhost:3000/articles/new),提交一個沒有標題的文章,Rails 會退回這個頁面,但這種處理方法沒多少用,你要告訴用戶哪兒出錯了。為了實現這種功能,請在 `app/views/articles/new.html.erb` 文件中檢測錯誤消息:
```
<%= form_for :article, url: articles_path do |f| %>
<% if @article.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@article.errors.count, "error") %> prohibited
this article from being saved:</h2>
<ul>
<% @article.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<p>
<%= f.label :title %><br>
<%= f.text_field :title %>
</p>
<p>
<%= f.label :text %><br>
<%= f.text_area :text %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
<%= link_to 'Back', articles_path %>
```
我們添加了很多代碼,使用 `@article.errors.any?` 檢查是否有錯誤,如果有錯誤,使用 `@article.errors.full_messages` 顯示錯誤。
`pluralize` 是 Rails 提供的幫助方法,接受一個數字和字符串作為參數。如果數字比 1 大,字符串會被轉換成復數形式。
在 `new` 動作中加入 `@article = Article.new` 的原因是,如果不這么做,在視圖中 `@article` 的值就是 `nil`,調用 `@article.errors.any?` 時會發生錯誤。
Rails 會自動把出錯的表單字段包含在一個 `div` 中,并為其添加了一個 class:`field_with_errors`。我們可以定義一些樣式,凸顯出錯的字段。
再次訪問 [http://localhost:3000/articles/new](http://localhost:3000/articles/new),嘗試發布一篇沒有標題的文章,會看到一個很有用的錯誤提示。

#### 5.11 更新文章
我們已經說明了 CRUD 中的 CR 兩種操作。下面進入 U 部分,更新文章。
首先,要在 `ArticlesController` 中添加 `edit` 動作:
```
def edit
@article = Article.find(params[:id])
end
```
視圖中要添加一個類似新建文章的表單。新建 `app/views/articles/edit.html.erb` 文件,寫入下面的代碼:
```
<h1>Editing article</h1>
<%= form_for :article, url: article_path(@article), method: :patch do |f| %>
<% if @article.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@article.errors.count, "error") %> prohibited
this article from being saved:</h2>
<ul>
<% @article.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<p>
<%= f.label :title %><br>
<%= f.text_field :title %>
</p>
<p>
<%= f.label :text %><br>
<%= f.text_area :text %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
<%= link_to 'Back', articles_path %>
```
這里的表單指向 `update` 動作,現在還沒定義,稍后會添加。
`method: :patch` 選項告訴 Rails,提交這個表單時使用 `PATCH` 方法發送請求。根據 REST 架構,更新資源時要使用 HTTP `PATCH` 方法。
`form_for` 的第一個參數可以是對象,例如 `@article`,把對象中的字段填入表單。如果傳入一個和實例變量(`@article`)同名的 Symbol(`:article`),效果也是一樣。上面的代碼使用的就是 Symbol。詳情參見 [form_for 的文檔](http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for)。
然后,要在 `app/controllers/articles_controller.rb` 中添加 `update` 動作:
```
def update
@article = Article.find(params[:id])
if @article.update(article_params)
redirect_to @article
else
render 'edit'
end
end
private
def article_params
params.require(:article).permit(:title, :text)
end
```
新定義的 `update` 方法用來處理對現有文章的更新操作,接收一個 Hash,包含想要修改的屬性。和之前一樣,如果更新文章出錯了,要再次顯示表單。
上面的代碼再次使用了前面為 `create` 動作定義的 `article_params` 方法。
不用把所有的屬性都提供給 `update` 動作。例如,如果使用 `@article.update(title: 'A new title')`,Rails 只會更新 `title` 屬性,不修改其他屬性。
最后,我們想在文章列表頁面,在每篇文章后面都加上一個鏈接,指向 `edit` 動作。打開 `app/views/articles/index.html.erb` 文件,在“Show”鏈接后面添加“Edit”鏈接:
```
<table>
<tr>
<th>Title</th>
<th>Text</th>
<th colspan="2"></th>
</tr>
<% @articles.each do |article| %>
<tr>
<td><%= article.title %></td>
<td><%= article.text %></td>
<td><%= link_to 'Show', article_path(article) %></td>
<td><%= link_to 'Edit', edit_article_path(article) %></td>
</tr>
<% end %>
</table>
```
我們還要在 `app/views/articles/show.html.erb` 模板的底部加上“Edit”鏈接:
```
...
<%= link_to 'Back', articles_path %>
| <%= link_to 'Edit', edit_article_path(@article) %>
```
下圖是文章列表頁面現在的樣子:

#### 5.12 使用局部視圖去掉視圖中的重復代碼
編輯文章頁面和新建文章頁面很相似,顯示表單的代碼是相同的。下面使用局部視圖去掉兩個視圖中的重復代碼。按照約定,局部視圖的文件名以下劃線開頭。
關于局部視圖的詳細介紹參閱“[Layouts and Rendering in Rails](/layouts_and_rendering.html)”一文。
新建 `app/views/articles/_form.html.erb` 文件,寫入以下代碼:
```
<%= form_for @article do |f| %>
<% if @article.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@article.errors.count, "error") %> prohibited
this article from being saved:</h2>
<ul>
<% @article.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<p>
<%= f.label :title %><br>
<%= f.text_field :title %>
</p>
<p>
<%= f.label :text %><br>
<%= f.text_area :text %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
```
除了第一行 `form_for` 的用法變了之外,其他代碼都和之前一樣。之所以能在兩個動作中共用一個 `form_for`,是因為 `@article` 是一個資源,對應于符合 REST 架構的路由,Rails 能自動分辨使用哪個地址和請求方法。
關于這種 `form_for` 用法的詳細說明,請查閱 [API 文檔](http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for-label-Resource-oriented+style)。
下面來修改 `app/views/articles/new.html.erb` 視圖,使用新建的局部視圖,把其中的代碼全刪掉,替換成:
```
<h1>New article</h1>
<%= render 'form' %>
<%= link_to 'Back', articles_path %>
```
然后按照同樣地方法修改 `app/views/articles/edit.html.erb` 視圖:
```
<h1>Edit article</h1>
<%= render 'form' %>
<%= link_to 'Back', articles_path %>
```
#### 5.13 刪除文章
現在介紹 CRUD 中的 D,從數據庫中刪除文章。按照 REST 架構的約定,刪除文章的路由是:
```
DELETE /articles/:id(.:format) articles#destroy
```
刪除資源時使用 DELETE 請求。如果還使用 GET 請求,可以構建如下所示的惡意地址:
```
<a href='http://example.com/articles/1/destroy'>look at this cat!</a>
```
刪除資源使用 DELETE 方法,路由會把請求發往 `app/controllers/articles_controller.rb` 中的 `destroy` 動作。`destroy` 動作現在還不存在,下面來添加:
```
def destroy
@article = Article.find(params[:id])
@article.destroy
redirect_to articles_path
end
```
想把記錄從數據庫刪除,可以在 Active Record 對象上調用 `destroy` 方法。注意,我們無需為這個動作編寫視圖,因為它會轉向 `index` 動作。
最后,在 `index` 動作的模板(`app/views/articles/index.html.erb`)中加上“Destroy”鏈接:
```
<h1>Listing Articles</h1>
<%= link_to 'New article', new_article_path %>
<table>
<tr>
<th>Title</th>
<th>Text</th>
<th colspan="3"></th>
</tr>
<% @articles.each do |article| %>
<tr>
<td><%= article.title %></td>
<td><%= article.text %></td>
<td><%= link_to 'Show', article_path(article) %></td>
<td><%= link_to 'Edit', edit_article_path(article) %></td>
<td><%= link_to 'Destroy', article_path(article),
method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</table>
```
生成“Destroy”鏈接的 `link_to` 用法有點不一樣,第二個參數是具名路由,隨后還傳入了幾個參數。`:method` 和 `:'data-confirm'` 選項設置鏈接的 HTML5 屬性,點擊鏈接后,首先會顯示一個對話框,然后發起 DELETE 請求。這兩個操作通過 `jquery_ujs` 這個 JavaScript 腳本實現。生成程序骨架時,會自動把 `jquery_ujs` 加入程序的布局中(`app/views/layouts/application.html.erb`)。沒有這個腳本,就不會顯示確認對話框。

恭喜,現在你可以新建、顯示、列出、更新、刪除文章了。
一般情況下,Rails 建議使用資源對象,而不手動設置路由。關于路由的詳細介紹參閱“[Rails 路由全解](/routing.html)”一文。
### 6 添加第二個模型
接下來要在程序中添加第二個模型,用來處理文章的評論。
#### 6.1 生成模型
下面要用到的生成器,和之前生成 `Article` 模型的一樣。我們要創建一個 `Comment` 模型,表示文章的評論。在終端執行下面的命令:
```
$ rails generate model Comment commenter:string body:text article:references
```
這個命令生成四個文件:
| 文件 | 作用 |
| --- | --- |
| db/migrate/20140120201010_create_comments.rb | 生成 comments 表所用的遷移文件(你得到的文件名稍有不同) |
| app/models/comment.rb | Comment 模型文件 |
| test/models/comment_test.rb | Comment 模型的測試文件 |
| test/fixtures/comments.yml | 測試時使用的固件 |
首先來看一下 `app/models/comment.rb` 文件:
```
class Comment < ActiveRecord::Base
belongs_to :article
end
```
文件的內容和前面的 `Article` 模型差不多,不過多了一行代碼:`belongs_to :article`。這行代碼用來建立 Active Record 關聯。下文會簡單介紹關聯。
除了模型文件,Rails 還生成了一個遷移文件,用來創建對應的數據表:
```
class CreateComments < ActiveRecord::Migration
def change
create_table :comments do |t|
t.string :commenter
t.text :body
# this line adds an integer column called `article_id`.
t.references :article, index: true
t.timestamps
end
end
end
```
`t.references` 這行代碼為兩個模型的關聯創建一個外鍵字段,同時還為這個字段創建了索引。下面運行這個遷移:
```
$ rake db:migrate
```
Rails 相當智能,只會執行還沒有運行的遷移,在命令行中會看到以下輸出:
```
== CreateComments: migrating =================================================
-- create_table(:comments)
-> 0.0115s
== CreateComments: migrated (0.0119s) ========================================
```
#### 6.2 模型關聯
使用 Active Record 關聯可以輕易的建立兩個模型之間的關系。評論和文章之間的關聯是這樣的:
* 評論屬于一篇文章
* 一篇文章有多個評論
這種關系和 Rails 用來聲明關聯的句法具有相同的邏輯。我們已經看過 `Comment` 模型中那行代碼,聲明評論屬于文章:
```
class Comment < ActiveRecord::Base
belongs_to :article
end
```
我們要編輯 `app/models/article.rb` 文件,加入這層關系的另一端:
```
class Article < ActiveRecord::Base
has_many :comments
validates :title, presence: true,
length: { minimum: 5 }
end
```
這兩行聲明能自動完成很多操作。例如,如果實例變量 `@article` 是一個文章對象,可以使用 `@article.comments` 取回一個數組,其元素是這篇文章的評論。
關于 Active Record 關聯的詳細介紹,參閱“[Active Record 關聯](/association_basics.html)”一文。
#### 6.3 添加評論的路由
和 `article` 控制器一樣,添加路由后 Rails 才知道在哪個地址上查看評論。打開 `config/routes.rb` 文件,按照下面的方式修改:
```
resources :articles do
resources :comments
end
```
我們把 `comments` 放在 `articles` 中,這叫做嵌套資源,表明了文章和評論間的層級關系。
關于路由的詳細介紹,參閱“[Rails 路由全解](/routing.html)”一文。
#### 6.4 生成控制器
有了模型,下面要創建控制器了,還是使用前面用過的生成器:
```
$ rails generate controller Comments
```
這個命令生成六個文件和一個空文件夾:
| 文件/文件夾 | 作用 |
| --- | --- |
| app/controllers/comments_controller.rb | Comments 控制器文件 |
| app/views/comments/ | 控制器的視圖存放在這個文件夾里 |
| test/controllers/comments_controller_test.rb | 控制器測試文件 |
| app/helpers/comments_helper.rb | 視圖幫助方法文件 |
| test/helpers/comments_helper_test.rb | 幫助方法測試文件 |
| app/assets/javascripts/comment.js.coffee | 控制器的 CoffeeScript 文件 |
| app/assets/stylesheets/comment.css.scss | 控制器的樣式表文件 |
在任何一個博客中,讀者讀完文章后就可以發布評論。評論發布后,會轉向文章顯示頁面,查看自己的評論是否顯示出來了。所以,`CommentsController` 中要定義新建評論的和刪除垃圾評論的方法。
首先,修改顯示文章的模板(`app/views/articles/show.html.erb`),允許讀者發布評論:
```
<p>
<strong>Title:</strong>
<%= @article.title %>
</p>
<p>
<strong>Text:</strong>
<%= @article.text %>
</p>
<h2>Add a comment:</h2>
<%= form_for([@article, @article.comments.build]) do |f| %>
<p>
<%= f.label :commenter %><br>
<%= f.text_field :commenter %>
</p>
<p>
<%= f.label :body %><br>
<%= f.text_area :body %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
<%= link_to 'Back', articles_path %>
| <%= link_to 'Edit', edit_article_path(@article) %>
```
上面的代碼在顯示文章的頁面添加了一個表單,調用 `CommentsController` 控制器的 `create` 動作發布評論。`form_for` 的參數是個數組,構建嵌套路由,例如 `/articles/1/comments`。
下面在 `app/controllers/comments_controller.rb` 文件中定義 `create` 方法:
```
class CommentsController < ApplicationController
def create
@article = Article.find(params[:article_id])
@comment = @article.comments.create(comment_params)
redirect_to article_path(@article)
end
private
def comment_params
params.require(:comment).permit(:commenter, :body)
end
end
```
這里使用的代碼要比文章的控制器復雜得多,因為設置了嵌套關系,必須這么做評論功能才能使用。發布評論時要知道這個評論屬于哪篇文章,所以要在 `Article` 模型上調用 `find` 方法查找文章對象。
而且,這段代碼還充分利用了關聯關系生成的方法。我們在 `@article.comments` 上調用 `create` 方法,創建并保存評論。這么做能自動把評論和文章聯系起來,讓這個評論屬于這篇文章。
添加評論后,調用 `article_path(@article)` 幫助方法,轉向原來的文章頁面。前面說過,這個幫助函數調用 `ArticlesController` 的 `show` 動作,渲染 `show.html.erb` 模板。我們要在這個模板中顯示評論,所以要修改一下 `app/views/articles/show.html.erb`:
```
<p>
<strong>Title:</strong>
<%= @article.title %>
</p>
<p>
<strong>Text:</strong>
<%= @article.text %>
</p>
<h2>Comments</h2>
<% @article.comments.each do |comment| %>
<p>
<strong>Commenter:</strong>
<%= comment.commenter %>
</p>
<p>
<strong>Comment:</strong>
<%= comment.body %>
</p>
<% end %>
<h2>Add a comment:</h2>
<%= form_for([@article, @article.comments.build]) do |f| %>
<p>
<%= f.label :commenter %><br>
<%= f.text_field :commenter %>
</p>
<p>
<%= f.label :body %><br>
<%= f.text_area :body %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
<%= link_to 'Edit Article', edit_article_path(@article) %> |
<%= link_to 'Back to Articles', articles_path %>
```
現在,可以為文章添加評論了,成功添加后,評論會在正確的位置顯示。

### 7 重構
現在博客的文章和評論都能正常使用了。看一下 `app/views/articles/show.html.erb` 模板,內容太多。下面使用局部視圖重構。
#### 7.1 渲染局部視圖中的集合
首先,把顯示文章評論的代碼抽出來,寫入局部視圖中。新建 `app/views/comments/_comment.html.erb` 文件,寫入下面的代碼:
```
<p>
<strong>Commenter:</strong>
<%= comment.commenter %>
</p>
<p>
<strong>Comment:</strong>
<%= comment.body %>
</p>
```
然后把 `app/views/articles/show.html.erb` 修改成:
```
<p>
<strong>Title:</strong>
<%= @article.title %>
</p>
<p>
<strong>Text:</strong>
<%= @article.text %>
</p>
<h2>Comments</h2>
<%= render @article.comments %>
<h2>Add a comment:</h2>
<%= form_for([@article, @article.comments.build]) do |f| %>
<p>
<%= f.label :commenter %><br>
<%= f.text_field :commenter %>
</p>
<p>
<%= f.label :body %><br>
<%= f.text_area :body %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
<%= link_to 'Edit Article', edit_article_path(@article) %> |
<%= link_to 'Back to Articles', articles_path %>
```
這個視圖會使用局部視圖 `app/views/comments/_comment.html.erb` 渲染 `@article.comments` 集合中的每個評論。`render` 方法會遍歷 `@article.comments` 集合,把每個評論賦值給一個和局部視圖同名的本地變量,在這個例子中本地變量是 `comment`,這個本地變量可以在局部視圖中使用。
#### 7.2 渲染局部視圖中的表單
我們把添加評論的代碼也移到局部視圖中。新建 `app/views/comments/_form.html.erb` 文件,寫入:
```
<%= form_for([@article, @article.comments.build]) do |f| %>
<p>
<%= f.label :commenter %><br>
<%= f.text_field :commenter %>
</p>
<p>
<%= f.label :body %><br>
<%= f.text_area :body %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
```
然后把 `app/views/articles/show.html.erb` 改成:
```
<p>
<strong>Title:</strong>
<%= @article.title %>
</p>
<p>
<strong>Text:</strong>
<%= @article.text %>
</p>
<h2>Comments</h2>
<%= render @article.comments %>
<h2>Add a comment:</h2>
<%= render "comments/form" %>
<%= link_to 'Edit Article', edit_article_path(@article) %> |
<%= link_to 'Back to Articles', articles_path %>
```
第二個 `render` 方法的參數就是要渲染的局部視圖,即 `comments/form`。Rails 很智能,能解析其中的斜線,知道要渲染 `app/views/comments` 文件夾中的 `_form.html.erb` 模板。
`@article` 變量在所有局部視圖中都可使用,因為它是實例變量。
### 8 刪除評論
博客還有一個重要的功能是刪除垃圾評論。為了實現這個功能,要在視圖中添加一個鏈接,并在 `CommentsController` 中定義 `destroy` 動作。
先在 `app/views/comments/_comment.html.erb` 局部視圖中加入刪除評論的鏈接:
```
<p>
<strong>Commenter:</strong>
<%= comment.commenter %>
</p>
<p>
<strong>Comment:</strong>
<%= comment.body %>
</p>
<p>
<%= link_to 'Destroy Comment', [comment.article, comment],
method: :delete,
data: { confirm: 'Are you sure?' } %>
</p>
```
點擊“Destroy Comment”鏈接后,會向 `CommentsController` 控制器發起 `DELETE /articles/:article_id/comments/:id` 請求。我們可以從這個請求中找到要刪除的評論。下面在控制器中加入 `destroy` 動作(`app/controllers/comments_controller.rb`):
```
class CommentsController < ApplicationController
def create
@article = Article.find(params[:article_id])
@comment = @article.comments.create(comment_params)
redirect_to article_path(@article)
end
def destroy
@article = Article.find(params[:article_id])
@comment = @article.comments.find(params[:id])
@comment.destroy
redirect_to article_path(@article)
end
private
def comment_params
params.require(:comment).permit(:commenter, :body)
end
end
```
`destroy` 動作先查找當前文章,然后在 `@article.comments` 集合中找到對應的評論,將其從數據庫中刪掉,最后轉向顯示文章的頁面。
#### 8.1 刪除關聯對象
如果刪除一篇文章,也要刪除文章中的評論,不然這些評論會占用數據庫空間。在 Rails 中可以在關聯中指定 `dependent` 選項達到這一目的。把 `Article` 模型(`app/models/article.rb`)修改成:
```
class Article < ActiveRecord::Base
has_many :comments, dependent: :destroy
validates :title, presence: true,
length: { minimum: 5 }
end
```
### 9 安全
#### 9.1 基本認證
如果把這個博客程序放在網上,所有人都能添加、編輯、刪除文章和評論。
Rails 提供了一種簡單的 HTTP 身份認證機制可以避免出現這種情況。
在 `ArticlesController` 中,我們要用一種方法禁止未通過認證的用戶訪問其中幾個動作。我們需要的是 `http_basic_authenticate_with` 方法,通過這個方法的認證后才能訪問所請求的動作。
要使用這個身份認證機制,需要在 `ArticlesController` 控制器的頂部調用 `http_basic_authenticate_with` 方法。除了 `index` 和 `show` 動作,訪問其他動作都要通過認證,所以在 `app/controllers/articles_controller.rb` 中,要這么做:
```
class ArticlesController < ApplicationController
http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show]
def index
@articles = Article.all
end
# snipped for brevity
```
同時,我們還希望只有通過認證的用戶才能刪除評論。修改 `CommentsController` 控制器(`app/controllers/comments_controller.rb`):
```
class CommentsController < ApplicationController
http_basic_authenticate_with name: "dhh", password: "secret", only: :destroy
def create
@article = Article.find(params[:article_id])
...
end
# snipped for brevity
```
現在,如果想新建文章,會看到一個 HTTP 基本認證對話框。

其他的身份認證方法也可以在 Rails 程序中使用。其中兩個比較流行的是 [Devise](https://github.com/plataformatec/devise) 引擎和 [Authlogic](https://github.com/binarylogic/authlogic) gem。
#### 9.2 其他安全注意事項
安全,尤其是在網頁程序中,是個很寬泛和值得深入研究的領域。Rails 程序的安全措施,在“[Ruby on Rails 安全指南](/security.html)”中有更深入的說明。
### 10 接下來做什么
至此,我們開發了第一個 Rails 程序,請盡情地修改、試驗。在開發過程中難免會需要幫助,如果使用 Rails 時需要協助,可以使用這些資源:
* [Ruby on Rails 指南](http://guides.ruby-china.org/index.html)
* [Ruby on Rails 教程](http://railstutorial-china.org)
* [Ruby on Rails 郵件列表](http://groups.google.com/group/rubyonrails-talk)
* irc.freenode.net 上的 [#rubyonrails](irc://irc.freenode.net/#rubyonrails) 頻道
Rails 本身也提供了幫助文檔,可以使用下面的 rake 任務生成:
* 運行 `rake doc:guides`,會在程序的 `doc/guides` 文件夾中生成一份 Rails 指南。在瀏覽器中打開 `doc/guides/index.html` 可以查看這份指南。
* 運行 `rake doc:rails`,會在程序的 `doc/api` 文件夾中生成一份完整的 API 文檔。在瀏覽器中打開 `doc/api/index.html` 可以查看 API 文檔。
使用 `doc:guides` 任務在本地生成 Rails 指南,要安裝 RedCloth gem。在 `Gemfile` 中加入這個 gem,然后執行 `bundle install` 命令即可。
### 11 常見問題
使用 Rails 時,最好使用 UTF-8 編碼存儲所有外部數據。如果沒使用 UTF-8 編碼,Ruby 的代碼庫和 Rails 一般都能將其轉換成 UTF-8,但不一定總能成功,所以最好還是確保所有的外部數據都使用 UTF-8 編碼。
如果編碼出錯,常見的征兆是瀏覽器中顯示很多黑色方塊和問號。還有一種常見的符號是“??”,包含在“ü”中。Rails 內部采用很多方法盡量避免出現這種問題。如果你使用的外部數據編碼不是 UTF-8,有時會出現這些問題,Rails 無法自動糾正。
非 UTF-8 編碼的數據經常來源于:
* 你的文本編輯器:大多數文本編輯器(例如 TextMate)默認使用 UTF-8 編碼保存文件。如果你的編輯器沒使用 UTF-8 編碼,有可能是你在模板中輸入了特殊字符(例如 é),在瀏覽器中顯示為方塊和問號。這種問題也會出現在國際化文件中。默認不使用 UTF-8 保存文件的編輯器(例如 Dreamweaver 的某些版本)都會提供一種方法,把默認編碼設為 UTF-8。記得要修改。
* 你的數據庫:默認情況下,Rails 會把從數據庫中取出的數據轉換成 UTF-8 格式。如果數據庫內部不使用 UTF-8 編碼,就無法保存用戶輸入的所有字符。例如,數據庫內部使用 Latin-1 編碼,用戶輸入俄語、希伯來語或日語字符時,存進數據庫時就會永遠丟失。如果可能,在數據庫中盡量使用 UTF-8 編碼。
### 反饋
歡迎幫忙改善指南質量。
如發現任何錯誤,歡迎修正。開始貢獻前,可先行閱讀[貢獻指南:文檔](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation)。
翻譯如有錯誤,深感抱歉,歡迎 [Fork](https://github.com/ruby-china/guides/fork) 修正,或至此處[回報](https://github.com/ruby-china/guides/issues/new)。
文章可能有未完成或過時的內容。請先檢查 [Edge Guides](http://edgeguides.rubyonrails.org) 來確定問題在 master 是否已經修掉了。再上 master 補上缺少的文件。內容參考 [Ruby on Rails 指南準則](ruby_on_rails_guides_guidelines.html)來了解行文風格。
最后,任何關于 Ruby on Rails 文檔的討論,歡迎到 [rubyonrails-docs 郵件群組](http://groups.google.com/group/rubyonrails-docs)。
- Ruby on Rails 指南 (651bba1)
- 入門
- Rails 入門
- 模型
- Active Record 基礎
- Active Record 數據庫遷移
- Active Record 數據驗證
- Active Record 回調
- Active Record 關聯
- Active Record 查詢
- 視圖
- Action View 基礎
- Rails 布局和視圖渲染
- 表單幫助方法
- 控制器
- Action Controller 簡介
- Rails 路由全解
- 深入
- Active Support 核心擴展
- Rails 國際化 API
- Action Mailer 基礎
- Active Job 基礎
- Rails 程序測試指南
- Rails 安全指南
- 調試 Rails 程序
- 設置 Rails 程序
- Rails 命令行
- Rails 緩存簡介
- Asset Pipeline
- 在 Rails 中使用 JavaScript
- 引擎入門
- Rails 應用的初始化過程
- Autoloading and Reloading Constants
- 擴展 Rails
- Rails 插件入門
- Rails on Rack
- 個性化Rails生成器與模板
- Rails應用模版
- 貢獻 Ruby on Rails
- Contributing to Ruby on Rails
- API Documentation Guidelines
- Ruby on Rails Guides Guidelines
- Ruby on Rails 維護方針
- 發布記
- A Guide for Upgrading Ruby on Rails
- Ruby on Rails 4.2 發布記
- Ruby on Rails 4.1 發布記
- Ruby on Rails 4.0 Release Notes
- Ruby on Rails 3.2 Release Notes
- Ruby on Rails 3.1 Release Notes
- Ruby on Rails 3.0 Release Notes
- Ruby on Rails 2.3 Release Notes
- Ruby on Rails 2.2 Release Notes