# Action Controller 簡介
本文介紹控制器的工作原理,以及控制器在程序請求周期內扮演的角色。
讀完本文,你將學到:
* 請求如何進入控制器;
* 如何限制傳入控制器的參數;
* 為什么以及如何把數據存儲在會話或 cookie 中;
* 處理請求時,如何使用過濾器執行代碼;
* 如何使用 Action Controller 內建的 HTTP 身份認證功能;
* 如何把數據流直發送給用戶的瀏覽器;
* 如何過濾敏感信息,不寫入程序的日志;
* 如何處理請求過程中可能出現的異常;
### Chapters
1. [控制器的作用](#%E6%8E%A7%E5%88%B6%E5%99%A8%E7%9A%84%E4%BD%9C%E7%94%A8)
2. [控制器命名約定](#%E6%8E%A7%E5%88%B6%E5%99%A8%E5%91%BD%E5%90%8D%E7%BA%A6%E5%AE%9A)
3. [方法和動作](#%E6%96%B9%E6%B3%95%E5%92%8C%E5%8A%A8%E4%BD%9C)
4. [參數](#%E5%8F%82%E6%95%B0)
* [Hash 和數組參數](#hash-%E5%92%8C%E6%95%B0%E7%BB%84%E5%8F%82%E6%95%B0)
* [JSON 參數](#json-%E5%8F%82%E6%95%B0)
* [路由參數](#%E8%B7%AF%E7%94%B1%E5%8F%82%E6%95%B0)
* [`default_url_options`](#default_url_options)
* [健壯參數](#%E5%81%A5%E5%A3%AE%E5%8F%82%E6%95%B0)
5. [會話](#%E4%BC%9A%E8%AF%9D)
* [獲取會話](#%E8%8E%B7%E5%8F%96%E4%BC%9A%E8%AF%9D)
* [Flash 消息](#flash-%E6%B6%88%E6%81%AF)
6. [Cookies](#cookies)
7. [渲染 XML 和 JSON 數據](#%E6%B8%B2%E6%9F%93-xml-%E5%92%8C-json-%E6%95%B0%E6%8D%AE)
8. [過濾器](#%E8%BF%87%E6%BB%A4%E5%99%A8)
* [后置過濾器和環繞過濾器](#%E5%90%8E%E7%BD%AE%E8%BF%87%E6%BB%A4%E5%99%A8%E5%92%8C%E7%8E%AF%E7%BB%95%E8%BF%87%E6%BB%A4%E5%99%A8)
* [過濾器的其他用法](#%E8%BF%87%E6%BB%A4%E5%99%A8%E7%9A%84%E5%85%B6%E4%BB%96%E7%94%A8%E6%B3%95)
9. [防止請求偽造](#%E9%98%B2%E6%AD%A2%E8%AF%B7%E6%B1%82%E4%BC%AA%E9%80%A0)
10. [`request` 和 `response` 對象](#request-%E5%92%8C-response-%E5%AF%B9%E8%B1%A1)
* [`request` 對象](#request-%E5%AF%B9%E8%B1%A1)
* [`response` 對象](#response-%E5%AF%B9%E8%B1%A1)
11. [HTTP 身份認證](#http-%E8%BA%AB%E4%BB%BD%E8%AE%A4%E8%AF%81)
* [HTTP 基本身份認證](#http-%E5%9F%BA%E6%9C%AC%E8%BA%AB%E4%BB%BD%E8%AE%A4%E8%AF%81)
* [HTTP 摘要身份認證](#http-%E6%91%98%E8%A6%81%E8%BA%AB%E4%BB%BD%E8%AE%A4%E8%AF%81)
12. [數據流和文件下載](#%E6%95%B0%E6%8D%AE%E6%B5%81%E5%92%8C%E6%96%87%E4%BB%B6%E4%B8%8B%E8%BD%BD)
* [發送文件](#%E5%8F%91%E9%80%81%E6%96%87%E4%BB%B6)
* [使用 REST 的方式下載文件](#%E4%BD%BF%E7%94%A8-rest-%E7%9A%84%E6%96%B9%E5%BC%8F%E4%B8%8B%E8%BD%BD%E6%96%87%E4%BB%B6)
* [任意數據的實時流](#%E4%BB%BB%E6%84%8F%E6%95%B0%E6%8D%AE%E7%9A%84%E5%AE%9E%E6%97%B6%E6%B5%81)
13. [過濾日志](#%E8%BF%87%E6%BB%A4%E6%97%A5%E5%BF%97)
* [過濾參數](#%E8%BF%87%E6%BB%A4%E5%8F%82%E6%95%B0)
* [過濾轉向](#%E8%BF%87%E6%BB%A4%E8%BD%AC%E5%90%91)
14. [異常處理](#%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86)
* [默認的 500 和 404 模板](#%E9%BB%98%E8%AE%A4%E7%9A%84-500-%E5%92%8C-404-%E6%A8%A1%E6%9D%BF)
* [`rescue_from`](#rescue_from)
15. [強制使用 HTTPS 協議](#%E5%BC%BA%E5%88%B6%E4%BD%BF%E7%94%A8-https-%E5%8D%8F%E8%AE%AE)
### 1 控制器的作用
Action Controller 是 MVC 中的 C(控制器)。路由決定使用哪個控制器處理請求后,控制器負責解析請求,生成對應的請求。Action Controller 會代為處理大多數底層工作,使用易懂的約定,讓整個過程清晰明了。
在大多數按照 [REST](http://en.wikipedia.org/wiki/Representational_state_transfer) 規范開發的程序中,控制器會接收請求(開發者不可見),從模型中獲取數據,或把數據寫入模型,再通過視圖生成 HTML。如果控制器需要做其他操作,也沒問題,以上只不過是控制器的主要作用。
因此,控制器可以視作模型和視圖的中間人,讓模型中的數據可以在視圖中使用,把數據顯示給用戶,再把用戶提交的數據保存或更新到模型中。
路由的處理細節請查閱 [Rails Routing From the Outside In](routing.html)。
### 2 控制器命名約定
Rails 控制器的命名習慣是,最后一個單詞使用**復數形式**,但也是有例外,比如 `ApplicationController`。例如:用 `ClientsController`,而不是 `ClientController`;用 `SiteAdminsController`,而不是 `SiteAdminController` 或 `SitesAdminsController`。
遵守這一約定便可享用默認的路由生成器(例如 `resources` 等),無需再指定 `:path` 或 `:controller`,URL 和路徑的幫助方法也能保持一致性。詳情參閱 [Layouts & Rendering Guide](layouts_and_rendering.html)。
控制器的命名習慣和模型不同,模型的名字習慣使用單數形式。
### 3 方法和動作
控制器是一個類,繼承自 `ApplicationController`,和其他類一樣,定義了很多方法。程序接到請求時,路由決定運行哪個控制器和哪個動作,然后創建該控制器的實例,運行和動作同名的方法。
```
class ClientsController < ApplicationController
def new
end
end
```
例如,用戶訪問 `/clients/new` 新建客戶,Rails 會創建一個 `ClientsController` 實例,運行 `new` 方法。注意,在上面這段代碼中,即使 `new` 方法是空的也沒關系,因為默認會渲染 `new.html.erb` 視圖,除非指定執行其他操作。在 `new` 方法中,聲明可在視圖中使用的 `@client` 實例變量,創建一個新的 `Client` 實例:
```
def new
@client = Client.new
end
```
詳情參閱 [Layouts & Rendering Guide](layouts_and_rendering.html)。
`ApplicationController` 繼承自 `ActionController::Base`。`ActionController::Base` 定義了很多實用方法。本文會介紹部分方法,如果想知道定義了哪些方法,可查閱 API 文檔或源碼。
只有公開方法才被視為動作。所以最好減少對外可見的方法數量,例如輔助方法和過濾器方法。
### 4 參數
在控制器的動作中,往往需要獲取用戶發送的數據,或其他參數。在網頁程序中參數分為兩類。第一類隨 URL 發送,叫做“請求參數”,即 URL 中 `?` 符號后面的部分。第二類經常成為“POST 數據”,一般來自用戶填寫的表單。之所以叫做“POST 數據”是因為,只能隨 HTTP POST 請求發送。Rails 不區分這兩種參數,在控制器中都可通過 `params` Hash 獲取:
```
class ClientsController < ApplicationController
# This action uses query string parameters because it gets run
# by an HTTP GET request, but this does not make any difference
# to the way in which the parameters are accessed. The URL for
# this action would look like this in order to list activated
# clients: /clients?status=activated
def index
if params[:status] == "activated"
@clients = Client.activated
else
@clients = Client.inactivated
end
end
# This action uses POST parameters. They are most likely coming
# from an HTML form which the user has submitted. The URL for
# this RESTful request will be "/clients", and the data will be
# sent as part of the request body.
def create
@client = Client.new(params[:client])
if @client.save
redirect_to @client
else
# This line overrides the default rendering behavior, which
# would have been to render the "create" view.
render "new"
end
end
end
```
#### 4.1 Hash 和數組參數
`params` Hash 不局限于只能使用一維鍵值對,其中可以包含數組和嵌套的 Hash。要發送數組,需要在鍵名后加上一對空方括號(`[]`):
```
GET /clients?ids[]=1&ids[]=2&ids[]=3
```
“[”和“]”這兩個符號不允許出現在 URL 中,所以上面的地址會被編碼成 `/clients?ids%5b%5d=1&ids%5b%5d=2&ids%5b%5d=3`。大多數情況下,無需你費心,瀏覽器會為你代勞編碼,接收到這樣的請求后,Rails 也會自動解碼。如果你要手動向服務器發送這樣的請求,就要留點心了。
此時,`params[:ids]` 的值是 `["1", "2", "3"]`。注意,參數的值始終是字符串,Rails 不會嘗試轉換類型。
默認情況下,基于安全考慮,參數中的 `[]`、`[nil]` 和 `[nil, nil, ...]` 會替換成 `nil`。詳情參閱[安全指南](security.html#unsafe-query-generation)。
要發送嵌套的 Hash 參數,需要在方括號內指定鍵名:
```
<form accept-charset="UTF-8" action="/clients" method="post">
<input type="text" name="client[name]" value="Acme" />
<input type="text" name="client[phone]" value="12345" />
<input type="text" name="client[address][postcode]" value="12345" />
<input type="text" name="client[address][city]" value="Carrot City" />
</form>
```
提交這個表單后,`params[:client]` 的值是 `{ "name" => "Acme", "phone" => "12345", "address" => { "postcode" => "12345", "city" => "Carrot City" } }`。注意 `params[:client][:address]` 是個嵌套 Hash。
注意,`params` Hash 其實是 `ActiveSupport::HashWithIndifferentAccess` 的實例,雖和普通的 Hash 一樣,但鍵名使用 Symbol 和字符串的效果一樣。
#### 4.2 JSON 參數
開發網頁服務程序時,你會發現,接收 JSON 格式的參數更容易處理。如果請求的 `Content-Type` 報頭是 `application/json`,Rails 會自動將其轉換成 `params` Hash,按照常規的方法使用:
例如,如果發送如下的 JSON 格式內容:
```
{ "company": { "name": "acme", "address": "123 Carrot Street" } }
```
得到的是 `params[:company]` 就是 `{ "name" => "acme", "address" => "123 Carrot Street" }`。
如果在初始化腳本中開啟了 `config.wrap_parameters` 選項,或者在控制器中調用了 `wrap_parameters` 方法,可以放心的省去 JSON 格式參數中的根鍵。Rails 會以控制器名新建一個鍵,復制參數,將其存入這個鍵名下。因此,上面的參數可以寫成:
```
{ "name": "acme", "address": "123 Carrot Street" }
```
假設數據傳送給 `CompaniesController`,那么參數會存入 `:company` 鍵名下:
```
{ name: "acme", address: "123 Carrot Street", company: { name: "acme", address: "123 Carrot Street" } }
```
如果想修改默認使用的鍵名,或者把其他參數存入其中,請參閱 [API 文檔](http://api.rubyonrails.org/classes/ActionController/ParamsWrapper.html)。
解析 XML 格式參數的功能現已抽出,制成了 gem,名為 `actionpack-xml_parser`。
#### 4.3 路由參數
`params` Hash 總有 `:controller` 和 `:action` 兩個鍵,但獲取這兩個值應該使用 `controller_name` 和 `action_name` 方法。路由中定義的參數,例如 `:id`,也可通過 `params` Hash 獲取。例如,假設有個客戶列表,可以列出激活和禁用的客戶。我們可以定義一個路由,捕獲下面這個 URL 中的 `:status` 參數:
```
get '/clients/:status' => 'clients#index', foo: 'bar'
```
在這個例子中,用戶訪問 `/clients/active` 時,`params[:status]` 的值是 `"active"`。同時,`params[:foo]` 的值也會被設為 `"bar"`,就像通過請求參數傳入的一樣。`params[:action]` 也是一樣,其值為 `"index"`。
#### 4.4 `default_url_options`
在控制器中定義名為 `default_url_options` 的方法,可以設置所生成 URL 中都包含的參數。這個方法必須返回一個 Hash,其值為所需的參數值,而且鍵必須使用 Symbol:
```
class ApplicationController < ActionController::Base
def default_url_options
{ locale: I18n.locale }
end
end
```
這個方法定義的只是預設參數,可以被 `url_for` 方法的參數覆蓋。
如果像上面的代碼一樣,在 `ApplicationController` 中定義 `default_url_options`,則會用于所有生成的 URL。`default_url_options` 也可以在具體的控制器中定義,只影響和該控制器有關的 URL。
#### 4.5 健壯參數
加入健壯參數功能后,Action Controller 的參數禁止在 Avtive Model 中批量賦值,除非參數在白名單中。也就是說,你要明確選擇那些屬性可以批量更新,避免意外把不該暴露的屬性暴露了。
而且,還可以標記哪些參數是必須傳入的,如果沒有收到,會交由 `raise/rescue` 處理,返回“400 Bad Request”。
```
class PeopleController < ActionController::Base
# This will raise an ActiveModel::ForbiddenAttributes exception
# because it's using mass assignment without an explicit permit
# step.
def create
Person.create(params[:person])
end
# This will pass with flying colors as long as there's a person key
# in the parameters, otherwise it'll raise a
# ActionController::ParameterMissing exception, which will get
# caught by ActionController::Base and turned into that 400 Bad
# Request reply.
def update
person = current_account.people.find(params[:id])
person.update!(person_params)
redirect_to person
end
private
# Using a private method to encapsulate the permissible parameters
# is just a good pattern since you'll be able to reuse the same
# permit list between create and update. Also, you can specialize
# this method with per-user checking of permissible attributes.
def person_params
params.require(:person).permit(:name, :age)
end
end
```
##### 4.5.1 允許使用的標量值
假如允許傳入 `:id`:
```
params.permit(:id)
```
若 `params` 中有 `:id`,且 `:id` 是標量值,就可以通過白名單檢查,否則 `:id` 會被過濾掉。因此不能傳入數組、Hash 或其他對象。
允許使用的標量類型有:`String`、`Symbol`、`NilClass`、`Numeric`、`TrueClass`、`FalseClass`、`Date`、`Time`、`DateTime`、`StringIO`、`IO`、`ActionDispatch::Http::UploadedFile` 和 `Rack::Test::UploadedFile`。
要想指定 `params` 中的值必須為數組,可以把鍵對應的值設為空數組:
```
params.permit(id: [])
```
要想允許傳入整個參數 Hash,可以使用 `permit!` 方法:
```
params.require(:log_entry).permit!
```
此時,允許傳入整個 `:log_entry` Hash 及嵌套 Hash。使用 `permit!` 時要特別注意,因為這么做模型中所有當前屬性及后續添加的屬性都允許進行批量賦值。
##### 4.5.2 嵌套參數
也可以允許傳入嵌套參數,例如:
```
params.permit(:name, { emails: [] },
friends: [ :name,
{ family: [ :name ], hobbies: [] }])
```
此時,允許傳入 `name`,`emails` 和 `friends` 屬性。其中,`emails` 必須是數組;`friends` 必須是一個由資源組成的數組:應該有個 `name` 屬性,還要有 `hobbies` 屬性,其值是由標量組成的數組,以及一個 `family` 屬性,其值只能包含 `name` 屬性(任何允許使用的標量值)。
##### 4.5.3 更多例子
你可能還想在 `new` 動作中限制允許傳入的屬性。不過此時無法再根鍵上調用 `require` 方法,因為此時根鍵還不存在:
```
# using `fetch` you can supply a default and use
# the Strong Parameters API from there.
params.fetch(:blog, {}).permit(:title, :author)
```
使用 `accepts_nested_attributes_for` 方法可以更新或銷毀響應的記錄。這個方法基于 `id` 和 `_destroy` 參數:
```
# permit :id and :_destroy
params.require(:author).permit(:name, books_attributes: [:title, :id, :_destroy])
```
如果 Hash 的鍵是數字,處理方式有所不同,此時可以把屬性作為 Hash 的直接子 Hash。`accepts_nested_attributes_for` 和 `has_many` 關聯同時使用時會得到這種參數:
```
# To whitelist the following data:
# {"book" => {"title" => "Some Book",
# "chapters_attributes" => { "1" => {"title" => "First Chapter"},
# "2" => {"title" => "Second Chapter"}}}}
params.require(:book).permit(:title, chapters_attributes: [:title])
```
##### 4.5.4 不用健壯參數
健壯參數的目的是為了解決常見問題,不是萬用良藥。不過,可以很方便的和自己的代碼結合,解決復雜需求。
假設有個參數包含產品的名字和一個由任意數據組成的產品附加信息 Hash,希望過濾產品名和整個附加數據 Hash。健壯參數不能過濾由任意鍵值組成的嵌套 Hash,不過可以使用嵌套 Hash 的鍵定義過濾規則:
```
def product_params
params.require(:product).permit(:name, data: params[:product][:data].try(:keys))
end
```
### 5 會話
程序中的每個用戶都有一個會話(session),可以存儲少量數據,在多次請求中永久存儲。會話只能在控制器和視圖中使用,可以通過以下幾種存儲機制實現:
* `ActionDispatch::Session::CookieStore`:所有數據都存儲在客戶端
* `ActionDispatch::Session::CacheStore`:數據存儲在 Rails 緩存里
* `ActionDispatch::Session::ActiveRecordStore`:使用 Active Record 把數據存儲在數據庫中(需要使用 `activerecord-session_store` gem)
* `ActionDispatch::Session::MemCacheStore`:數據存儲在 Memcached 集群中(這是以前的實現方式,現在請改用 CacheStore)
所有存儲機制都會用到一個 cookie,存儲每個會話的 ID(必須使用 cookie,因為 Rails 不允許在 URL 中傳遞會話 ID,這么做不安全)。
大多數存儲機制都會使用這個 ID 在服務商查詢會話數據,例如在數據庫中查詢。不過有個例外,即默認也是推薦使用的存儲方式 CookieStore。CookieStore 把所有會話數據都存儲在 cookie 中(如果需要,還是可以使用 ID)。CookieStore 的優點是輕量,而且在新程序中使用會話也不用額外的設置。cookie 中存儲的數據會使用密令簽名,以防篡改。cookie 會被加密,任何有權訪問的人都無法讀取其內容。(如果修改了 cookie,Rails 會拒絕使用。)
CookieStore 可以存儲大約 4KB 數據,比其他幾種存儲機制都少很多,但一般也足夠用了。不過使用哪種存儲機制,都不建議在會話中存儲大量數據。應該特別避免在會話中存儲復雜的對象(Ruby 基本對象之外的一切對象,最常見的是模型實例),服務器可能無法在多次請求中重組數據,最終導致錯誤。
如果會話中沒有存儲重要的數據,或者不需要持久存儲(例如使用 Falsh 存儲消息),可以考慮使用 `ActionDispatch::Session::CacheStore`。這種存儲機制使用程序所配置的緩存方式。CacheStore 的優點是,可以直接使用現有的緩存方式存儲會話,不用額外的設置。不過缺點也很明顯,會話存在時間很多,隨時可能消失。
關于會話存儲的更多內容請參閱[安全指南](security.html)
如果想使用其他的會話存儲機制,可以在 `config/initializers/session_store.rb` 文件中設置:
```
# Use the database for sessions instead of the cookie-based default,
# which shouldn't be used to store highly confidential information
# (create the session table with "rails g active_record:session_migration")
# YourApp::Application.config.session_store :active_record_store
```
簽署會話數據時,Rails 會用到會話的鍵(cookie 的名字),這個值可以在 `config/initializers/session_store.rb` 中修改:
```
# Be sure to restart your server when you modify this file.
YourApp::Application.config.session_store :cookie_store, key: '_your_app_session'
```
還可以傳入 `:domain` 鍵,指定可使用此 cookie 的域名:
```
# Be sure to restart your server when you modify this file.
YourApp::Application.config.session_store :cookie_store, key: '_your_app_session', domain: ".example.com"
```
Rails 為 CookieStore 提供了一個密令,用來簽署會話數據。這個密令可以在 `config/secrets.yml` 文件中修改:
```
# Be sure to restart your server when you modify this file.
# Your secret key is used for verifying the integrity of signed cookies.
# If you change this key, all old signed cookies will become invalid!
# Make sure the secret is at least 30 characters and all random,
# no regular words or you'll be exposed to dictionary attacks.
# You can use `rake secret` to generate a secure secret key.
# Make sure the secrets in this file are kept private
# if you're sharing your code publicly.
development:
secret_key_base: a75d...
test:
secret_key_base: 492f...
# Do not keep production secrets in the repository,
# instead read values from the environment.
production:
secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
```
使用 CookieStore 時,如果修改了密令,之前所有的會話都會失效。
#### 5.1 獲取會話
在控制器中,可以使用實例方法 `session` 獲取會話。
會話是惰性加載的,如果不在動作中獲取,不會自動加載。因此無需禁用會話,不獲取即可。
會話中的數據以鍵值對的形式存儲,類似 Hash:
```
class ApplicationController < ActionController::Base
private
# Finds the User with the ID stored in the session with the key
# :current_user_id This is a common way to handle user login in
# a Rails application; logging in sets the session value and
# logging out removes it.
def current_user
@_current_user ||= session[:current_user_id] &&
User.find_by(id: session[:current_user_id])
end
end
```
要想把數據存入會話,像 Hash 一樣,給鍵賦值即可:
```
class LoginsController < ApplicationController
# "Create" a login, aka "log the user in"
def create
if user = User.authenticate(params[:username], params[:password])
# Save the user ID in the session so it can be used in
# subsequent requests
session[:current_user_id] = user.id
redirect_to root_url
end
end
end
```
要從會話中刪除數據,把鍵的值設為 `nil` 即可:
```
class LoginsController < ApplicationController
# "Delete" a login, aka "log the user out"
def destroy
# Remove the user id from the session
@_current_user = session[:current_user_id] = nil
redirect_to root_url
end
end
```
要重設整個會話,請使用 `reset_session` 方法。
#### 5.2 Flash 消息
Flash 是會話的一個特殊部分,每次請求都會清空。也就是說,其中存儲的數據只能在下次請求時使用,可用來傳遞錯誤消息等。
Flash 消息的獲取方式和會話差不多,類似 Hash。Flash 消息是 [FlashHash](http://api.rubyonrails.org/classes/ActionDispatch/Flash/FlashHash.html) 實例。
下面以退出登錄為例。控制器可以發送一個消息,在下一次請求時顯示:
```
class LoginsController < ApplicationController
def destroy
session[:current_user_id] = nil
flash[:notice] = "You have successfully logged out."
redirect_to root_url
end
end
```
注意,Flash 消息還可以直接在轉向中設置。可以指定 `:notice`、`:alert` 或者常規的 `:flash`:
```
redirect_to root_url, notice: "You have successfully logged out."
redirect_to root_url, alert: "You're stuck here!"
redirect_to root_url, flash: { referral_code: 1234 }
```
上例中,`destroy` 動作轉向程序的 `root_url`,然后顯示 Flash 消息。注意,只有下一個動作才能處理前一個動作中設置的 Flash 消息。一般都會在程序的布局中加入顯示警告或提醒 Flash 消息的代碼:
```
<html>
<!-- <head/> -->
<body>
<% flash.each do |name, msg| -%>
<%= content_tag :div, msg, class: name %>
<% end -%>
<!-- more content -->
</body>
</html>
```
如此一來,如果動作中設置了警告或提醒消息,就會出現在布局中。
Flash 不局限于警告和提醒,可以設置任何可在會話中存儲的內容:
```
<% if flash[:just_signed_up] %>
<p class="welcome">Welcome to our site!</p>
<% end %>
```
如果希望 Flash 消息保留到其他請求,可以使用 `keep` 方法:
```
class MainController < ApplicationController
# Let's say this action corresponds to root_url, but you want
# all requests here to be redirected to UsersController#index.
# If an action sets the flash and redirects here, the values
# would normally be lost when another redirect happens, but you
# can use 'keep' to make it persist for another request.
def index
# Will persist all flash values.
flash.keep
# You can also use a key to keep only some kind of value.
# flash.keep(:notice)
redirect_to users_url
end
end
```
##### 5.2.1 `flash.now`
默認情況下,Flash 中的內容只在下一次請求中可用,但有時希望在同一個請求中使用。例如,`create` 動作沒有成功保存資源時,會直接渲染 `new` 模板,這并不是一個新請求,但卻希望希望顯示一個 Flash 消息。針對這種情況,可以使用 `flash.now`,用法和 `flash` 一樣:
```
class ClientsController < ApplicationController
def create
@client = Client.new(params[:client])
if @client.save
# ...
else
flash.now[:error] = "Could not save client"
render action: "new"
end
end
end
```
### 6 Cookies
程序可以在客戶端存儲少量數據(稱為 cookie),在多次請求中使用,甚至可以用作會話。在 Rails 中可以使用 `cookies` 方法輕松獲取 cookies,用法和 `session` 差不多,就像一個 Hash:
```
class CommentsController < ApplicationController
def new
# Auto-fill the commenter's name if it has been stored in a cookie
@comment = Comment.new(author: cookies[:commenter_name])
end
def create
@comment = Comment.new(params[:comment])
if @comment.save
flash[:notice] = "Thanks for your comment!"
if params[:remember_name]
# Remember the commenter's name.
cookies[:commenter_name] = @comment.author
else
# Delete cookie for the commenter's name cookie, if any.
cookies.delete(:commenter_name)
end
redirect_to @comment.article
else
render action: "new"
end
end
end
```
注意,刪除會話中的數據是把鍵的值設為 `nil`,但要刪除 cookie 中的值,要使用 `cookies.delete(:key)` 方法。
Rails 還提供了簽名 cookie 和加密 cookie,用來存儲敏感數據。簽名 cookie 會在 cookie 的值后面加上一個簽名,確保值沒被修改。加密 cookie 除了會簽名之外,還會加密,讓終端用戶無法讀取。詳細信息請參閱 [API 文檔](http://api.rubyonrails.org/classes/ActionDispatch/Cookies.html)。
這兩種特殊的 cookie 會序列化簽名后的值,生成字符串,讀取時再反序列化成 Ruby 對象。
序列化所用的方式可以指定:
```
Rails.application.config.action_dispatch.cookies_serializer = :json
```
新程序默認使用的序列化方法是 `:json`。為了兼容以前程序中的 cookie,如果沒設定 `cookies_serializer`,就會使用 `:marshal`。
這個選項還可以設為 `:hybrid`,讀取時,Rails 會自動返序列化使用 `Marshal` 序列化的 cookie,寫入時使用 `JSON` 格式。把現有程序遷移到使用 `:json` 序列化方式時,這么設定非常方便。
序列化方式還可以使用其他方式,只要定義了 `load` 和 `dump` 方法即可:
```
Rails.application.config.action_dispatch.cookies_serializer = MyCustomSerializer
```
### 7 渲染 XML 和 JSON 數據
在 `ActionController` 中渲染 `XML` 和 `JSON` 數據非常簡單。使用腳手架生成的控制器如下所示:
```
class UsersController < ApplicationController
def index
@users = User.all
respond_to do |format|
format.html # index.html.erb
format.xml { render xml: @users}
format.json { render json: @users}
end
end
end
```
你可能注意到了,在這段代碼中,我們使用的是 `render xml: @users` 而不是 `render xml: @users.to_xml`。如果不是字符串對象,Rails 會自動調用 `to_xml` 方法。
### 8 過濾器
過濾器(filter)是一些方法,在控制器動作運行之前、之后,或者前后運行。
過濾器會繼承,如果在 `ApplicationController` 中定義了過濾器,那么程序的每個控制器都可使用。
前置過濾器有可能會終止請求循環。前置過濾器經常用來確保動作運行之前用戶已經登錄。這種過濾器的定義如下:
```
class ApplicationController < ActionController::Base
before_action :require_login
private
def require_login
unless logged_in?
flash[:error] = "You must be logged in to access this section"
redirect_to new_login_url # halts request cycle
end
end
end
```
如果用戶沒有登錄,這個方法會在 Flash 中存儲一個錯誤消息,然后轉向登錄表單頁面。如果前置過濾器渲染了頁面或者做了轉向,動作就不會運行。如果動作上還有后置過濾器,也不會運行。
在上面的例子中,過濾器在 `ApplicationController` 中定義,所以程序中的所有控制器都會繼承。程序中的所有頁面都要求用戶登錄后才能訪問。很顯然(這樣用戶根本無法登錄),并不是所有控制器或動作都要做這種限制。如果想跳過某個動作,可以使用 `skip_before_action`:
```
class LoginsController < ApplicationController
skip_before_action :require_login, only: [:new, :create]
end
```
此時,`LoginsController` 的 `new` 動作和 `create` 動作就不需要用戶先登錄。`:only` 選項的意思是只跳過這些動作。還有個 `:except` 選項,用法類似。定義過濾器時也可使用這些選項,指定只在選中的動作上運行。
#### 8.1 后置過濾器和環繞過濾器
除了前置過濾器之外,還可以在動作運行之后,或者在動作運行前后執行過濾器。
后置過濾器類似于前置過濾器,不過因為動作已經運行了,所以可以獲取即將發送給客戶端的響應數據。顯然,后置過濾器無法阻止運行動作。
環繞過濾器會把動作拉入(yield)過濾器中,工作方式類似 Rack 中間件。
例如,網站的改動需要經過管理員預覽,然后批準。可以把這些操作定義在一個事務中:
```
class ChangesController < ApplicationController
around_action :wrap_in_transaction, only: :show
private
def wrap_in_transaction
ActiveRecord::Base.transaction do
begin
yield
ensure
raise ActiveRecord::Rollback
end
end
end
end
```
注意,環繞過濾器還包含了渲染操作。在上面的例子中,視圖本身是從數據庫中讀取出來的(例如,通過作用域(scope)),讀取視圖的操作在事務中完成,然后提供預覽數據。
也可以不拉入動作,自己生成響應,不過這種情況不會運行動作。
#### 8.2 過濾器的其他用法
一般情況下,過濾器的使用方法是定義私有方法,然后調用相應的 `*_action` 方法添加過濾器。不過過濾器還有其他兩種用法。
第一種,直接在 `*_action` 方法中使用代碼塊。代碼塊接收控制器作為參數。使用這種方法,前面的 `require_login` 過濾器可以改寫成:
```
class ApplicationController < ActionController::Base
before_action do |controller|
unless controller.send(:logged_in?)
flash[:error] = "You must be logged in to access this section"
redirect_to new_login_url
end
end
end
```
注意,此時在過濾器中使用的是 `send` 方法,因為 `logged_in?` 是私有方法,而且過濾器和控制器不在同一作用域內。定義 `require_login` 過濾器不推薦使用這種方法,但比較簡單的過濾器可以這么用。
第二種,在類(其實任何能響應正確方法的對象都可以)中定義過濾器。這種方法用來實現復雜的過濾器,使用前面的兩種方法無法保證代碼可讀性和重用性。例如,可以在一個類中定義前面的 `require_login` 過濾器:
```
class ApplicationController < ActionController::Base
before_action LoginFilter
end
class LoginFilter
def self.before(controller)
unless controller.send(:logged_in?)
controller.flash[:error] = "You must be logged in to access this section"
controller.redirect_to controller.new_login_url
end
end
end
```
這種方法也不是定義 `require_login` 過濾器的理想方式,因為和控制器不在同一作用域,要把控制器作為參數傳入。定義過濾器的類,必須有一個和過濾器種類同名的方法。對于 `before_action` 過濾器,類中必須定義 `before` 方法。其他類型的過濾器以此類推。`around` 方法必須調用 `yield` 方法執行動作。
### 9 防止請求偽造
跨站請求偽造(CSRF)是一種攻擊方式,A 網站的用戶偽裝成 B 網站的用戶發送請求,在 B 站中添加、修改或刪除數據,而 B 站的用戶絕然不知。
防止這種攻擊的第一步是,確保所有析構動作(`create`,`update` 和 `destroy`)只能通過 GET 之外的請求方法訪問。如果遵從 REST 架構,已經完成了這一步。不過,惡意網站還是可以很輕易地發起非 GET 請求,這時就要用到其他防止跨站攻擊的方法了。
我們添加一個只有自己的服務器才知道的難以猜測的令牌。如果請求中沒有該令牌,就會禁止訪問。
如果使用下面的代碼生成一個表單:
```
<%= form_for @user do |f| %>
<%= f.text_field :username %>
<%= f.text_field :password %>
<% end %>
```
會看到 Rails 自動添加了一個隱藏字段:
```
<form accept-charset="UTF-8" action="/users/1" method="post">
<input type="hidden"
value="67250ab105eb5ad10851c00a5621854a23af5489"
name="authenticity_token"/>
<!-- username & password fields -->
</form>
```
所有使用[表單幫助方法](form_helpers.html)生成的表單,都有會添加這個令牌。如果想自己編寫表單,或者基于其他原因添加令牌,可以使用 `form_authenticity_token` 方法。
`form_authenticity_token` 會生成一個有效的令牌。在 Rails 沒有自動添加令牌的地方(例如 Ajax)可以使用這個方法。
[安全指南](security.html)一文更深入的介紹了請求偽造防范措施,還有一些開發網頁程序需要知道的安全隱患。
### 10 `request` 和 `response` 對象
在每個控制器中都有兩個存取器方法,分別用來獲取當前請求循環的請求對象和響應對象。`request` 方法的返回值是 `AbstractRequest` 對象的實例;`response` 方法的返回值是一個響應對象,表示回送客戶端的數據。
#### 10.1 `request` 對象
`request` 對象中有很多發自客戶端請求的信息。可用方法的完整列表參閱 [API 文檔](http://api.rubyonrails.org/classes/ActionDispatch/Request.html)。其中部分方法說明如下:
| `request` 對象的屬性 | 作用 |
| --- | --- |
| host | 請求發往的主機名 |
| domain(n=2) | 主機名的前 `n` 個片段,從頂級域名的右側算起 |
| format | 客戶端發起請求時使用的內容類型 |
| method | 請求使用的 HTTP 方法 |
| get?, post?, patch?, put?, delete?, head? | 如果 HTTP 方法是 GET/POST/PATCH/PUT/DELETE/HEAD,返回 `true` |
| headers | 返回一個 Hash,包含請求的報頭 |
| port | 請求發往的端口,整數類型 |
| protocol | 返回所用的協議外加 `"://"`,例如 `"http://"` |
| query_string | URL 中包含的請求參數,`?` 后面的字符串 |
| remote_ip | 客戶端的 IP 地址 |
| url | 請求發往的完整 URL |
##### 10.1.1 `path_parameters`,`query_parameters` 和 `request_parameters`
不過請求中的參數隨 URL 而來,而是通過表單提交,Rails 都會把這些參數存入 `params` Hash 中。`request` 對象中有三個存取器,用來獲取各種類型的參數。`query_parameters` Hash 中的參數來自 URL;`request_parameters` Hash 中的參數來自提交的表單;`path_parameters` Hash 中的參數來自路由,傳入相應的控制器和動作。
#### 10.2 `response` 對象
一般情況下不會直接使用 `response` 對象。`response` 對象在動作中渲染,把數據回送給客戶端。不過有時可能需要直接獲取響應,比如在后置過濾器中。`response` 對象上的方法很多都可以用來賦值。
| `response` 對象的數學 | 作用 |
| --- | --- |
| body | 回送客戶端的數據,字符串格式。大多數情況下是 HTML |
| status | 響應的 HTTP 狀態碼,例如,請求成功時是 200,文件未找到時是 404 |
| location | 轉向地址(如果轉向的話) |
| content_type | 響應的內容類型 |
| charset | 響應使用的字符集。默認是 `"utf-8"` |
| headers | 響應報頭 |
##### 10.2.1 設置自定義報頭
如果想設置自定義報頭,可以使用 `response.headers` 方法。報頭是一個 Hash,鍵為報頭名,值為報頭的值。Rails 會自動設置一些報頭,如果想添加或者修改報頭,賦值給 `response.headers` 即可,例如:
```
response.headers["Content-Type"] = "application/pdf"
```
注意,上面這段代碼直接使用 `content_type=` 方法更直接。
### 11 HTTP 身份認證
Rails 內建了兩種 HTTP 身份認證方式:
* 基本認證
* 摘要認證
#### 11.1 HTTP 基本身份認證
大多數瀏覽器和 HTTP 客戶端都支持 HTTP 基本身份認證。例如,在瀏覽器中如果要訪問只有管理員才能查看的頁面,就會出現一個對話框,要求輸入用戶名和密碼。使用內建的身份認證非常簡單,只要使用一個方法,即 `http_basic_authenticate_with`。
```
class AdminsController < ApplicationController
http_basic_authenticate_with name: "humbaba", password: "5baa61e4"
end
```
添加 `http_basic_authenticate_with` 方法后,可以創建具有命名空間的控制器,繼承自 `AdminsController`,`http_basic_authenticate_with` 方法會在這些控制器的所有動作運行之前執行,啟用 HTTP 基本身份認證。
#### 11.2 HTTP 摘要身份認證
HTTP 摘要身份認證比基本認證高級,因為客戶端不會在網絡中發送明文密碼(不過在 HTTPS 中基本認證是安全的)。在 Rails 中使用摘要認證非常簡單,只需使用一個方法,即 `authenticate_or_request_with_http_digest`。
```
class AdminsController < ApplicationController
USERS = { "lifo" => "world" }
before_action :authenticate
private
def authenticate
authenticate_or_request_with_http_digest do |username|
USERS[username]
end
end
end
```
如上面的代碼所示,`authenticate_or_request_with_http_digest` 方法的塊只接受一個參數,用戶名,返回值是密碼。如果 `authenticate_or_request_with_http_digest` 返回 `false` 或 `nil`,表明認證失敗。
### 12 數據流和文件下載
有時不想渲染 HTML 頁面,而要把文件發送給用戶。在所有的控制器中都可以使用 `send_data` 和 `send_file` 方法。這兩個方法都會以數據流的方式發送數據。`send_file` 方法很方便,只要提供硬盤中文件的名字,就會用數據流發送文件內容。
要想把數據以數據流的形式發送給客戶端,可以使用 `send_data` 方法:
```
require "prawn"
class ClientsController < ApplicationController
# Generates a PDF document with information on the client and
# returns it. The user will get the PDF as a file download.
def download_pdf
client = Client.find(params[:id])
send_data generate_pdf(client),
filename: "#{client.name}.pdf",
type: "application/pdf"
end
private
def generate_pdf(client)
Prawn::Document.new do
text client.name, align: :center
text "Address: #{client.address}"
text "Email: #{client.email}"
end.render
end
end
```
在上面的代碼中,`download_pdf` 動作調用私有方法 `generate_pdf`。`generate_pdf` 才是真正生成 PDF 的方法,返回值字符串形式的文件內容。返回的字符串會以數據流的形式發送給客戶端,并為用戶推薦一個文件名。有時發送文件流時,并不希望用戶下載這個文件,比如嵌在 HTML 頁面中的圖片。告訴瀏覽器文件不是用來下載的,可以把 `:disposition` 選項設為 `"inline"`。這個選項的另外一個值,也是默認值,是 `"attachment"`。
#### 12.1 發送文件
如果想發送硬盤上已經存在的文件,可以使用 `send_file` 方法。
```
class ClientsController < ApplicationController
# Stream a file that has already been generated and stored on disk.
def download_pdf
client = Client.find(params[:id])
send_file("#{Rails.root}/files/clients/#{client.id}.pdf",
filename: "#{client.name}.pdf",
type: "application/pdf")
end
end
```
`send_file` 一次只發送 4kB,而不是一次把整個文件都寫入內存。如果不想使用數據流方式,可以把 `:stream` 選項設為 `false`。如果想調整數據塊大小,可以設置 `:buffer_size` 選項。
如果沒有指定 `:type` 選項,Rails 會根據 `:filename` 中的文件擴展名猜測。如果沒有注冊擴展名對應的文件類型,則使用 `application/octet-stream`。
要謹慎處理用戶提交數據(參數,cookies 等)中的文件路徑,有安全隱患,你可能并不想讓別人下載這個文件。
不建議通過 Rails 以數據流的方式發送靜態文件,你可以把靜態文件放在服務器的公共文件夾中,使用 Apache 或其他服務器下載效率更高,因為不用經由整個 Rails 處理。
#### 12.2 使用 REST 的方式下載文件
雖然可以使用 `send_data` 方法發送數據,但是在 REST 架構的程序中,單獨為下載文件操作寫個動作有些多余。在 REST 架構下,上例中的 PDF 文件可以視作一種客戶端資源。Rails 提供了一種更符合 REST 架構的文件下載方法。下面這段代碼重寫了前面的例子,把下載 PDF 文件的操作放在 `show` 動作中,不使用數據流:
```
class ClientsController < ApplicationController
# The user can request to receive this resource as HTML or PDF.
def show
@client = Client.find(params[:id])
respond_to do |format|
format.html
format.pdf { render pdf: generate_pdf(@client) }
end
end
end
```
為了讓這段代碼能順利運行,要把 PDF MIME 加入 Rails。在 `config/initializers/mime_types.rb` 文件中加入下面這行代碼即可:
```
Mime::Type.register "application/pdf", :pdf
```
設置文件不會在每次請求中都重新加載,所以為了讓改動生效,需要重啟服務器。
現在客戶端請求 PDF 版本,只要在 URL 后加上 `".pdf"` 即可:
```
GET /clients/1.pdf
```
#### 12.3 任意數據的實時流
在 Rails 中,不僅文件可以使用數據流的方式處理,在響應對象中,任何數據都可以視作數據流。`ActionController::Live` 模塊可以和瀏覽器建立持久連接,隨時隨地把數據傳送給瀏覽器。
##### 12.3.1 使用實時流
把 `ActionController::Live` 模塊引入控制器中后,所有的動作都可以處理數據流。
```
class MyController < ActionController::Base
include ActionController::Live
def stream
response.headers['Content-Type'] = 'text/event-stream'
100.times {
response.stream.write "hello world\n"
sleep 1
}
ensure
response.stream.close
end
end
```
上面的代碼會和瀏覽器建立持久連接,每秒一次,共發送 100 次 `"hello world\n"`。
關于這段代碼有一些注意事項。必須關閉響應數據流。如果忘記關閉,套接字就會一直處于打開狀態。發送數據流之前,還要把內容類型設為 `text/event-stream`。因為響應發送后(`response.committed` 返回真值后)就無法設置報頭了。
##### 12.3.2 使用舉例
架設你在制作一個卡拉 OK 機,用戶想查看某首歌的歌詞。每首歌(`Song`)都有很多行歌詞,每一行歌詞都要花一些時間(`num_beats`)才能唱完。
如果按照卡拉 OK 機的工作方式,等上一句唱完才顯示下一行,就要使用 `ActionController::Live`:
```
class LyricsController < ActionController::Base
include ActionController::Live
def show
response.headers['Content-Type'] = 'text/event-stream'
song = Song.find(params[:id])
song.each do |line|
response.stream.write line.lyrics
sleep line.num_beats
end
ensure
response.stream.close
end
end
```
在這段代碼中,只有上一句唱完才會發送下一句歌詞。
##### 12.3.3 使用數據流時的注意事項
以數據流的方式發送任意數據是個強大的功能,如前面幾個例子所示,你可以選擇何時發送什么數據。不過,在使用時,要注意以下事項:
* 每次以數據流形式發送響應時都會新建一個線程,然后把原線程中的本地變量復制過來。線程中包含太多的本地變量會降低性能。而且,線程太多也會影響性能。
* 忘記關閉響應流會導致套接字一直處于打開狀態。使用響應流時一定要記得調用 `close` 方法。
* WEBrick 會緩沖所有響應,因此引入 `ActionController::Live` 也不會有任何效果。你應該使用不自動緩沖響應的服務器。
### 13 過濾日志
Rails 在 `log` 文件夾中為每個環境都準備了一個日志文件。這些文件在調試時特別有用,但上線后的程序并不用把所有信息都寫入日志。
#### 13.1 過濾參數
要想過濾特定的請求參數,禁止寫入日志文件,可以在程序的設置文件中設置 `config.filter_parameters` 選項。過濾掉得參數在日志中會顯示為 `[FILTERED]`。
```
config.filter_parameters << :password
```
#### 13.2 過濾轉向
有時需要從日志文件中過濾掉一些程序轉向的敏感數據,此時可以設置 `config.filter_redirect` 選項:
```
config.filter_redirect << 's3.amazonaws.com'
```
可以使用字符串,正則表達式,或者一個數組,包含字符串或正則表達式:
```
config.filter_redirect.concat ['s3.amazonaws.com', /private_path/]
```
匹配的 URL 會顯示為 `'[FILTERED]'`。
### 14 異常處理
程序很有可能有錯誤,錯誤發生時會拋出異常,這些異常是需要處理的。例如,如果用戶訪問一個連接,但數據庫中已經沒有對應的資源了,此時 Active Record 會拋出 `ActiveRecord::RecordNotFound` 異常。
在 Rails 中,異常的默認處理方式是顯示“500 Internal Server Error”消息。如果程序在本地運行,出錯后會顯示一個精美的調用堆棧,以及其他附加信息,讓開發者快速找到錯誤的地方,然后修正。如果程序已經上線,Rails 則會簡單的顯示“500 Server Error”消息,如果是路由錯誤或記錄不存在,則顯示“404 Not Found”。有時你可能想換種方式捕獲錯誤,以及如何顯示報錯信息。在 Rails 中,有很多層異常處理,詳解如下。
#### 14.1 默認的 500 和 404 模板
默認情況下,如果程序錯誤,會顯示 404 或者 500 錯誤消息。錯誤消息在 `public` 文件夾中的靜態 HTML 文件中,分別是 `404.html` 和 `500.html`。你可以修改這兩個文件,添加其他信息或布局,不過要記住,這兩個是靜態文件,不能使用 RHTML,只能寫入純粹的 HTML。
#### 14.2 `rescue_from`
捕獲錯誤后如果想做更詳盡的處理,可以使用 `rescue_form`。`rescue_from` 可以處理整個控制器及其子類中的某種(或多種)異常。
異常發生時,會被 `rescue_from` 捕獲,異常對象會傳入處理代碼。處理異常的代碼可以是方法,也可以是 `Proc` 對象,由 `:with` 選項指定。也可以不用 `Proc` 對象,直接使用塊。
下面的代碼使用 `rescue_from` 截獲所有 `ActiveRecord::RecordNotFound` 異常,然后做相應的處理。
```
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
private
def record_not_found
render plain: "404 Not Found", status: 404
end
end
```
這段代碼對異常的處理并不詳盡,比默認的處理也沒好多少。不過只要你能捕獲異常,就可以做任何想做的處理。例如,可以新建一個異常類,用戶無權查看頁面時拋出:
```
class ApplicationController < ActionController::Base
rescue_from User::NotAuthorized, with: :user_not_authorized
private
def user_not_authorized
flash[:error] = "You don't have access to this section."
redirect_to :back
end
end
class ClientsController < ApplicationController
# Check that the user has the right authorization to access clients.
before_action :check_authorization
# Note how the actions don't have to worry about all the auth stuff.
def edit
@client = Client.find(params[:id])
end
private
# If the user is not authorized, just throw the exception.
def check_authorization
raise User::NotAuthorized unless current_user.admin?
end
end
```
某些異常只能在 `ApplicationController` 中捕獲,因為在異常拋出前控制器還沒初始化,動作也沒執行。詳情參見 [Pratik Naik 的文章](http://m.onkey.org/2008/7/20/rescue-from-dispatching)。
### 15 強制使用 HTTPS 協議
有時,基于安全考慮,可能希望某個控制器只能通過 HTTPS 協議訪問。為了達到這個目的,可以在控制器中使用 `force_ssl` 方法:
```
class DinnerController
force_ssl
end
```
和過濾器類似,也可指定 `:only` 或 `:except` 選項,設置只在某些動作上強制使用 HTTPS:
```
class DinnerController
force_ssl only: :cheeseburger
# or
force_ssl except: :cheeseburger
end
```
注意,如果你在很多控制器中都使用了 `force_ssl`,或許你想讓整個程序都使用 HTTPS。此時,你可以在環境設置文件中設置 `config.force_ssl` 選項。
### 反饋
歡迎幫忙改善指南質量。
如發現任何錯誤,歡迎修正。開始貢獻前,可先行閱讀[貢獻指南:文檔](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