# Rails 路由全解
本文介紹面向用戶的 Rails 路由功能。
讀完本文,你將學到:
* 如何理解 `routes.rb` 文件中的代碼;
* 如何使用推薦的資源式,或使用 `match` 方法編寫路由;
* 動作能接收到什么參數;
* 如何使用路由幫助方法自動創建路徑和 URL;
* 約束和 Rack 端點等高級技術;
### Chapters
1. [Rails 路由的作用](#rails-%E8%B7%AF%E7%94%B1%E7%9A%84%E4%BD%9C%E7%94%A8)
* [把 URL 和代碼連接起來](#%E6%8A%8A-url-%E5%92%8C%E4%BB%A3%E7%A0%81%E8%BF%9E%E6%8E%A5%E8%B5%B7%E6%9D%A5)
* [生成路徑和 URL](#%E7%94%9F%E6%88%90%E8%B7%AF%E5%BE%84%E5%92%8C-url)
2. [資源路徑:Rails 的默認值](#%E8%B5%84%E6%BA%90%E8%B7%AF%E5%BE%84%EF%BC%9Arails-%E7%9A%84%E9%BB%98%E8%AE%A4%E5%80%BC)
* [網絡中的資源](#%E7%BD%91%E7%BB%9C%E4%B8%AD%E7%9A%84%E8%B5%84%E6%BA%90)
* [CRUD,HTTP 方法和動作](#crud%EF%BC%8Chttp-%E6%96%B9%E6%B3%95%E5%92%8C%E5%8A%A8%E4%BD%9C)
* [路徑和 URL 幫助方法](#%E8%B7%AF%E5%BE%84%E5%92%8C-url-%E5%B8%AE%E5%8A%A9%E6%96%B9%E6%B3%95)
* [一次聲明多個資源路由](#%E4%B8%80%E6%AC%A1%E5%A3%B0%E6%98%8E%E5%A4%9A%E4%B8%AA%E8%B5%84%E6%BA%90%E8%B7%AF%E7%94%B1)
* [單數資源](#%E5%8D%95%E6%95%B0%E8%B5%84%E6%BA%90)
* [控制器命名空間和路由](#%E6%8E%A7%E5%88%B6%E5%99%A8%E5%91%BD%E5%90%8D%E7%A9%BA%E9%97%B4%E5%92%8C%E8%B7%AF%E7%94%B1)
* [嵌套資源](#%E5%B5%8C%E5%A5%97%E8%B5%84%E6%BA%90)
* [Routing Concerns](#routing-concerns)
* [由對象創建路徑和 URL](#%E7%94%B1%E5%AF%B9%E8%B1%A1%E5%88%9B%E5%BB%BA%E8%B7%AF%E5%BE%84%E5%92%8C-url)
* [添加更多的 REST 架構動作](#%E6%B7%BB%E5%8A%A0%E6%9B%B4%E5%A4%9A%E7%9A%84-rest-%E6%9E%B6%E6%9E%84%E5%8A%A8%E4%BD%9C)
3. [非資源式路由](#%E9%9D%9E%E8%B5%84%E6%BA%90%E5%BC%8F%E8%B7%AF%E7%94%B1)
* [綁定參數](#%E7%BB%91%E5%AE%9A%E5%8F%82%E6%95%B0)
* [動態路徑片段](#%E5%8A%A8%E6%80%81%E8%B7%AF%E5%BE%84%E7%89%87%E6%AE%B5)
* [靜態路徑片段](#%E9%9D%99%E6%80%81%E8%B7%AF%E5%BE%84%E7%89%87%E6%AE%B5)
* [查詢字符串](#%E6%9F%A5%E8%AF%A2%E5%AD%97%E7%AC%A6%E4%B8%B2)
* [定義默認值](#%E5%AE%9A%E4%B9%89%E9%BB%98%E8%AE%A4%E5%80%BC)
* [命名路由](#%E5%91%BD%E5%90%8D%E8%B7%AF%E7%94%B1)
* [HTTP 方法約束](#http-%E6%96%B9%E6%B3%95%E7%BA%A6%E6%9D%9F)
* [路徑片段約束](#%E8%B7%AF%E5%BE%84%E7%89%87%E6%AE%B5%E7%BA%A6%E6%9D%9F)
* [基于請求的約束](#%E5%9F%BA%E4%BA%8E%E8%AF%B7%E6%B1%82%E7%9A%84%E7%BA%A6%E6%9D%9F)
* [高級約束](#%E9%AB%98%E7%BA%A7%E7%BA%A6%E6%9D%9F)
* [通配片段](#%E9%80%9A%E9%85%8D%E7%89%87%E6%AE%B5)
* [重定向](#%E9%87%8D%E5%AE%9A%E5%90%91)
* [映射到 Rack 程序](#%E6%98%A0%E5%B0%84%E5%88%B0-rack-%E7%A8%8B%E5%BA%8F)
* [使用 `root`](#%E4%BD%BF%E7%94%A8-root)
* [Unicode 字符路由](#unicode-%E5%AD%97%E7%AC%A6%E8%B7%AF%E7%94%B1)
4. [定制資源式路由](#%E5%AE%9A%E5%88%B6%E8%B5%84%E6%BA%90%E5%BC%8F%E8%B7%AF%E7%94%B1)
* [指定使用的控制器](#%E6%8C%87%E5%AE%9A%E4%BD%BF%E7%94%A8%E7%9A%84%E6%8E%A7%E5%88%B6%E5%99%A8)
* [指定約束](#%E6%8C%87%E5%AE%9A%E7%BA%A6%E6%9D%9F)
* [改寫具名幫助方法](#%E6%94%B9%E5%86%99%E5%85%B7%E5%90%8D%E5%B8%AE%E5%8A%A9%E6%96%B9%E6%B3%95)
* [改寫 `new` 和 `edit` 片段](#%E6%94%B9%E5%86%99-new-%E5%92%8C-edit-%E7%89%87%E6%AE%B5)
* [為具名路由幫助方法加上前綴](#%E4%B8%BA%E5%85%B7%E5%90%8D%E8%B7%AF%E7%94%B1%E5%B8%AE%E5%8A%A9%E6%96%B9%E6%B3%95%E5%8A%A0%E4%B8%8A%E5%89%8D%E7%BC%80)
* [限制生成的路由](#%E9%99%90%E5%88%B6%E7%94%9F%E6%88%90%E7%9A%84%E8%B7%AF%E7%94%B1)
* [翻譯路徑](#%E7%BF%BB%E8%AF%91%E8%B7%AF%E5%BE%84)
* [改寫單數形式](#%E6%94%B9%E5%86%99%E5%8D%95%E6%95%B0%E5%BD%A2%E5%BC%8F)
* [在嵌套資源中使用 `:as` 選項](#%E5%9C%A8%E5%B5%8C%E5%A5%97%E8%B5%84%E6%BA%90%E4%B8%AD%E4%BD%BF%E7%94%A8-:as-%E9%80%89%E9%A1%B9)
5. [路由審查和測試](#%E8%B7%AF%E7%94%B1%E5%AE%A1%E6%9F%A5%E5%92%8C%E6%B5%8B%E8%AF%95)
* [列出現有路由](#%E5%88%97%E5%87%BA%E7%8E%B0%E6%9C%89%E8%B7%AF%E7%94%B1)
* [測試路由](#%E6%B5%8B%E8%AF%95%E8%B7%AF%E7%94%B1)
### 1 Rails 路由的作用
Rails 路由能識別 URL,將其分發給控制器的動作進行處理,還能生成路徑和 URL,無需直接在視圖中硬編碼字符串。
#### 1.1 把 URL 和代碼連接起來
Rails 程序收到如下請求時
```
GET /patients/17
```
會查詢路由,找到匹配的控制器動作。如果首個匹配的路由是:
```
get '/patients/:id', to: 'patients#show'
```
那么這個請求就交給 `patients` 控制器的 `show` 動作處理,并把 `{ id: '17' }` 傳入 `params`。
#### 1.2 生成路徑和 URL
通過路由還可生成路徑和 URL。如果把前面的路由修改成:
```
get '/patients/:id', to: 'patients#show', as: 'patient'
```
在控制器中有如下代碼:
```
@patient = Patient.find(17)
```
在相應的視圖中有如下代碼:
```
<%= link_to 'Patient Record', patient_path(@patient) %>
```
那么路由就會生成路徑 `/patients/17`。這么做代碼易于維護、理解。注意,在路由幫助方法中無需指定 ID。
### 2 資源路徑:Rails 的默認值
使用資源路徑可以快速聲明資源式控制器所有的常規路由,無需分別為 `index`、`show`、`new`、`edit`、`create`、`update` 和 `destroy` 動作分別聲明路由,只需一行代碼就能搞定。
#### 2.1 網絡中的資源
瀏覽器向 Rails 程序請求頁面時會使用特定的 HTTP 方法,例如 `GET`、`POST`、`PATCH`、`PUT` 和 `DELETE`。每個方法對應對資源的一種操作。資源路由會把一系列相關請求映射到單個路由器的不同動作上。
如果 Rails 程序收到如下請求:
```
DELETE /photos/17
```
會查詢路由將其映射到一個控制器的路由上。如果首個匹配的路由是:
```
resources :photos
```
那么這個請求就交給 `photos` 控制器的 `destroy` 方法處理,并把 `{ id: '17' }` 傳入 `params`。
#### 2.2 CRUD,HTTP 方法和動作
在 Rails 中,資源式路由把 HTTP 方法和 URL 映射到控制器的動作上。而且根據約定,還映射到數據庫的 CRUD 操作上。路由文件中如下的單行聲明:
```
resources :photos
```
會創建七個不同的路由,全部映射到 `Photos` 控制器上:
| HTTP 方法 | 路徑 | 控制器#動作 | 作用 |
| --- | --- | --- | --- |
| GET | /photos | photos#index | 顯示所有圖片 |
| GET | /photos/new | photos#new | 顯示新建圖片的表單 |
| POST | /photos | photos#create | 新建圖片 |
| GET | /photos/:id | photos#show | 顯示指定的圖片 |
| GET | /photos/:id/edit | photos#edit | 顯示編輯圖片的表單 |
| PATCH/PUT | /photos/:id | photos#update | 更新指定的圖片 |
| DELETE | /photos/:id | photos#destroy | 刪除指定的圖片 |
路由使用 HTTP 方法和 URL 匹配請求,把四個 URL 映射到七個不同的動作上。 I> NOTE: 路由按照聲明的順序匹配哦,如果在 `get 'photos/poll'` 之前聲明了 `resources :photos`,那么 `show` 動作的路由由 `resources` 這行解析。如果想使用 `get` 這行,就要將其移到 `resources` 之前。
#### 2.3 路徑和 URL 幫助方法
聲明資源式路由后,會自動創建一些幫助方法。以 `resources :photos` 為例:
* `photos_path` 返回 `/photos`
* `new_photo_path` 返回 `/photos/new`
* `edit_photo_path(:id)` 返回 `/photos/:id/edit`,例如 `edit_photo_path(10)` 返回 `/photos/10/edit`
* `photo_path(:id)` 返回 `/photos/:id`,例如 `photo_path(10)` 返回 `/photos/10`
這些幫助方法都有對應的 `_url` 形式,例如 `photos_url`,返回主機、端口加路徑。
#### 2.4 一次聲明多個資源路由
如果需要為多個資源聲明路由,可以節省一點時間,調用一次 `resources` 方法完成:
```
resources :photos, :books, :videos
```
這種方式等價于:
```
resources :photos
resources :books
resources :videos
```
#### 2.5 單數資源
有時希望不用 ID 就能查看資源,例如,`/profile` 一直顯示當前登入用戶的個人信息。針對這種需求,可以使用單數資源,把 `/profile`(不是 `/profile/:id`)映射到 `show` 動作:
```
get 'profile', to: 'users#show'
```
如果 `get` 方法的 `to` 選項是字符串,要使用 `controller#action` 形式;如果是 Symbol,就可以直接指定動作:
```
get 'profile', to: :show
```
下面這個資源式路由:
```
resource :geocoder
```
會生成六個路由,全部映射到 `Geocoders` 控制器:
| HTTP 方法 | 路徑 | 控制器#動作 | 作用 |
| --- | --- | --- | --- |
| GET | /geocoder/new | geocoders#new | 顯示新建 geocoder 的表單 |
| POST | /geocoder | geocoders#create | 新建 geocoder |
| GET | /geocoder | geocoders#show | 顯示唯一的 geocoder 資源 |
| GET | /geocoder/edit | geocoders#edit | 顯示編輯 geocoder 的表單 |
| PATCH/PUT | /geocoder | geocoders#update | 更新唯一的 geocoder 資源 |
| DELETE | /geocoder | geocoders#destroy | 刪除 geocoder 資源 |
有時需要使用同個控制器處理單數路由(例如 `/account`)和復數路由(例如 `/accounts/45`),把單數資源映射到復數控制器上。例如,`resource :photo` 和 `resources :photos` 分別聲明單數和復數路由,映射到同個控制器(`PhotosController`)上。
單數資源式路由生成以下幫助方法:
* `new_geocoder_path` 返回 `/geocoder/new`
* `edit_geocoder_path` 返回 `/geocoder/edit`
* `geocoder_path` 返回 `/geocoder`
和復數資源一樣,上面各幫助方法都有對應的 `_url` 形式,返回主機、端口加路徑。
有個一直存在的問題導致 `form_for` 無法自動處理單數資源。為了解決這個問題,可以直接指定表單的 URL,例如:
```
form_for @geocoder, url: geocoder_path do |f|
```
#### 2.6 控制器命名空間和路由
你可能想把一系列控制器放在一個命名空間內,最常見的是把管理相關的控制器放在 `Admin::` 命名空間內。你需要把這些控制器存在 `app/controllers/admin` 文件夾中,然后在路由中做如下聲明:
```
namespace :admin do
resources :articles, :comments
end
```
上述代碼會為 `articles` 和 `comments` 控制器生成很多路由。對 `Admin::ArticlesController` 來說,Rails 會生成:
| HTTP 方法 | 路徑 | 控制器#動作 | 具名幫助方法 |
| --- | --- | --- | --- |
| GET | /admin/articles | admin/articles#index | admin_articles_path |
| GET | /admin/articles/new | admin/articles#new | new_admin_article_path |
| POST | /admin/articles | admin/articles#create | admin_articles_path |
| GET | /admin/articles/:id | admin/articles#show | admin_article_path(:id) |
| GET | /admin/articles/:id/edit | admin/articles#edit | edit_admin_article_path(:id) |
| PATCH/PUT | /admin/articles/:id | admin/articles#update | admin_article_path(:id) |
| DELETE | /admin/articles/:id | admin/articles#destroy | admin_article_path(:id) |
如果想把 `/articles`(前面沒有 `/admin`)映射到 `Admin::ArticlesController` 控制器上,可以這么聲明:
```
scope module: 'admin' do
resources :articles, :comments
end
```
如果只有一個資源,還可以這么聲明:
```
resources :articles, module: 'admin'
```
如果想把 `/admin/articles` 映射到 `ArticlesController` 控制器(不在 `Admin::` 命名空間內),可以這么聲明:
```
scope '/admin' do
resources :articles, :comments
end
```
如果只有一個資源,還可以這么聲明:
```
resources :articles, path: '/admin/articles'
```
在上述兩種用法中,具名路由沒有變化,跟不用 `scope` 時一樣。在后一種用法中,映射到 `ArticlesController` 控制器上的路徑如下:
| HTTP 方法 | 路徑 | 控制器#動作 | 具名幫助方法 |
| --- | --- | --- | --- |
| GET | /admin/articles | articles#index | articles_path |
| GET | /admin/articles/new | articles#new | new_article_path |
| POST | /admin/articles | articles#create | articles_path |
| GET | /admin/articles/:id | articles#show | article_path(:id) |
| GET | /admin/articles/:id/edit | articles#edit | edit_article_path(:id) |
| PATCH/PUT | /admin/articles/:id | articles#update | article_path(:id) |
| DELETE | /admin/articles/:id | articles#destroy | article_path(:id) |
如果在 `namespace` 代碼塊中想使用其他的控制器命名空間,可以指定控制器的絕對路徑,例如 `get '/foo' => '/foo#index'`。
#### 2.7 嵌套資源
開發程序時經常會遇到一個資源是其他資源的子資源這種情況。假設程序中有如下的模型:
```
class Magazine < ActiveRecord::Base
has_many :ads
end
class Ad < ActiveRecord::Base
belongs_to :magazine
end
```
在路由中可以使用“嵌套路由”反應這種關系。針對這個例子,可以聲明如下路由:
```
resources :magazines do
resources :ads
end
```
除了創建 `MagazinesController` 的路由之外,上述聲明還會創建 `AdsController` 的路由。廣告的 URL 要用到雜志資源:
| HTTP 方法 | 路徑 | 控制器#動作 | 作用 |
| --- | --- | --- | --- |
| GET | /magazines/:magazine_id/ads | ads#index | 顯示指定雜志的所有廣告 |
| GET | /magazines/:magazine_id/ads/new | ads#new | 顯示新建廣告的表單,該告屬于指定的雜志 |
| POST | /magazines/:magazine_id/ads | ads#create | 創建屬于指定雜志的廣告 |
| GET | /magazines/:magazine_id/ads/:id | ads#show | 顯示屬于指定雜志的指定廣告 |
| GET | /magazines/:magazine_id/ads/:id/edit | ads#edit | 顯示編輯廣告的表單,該廣告屬于指定的雜志 |
| PATCH/PUT | /magazines/:magazine_id/ads/:id | ads#update | 更新屬于指定雜志的指定廣告 |
| DELETE | /magazines/:magazine_id/ads/:id | ads#destroy | 刪除屬于指定雜志的指定廣告 |
上述路由還會生成 `magazine_ads_url` 和 `edit_magazine_ad_path` 等路由幫助方法。這些幫助方法的第一個參數是 `Magazine` 實例,例如 `magazine_ads_url(@magazine)`。
##### 2.7.1 嵌套限制
嵌套路由可以放在其他嵌套路由中,例如:
```
resources :publishers do
resources :magazines do
resources :photos
end
end
```
層級較多的嵌套路由很難處理。例如,程序可能要識別如下的路徑:
```
/publishers/1/magazines/2/photos/3
```
對應的路由幫助方法是 `publisher_magazine_photo_url`,要指定三個層級的對象。這種用法很讓人困擾,Jamis Buck 在[一篇文章](http://weblog.jamisbuck.org/2007/2/5/nesting-resources)中指出了嵌套路由的用法總則,即:
嵌套資源不可超過一層。
##### 2.7.2 淺層嵌套
避免深層嵌套的方法之一,是把控制器集合動作放在父級資源中,表明層級關系,但不嵌套成員動作。也就是說,用最少的信息表明資源的路由關系,如下所示:
```
resources :articles do
resources :comments, only: [:index, :new, :create]
end
resources :comments, only: [:show, :edit, :update, :destroy]
```
這種做法在描述路由和深層嵌套之間做了適當的平衡。上述代碼還有簡寫形式,即使用 `:shallow` 選項:
```
resources :articles do
resources :comments, shallow: true
end
```
這種形式生成的路由和前面一樣。`:shallow` 選項還可以在父級資源中使用,此時所有嵌套其中的資源都是淺層嵌套:
```
resources :articles, shallow: true do
resources :comments
resources :quotes
resources :drafts
end
```
`shallow` 方法可以創建一個作用域,其中所有嵌套都是淺層嵌套。如下代碼生成的路由和前面一樣:
```
shallow do
resources :articles do
resources :comments
resources :quotes
resources :drafts
end
end
```
`scope` 方法有兩個選項可以定制淺層嵌套路由。`:shallow_path` 選項在成員路徑前加上指定的前綴:
```
scope shallow_path: "sekret" do
resources :articles do
resources :comments, shallow: true
end
end
```
上述代碼為 `comments` 資源生成的路由如下:
| HTTP 方法 | 路徑 | 控制器#動作 | 具名幫助方法 |
| --- | --- | --- | --- |
| GET | /articles/:article_id/comments(.:format) | comments#index | article_comments_path |
| POST | /articles/:article_id/comments(.:format) | comments#create | article_comments_path |
| GET | /articles/:article_id/comments/new(.:format) | comments#new | new_article_comment_path |
| GET | /sekret/comments/:id/edit(.:format) | comments#edit | edit_comment_path |
| GET | /sekret/comments/:id(.:format) | comments#show | comment_path |
| PATCH/PUT | /sekret/comments/:id(.:format) | comments#update | comment_path |
| DELETE | /sekret/comments/:id(.:format) | comments#destroy | comment_path |
`:shallow_prefix` 選項在具名幫助方法前加上指定的前綴:
```
scope shallow_prefix: "sekret" do
resources :articles do
resources :comments, shallow: true
end
end
```
上述代碼為 `comments` 資源生成的路由如下:
| HTTP 方法 | 路徑 | 控制器#動作 | 具名幫助方法 |
| --- | --- | --- | --- |
| GET | /articles/:article_id/comments(.:format) | comments#index | article_comments_path |
| POST | /articles/:article_id/comments(.:format) | comments#create | article_comments_path |
| GET | /articles/:article_id/comments/new(.:format) | comments#new | new_article_comment_path |
| GET | /comments/:id/edit(.:format) | comments#edit | edit_sekret_comment_path |
| GET | /comments/:id(.:format) | comments#show | sekret_comment_path |
| PATCH/PUT | /comments/:id(.:format) | comments#update | sekret_comment_path |
| DELETE | /comments/:id(.:format) | comments#destroy | sekret_comment_path |
#### 2.8 Routing Concerns
Routing Concerns 用來聲明通用路由,可在其他資源和路由中重復使用。定義 concern 的方式如下:
```
concern :commentable do
resources :comments
end
concern :image_attachable do
resources :images, only: :index
end
```
Concerns 可在資源中重復使用,避免代碼重復:
```
resources :messages, concerns: :commentable
resources :articles, concerns: [:commentable, :image_attachable]
```
上述聲明等價于:
```
resources :messages do
resources :comments
end
resources :articles do
resources :comments
resources :images, only: :index
end
```
Concerns 在路由的任何地方都能使用,例如,在作用域或命名空間中:
```
namespace :articles do
concerns :commentable
end
```
#### 2.9 由對象創建路徑和 URL
除了使用路由幫助方法之外,Rails 還能從參數數組中創建路徑和 URL。例如,假設有如下路由:
```
resources :magazines do
resources :ads
end
```
使用 `magazine_ad_path` 時,可以不傳入數字 ID,傳入 `Magazine` 和 `Ad` 實例即可:
```
<%= link_to 'Ad details', magazine_ad_path(@magazine, @ad) %>
```
而且還可使用 `url_for` 方法,指定一組對象,Rails 會自動決定使用哪個路由:
```
<%= link_to 'Ad details', url_for([@magazine, @ad]) %>
```
此時,Rails 知道 `@magazine` 是 `Magazine` 的實例,`@ad` 是 `Ad` 的實例,所以會調用 `magazine_ad_path` 幫助方法。使用 `link_to` 等方法時,無需使用完整的 `url_for` 方法,直接指定對象即可:
```
<%= link_to 'Ad details', [@magazine, @ad] %>
```
如果想鏈接到一本雜志,可以這么做:
```
<%= link_to 'Magazine details', @magazine %>
```
要想鏈接到其他動作,把數組的第一個元素設為所需動作名即可:
```
<%= link_to 'Edit Ad', [:edit, @magazine, @ad] %>
```
在這種用法中,會把模型實例轉換成對應的 URL,這是資源式路由帶來的主要好處之一。
#### 2.10 添加更多的 REST 架構動作
可用的路由并不局限于 REST 路由默認創建的那七個,還可以添加額外的集合路由或成員路由。
##### 2.10.1 添加成員路由
要添加成員路由,在 `resource` 代碼塊中使用 `member` 塊即可:
```
resources :photos do
member do
get 'preview'
end
end
```
這段路由能識別 `/photos/1/preview` 是個 GET 請求,映射到 `PhotosController` 的 `preview` 動作上,資源的 ID 傳入 `params[:id]`。同時還生成了 `preview_photo_url` 和 `preview_photo_path` 兩個幫助方法。
在 `member` 代碼塊中,每個路由都要指定使用的 HTTP 方法。可以使用 `get`,`patch`,`put`,`post` 或 `delete`。如果成員路由不多,可以不使用代碼塊形式,直接在路由上使用 `:on` 選項:
```
resources :photos do
get 'preview', on: :member
end
```
也可以不使用 `:on` 選項,得到的成員路由是相同的,但資源 ID 存儲在 `params[:photo_id]` 而不是 `params[:id]` 中。
##### 2.10.2 添加集合路由
添加集合路由的方式如下:
```
resources :photos do
collection do
get 'search'
end
end
```
這段路由能識別 `/photos/search` 是個 GET 請求,映射到 `PhotosController` 的 `search` 動作上。同時還會生成 `search_photos_url` 和 `search_photos_path` 兩個幫助方法。
和成員路由一樣,也可使用 `:on` 選項:
```
resources :photos do
get 'search', on: :collection
end
```
##### 2.10.3 添加額外新建動作的路由
要添加額外的新建動作,可以使用 `:on` 選項:
```
resources :comments do
get 'preview', on: :new
end
```
這段代碼能識別 `/comments/new/preview` 是個 GET 請求,映射到 `CommentsController` 的 `preview` 動作上。同時還會生成 `preview_new_comment_url` 和 `preview_new_comment_path` 兩個路由幫助方法。
如果在資源式路由中添加了過多額外動作,這時就要停下來問自己,是不是要新建一個資源。
### 3 非資源式路由
除了資源路由之外,Rails 還提供了強大功能,把任意 URL 映射到動作上。此時,不會得到資源式路由自動生成的一系列路由,而是分別聲明各個路由。
雖然一般情況下要使用資源式路由,但也有一些情況使用簡單的路由更合適。如果不合適,也不用非得使用資源實現程序的每種功能。
簡單的路由特別適合把傳統的 URL 映射到 Rails 動作上。
#### 3.1 綁定參數
聲明常規路由時,可以提供一系列 Symbol,做為 HTTP 請求的一部分,傳入 Rails 程序。其中兩個 Symbol 有特殊作用:`:controller` 映射程序的控制器名,`:action` 映射控制器中的動作名。例如,有下面的路由:
```
get ':controller(/:action(/:id))'
```
如果 `/photos/show/1` 由這個路由處理(沒匹配路由文件中其他路由聲明),會映射到 `PhotosController` 的 `show` 動作上,最后一個參數 `"1"` 可通過 `params[:id]` 獲取。上述路由還能處理 `/photos` 請求,映射到 `PhotosController#index`,因為 `:action` 和 `:id` 放在括號中,是可選參數。
#### 3.2 動態路徑片段
在常規路由中可以使用任意數量的動態片段。`:controller` 和 `:action` 之外的參數都會存入 `params` 傳給動作。如果有下面的路由:
```
get ':controller/:action/:id/:user_id'
```
`/photos/show/1/2` 請求會映射到 `PhotosController` 的 `show` 動作。`params[:id]` 的值是 `"1"`,`params[:user_id]` 的值是 `"2"`。
匹配控制器時不能使用 `:namespace` 或 `:module`。如果需要這種功能,可以為控制器做個約束,匹配所需的命名空間。例如: I> I> I>`ruby NOTE: get ':controller(/:action(/:id))', controller: /admin\/[^\/]+/ NOTE:`
默認情況下,動態路徑片段中不能使用點號,因為點號是格式化路由的分隔符。如果需要在動態路徑片段中使用點號,可以添加一個約束條件。例如,`id: /[^\/]+/` 可以接受除斜線之外的所有字符。
#### 3.3 靜態路徑片段
聲明路由時可以指定靜態路徑片段,片段前不加冒號即可:
```
get ':controller/:action/:id/with_user/:user_id'
```
這個路由能響應 `/photos/show/1/with_user/2` 這種路徑。此時,`params` 的值為 `{ controller: 'photos', action: 'show', id: '1', user_id: '2' }`。
#### 3.4 查詢字符串
`params` 中還包含查詢字符串中的所有參數。例如,有下面的路由:
```
get ':controller/:action/:id'
```
`/photos/show/1?user_id=2` 請求會映射到 `Photos` 控制器的 `show` 動作上。`params` 的值為 `{ controller: 'photos', action: 'show', id: '1', user_id: '2' }`。
#### 3.5 定義默認值
在路由中無需特別使用 `:controller` 和 `:action`,可以指定默認值:
```
get 'photos/:id', to: 'photos#show'
```
這樣聲明路由后,Rails 會把 `/photos/12` 映射到 `PhotosController` 的 `show` 動作上。
路由中的其他部分也使用 `:defaults` 選項設置默認值。甚至可以為沒有指定的動態路徑片段設定默認值。例如:
```
get 'photos/:id', to: 'photos#show', defaults: { format: 'jpg' }
```
Rails 會把 `photos/12` 請求映射到 `PhotosController` 的 `show` 動作上,把 `params[:format]` 的值設為 `"jpg"`。
#### 3.6 命名路由
使用 `:as` 選項可以為路由起個名字:
```
get 'exit', to: 'sessions#destroy', as: :logout
```
這段路由會生成 `logout_path` 和 `logout_url` 這兩個具名路由幫助方法。調用 `logout_path` 方法會返回 `/exit`。
使用 `:as` 選項還能重設資源的路徑方法,例如:
```
get ':username', to: 'users#show', as: :user
```
這段路由會定義一個名為 `user_path` 的方法,可在控制器、幫助方法和視圖中使用。在 `UsersController` 的 `show` 動作中,`params[:username]` 的值即用戶的用戶名。如果不想使用 `:username` 作為參數名,可在路由聲明中修改。
#### 3.7 HTTP 方法約束
一般情況下,應該使用 `get`、`post`、`put`、`patch` 和 `delete` 方法限制路由可使用的 HTTP 方法。如果使用 `match` 方法,可以通過 `:via` 選項一次指定多個 HTTP 方法:
```
match 'photos', to: 'photos#show', via: [:get, :post]
```
如果某個路由想使用所有 HTTP 方法,可以使用 `via: :all`:
```
match 'photos', to: 'photos#show', via: :all
```
同個路由即處理 `GET` 請求又處理 `POST` 請求有安全隱患。一般情況下,除非有特殊原因,切記不要允許在一個動作上使用所有 HTTP 方法。
#### 3.8 路徑片段約束
可使用 `:constraints` 選項限制動態路徑片段的格式:
```
get 'photos/:id', to: 'photos#show', constraints: { id: /[A-Z]\d{5}/ }
```
這個路由能匹配 `/photos/A12345`,但不能匹配 `/photos/893`。上述路由還可簡化成:
```
get 'photos/:id', to: 'photos#show', id: /[A-Z]\d{5}/
```
`:constraints` 選項中的正則表達式不能使用“錨記”。例如,下面的路由是錯誤的:
```
get '/:id', to: 'photos#show', constraints: {id: /^\d/}
```
之所以不能使用錨記,是因為所有正則表達式都從頭開始匹配。
例如,有下面的路由。如果 `to_param` 方法得到的值以數字開頭,例如 `1-hello-world`,就會把請求交給 `articles` 控制器處理;如果 `to_param` 方法得到的值不以數字開頭,例如 `david`,就交給 `users` 控制器處理。
```
get '/:id', to: 'articles#show', constraints: { id: /\d.+/ }
get '/:username', to: 'users#show'
```
#### 3.9 基于請求的約束
約束還可以根據任何一個返回值為字符串的 [Request](action_controller_overview.html#the-request-object) 方法設定。
基于請求的約束和路徑片段約束的設定方式一樣:
```
get 'photos', constraints: {subdomain: 'admin'}
```
約束還可使用代碼塊形式:
```
namespace :admin do
constraints subdomain: 'admin' do
resources :photos
end
end
```
#### 3.10 高級約束
如果約束很復雜,可以指定一個能響應 `matches?` 方法的對象。假設要用 `BlacklistConstraint` 過濾所有用戶,可以這么做:
```
class BlacklistConstraint
def initialize
@ips = Blacklist.retrieve_ips
end
def matches?(request)
@ips.include?(request.remote_ip)
end
end
TwitterClone::Application.routes.draw do
get '*path', to: 'blacklist#index',
constraints: BlacklistConstraint.new
end
```
約束還可以在 lambda 中指定:
```
TwitterClone::Application.routes.draw do
get '*path', to: 'blacklist#index',
constraints: lambda { |request| Blacklist.retrieve_ips.include?(request.remote_ip) }
end
```
`matches?` 方法和 lambda 的參數都是 `request` 對象。
#### 3.11 通配片段
路由中的通配符可以匹配其后的所有路徑片段。例如:
```
get 'photos/*other', to: 'photos#unknown'
```
這個路由可以匹配 `photos/12` 或 `/photos/long/path/to/12`,`params[:other]` 的值為 `"12"` 或 `"long/path/to/12"`。以星號開頭的路徑片段叫做“通配片段”。
通配片段可以出現在路由的任何位置。例如:
```
get 'books/*section/:title', to: 'books#show'
```
這個路由可以匹配 `books/some/section/last-words-a-memoir`,`params[:section]` 的值為 `'some/section'`,`params[:title]` 的值為 `'last-words-a-memoir'`。
嚴格來說,路由中可以有多個通配片段。匹配器會根據直覺賦值各片段。例如:
```
get '*a/foo/*b', to: 'test#index'
```
這個路由可以匹配 `zoo/woo/foo/bar/baz`,`params[:a]` 的值為 `'zoo/woo'`,`params[:b]` 的值為 `'bar/baz'`。
如果請求 `'/foo/bar.json'`,那么 `params[:pages]` 的值為 `'foo/bar'`,請求類型為 JSON。如果想使用 Rails 3.0.x 中的表現,可以指定 `format: false` 選項,如下所示: I> I> I>`ruby NOTE: get '*pages', to: 'pages#show', format: false NOTE:` I> NOTE: 如果必須指定格式,可以指定 `format: true` 選項,如下所示: I> I> I>`ruby NOTE: get '*pages', to: 'pages#show', format: true NOTE:`
#### 3.12 重定向
在路由中可以使用 `redirect` 幫助方法把一個路徑重定向到另一個路徑:
```
get '/stories', to: redirect('/articles')
```
重定向時還可使用匹配的動態路徑片段:
```
get '/stories/:name', to: redirect('/articles/%{name}')
```
`redirect` 還可使用代碼塊形式,傳入路徑參數和 `request` 對象作為參數:
```
get '/stories/:name', to: redirect {|path_params, req| "/articles/#{path_params[:name].pluralize}" }
get '/stories', to: redirect {|path_params, req| "/articles/#{req.subdomain}" }
```
注意,`redirect` 實現的是 301 "Moved Permanently" 重定向,有些瀏覽器或代理服務器會緩存這種重定向,導致舊的頁面不可用。
如果不指定主機(`http://www.example.com`),Rails 會從當前請求中獲取。
#### 3.13 映射到 Rack 程序
除了使用字符串,例如 `'articles#index'`,把請求映射到 `ArticlesController` 的 `index` 動作上之外,還可使用 [Rack](rails_on_rack.html) 程序作為端點:
```
match '/application.js', to: Sprockets, via: :all
```
只要 `Sprockets` 能響應 `call` 方法,而且返回 `[status, headers, body]` 形式的結果,路由器就不知道這是個 Rack 程序還是動作。這里使用 `via: :all` 是正確的,因為我們想讓 Rack 程序自行判斷,處理所有 HTTP 方法。
其實 `'articles#index'` 的復雜形式是 `ArticlesController.action(:index)`,得到的也是個合法的 Rack 程序。
#### 3.14 使用 `root`
使用 `root` 方法可以指定怎么處理 `'/'` 請求:
```
root to: 'pages#main'
root 'pages#main' # shortcut for the above
```
`root` 路由應該放在文件的頂部,因為這是最常用的路由,應該先匹配。
`root` 路由只處理映射到動作上的 `GET` 請求。
在命名空間和作用域中也可使用 `root`。例如:
```
namespace :admin do
root to: "admin#index"
end
root to: "home#index"
```
#### 3.15 Unicode 字符路由
路由中可直接使用 Unicode 字符。例如:
```
get 'こんにちは', to: 'welcome#index'
```
### 4 定制資源式路由
雖然 `resources :articles` 默認生成的路由和幫助方法都滿足大多數需求,但有時還是想做些定制。Rails 允許對資源式幫助方法做幾乎任何形式的定制。
#### 4.1 指定使用的控制器
`:controller` 選項用來指定資源使用的控制器。例如:
```
resources :photos, controller: 'images'
```
能識別以 `/photos` 開頭的請求,但交給 `Images` 控制器處理:
| HTTP 方法 | 路徑 | 控制器#動作 | 具名幫助方法 |
| --- | --- | --- | --- |
| GET | /photos | images#index | photos_path |
| GET | /photos/new | images#new | new_photo_path |
| POST | /photos | images#create | photos_path |
| GET | /photos/:id | images#show | photo_path(:id) |
| GET | /photos/:id/edit | images#edit | edit_photo_path(:id) |
| PATCH/PUT | /photos/:id | images#update | photo_path(:id) |
| DELETE | /photos/:id | images#destroy | photo_path(:id) |
要使用 `photos_path`、`new_photo_path` 等生成該資源的路徑。
命名空間中的控制器可通過目錄形式指定。例如:
```
resources :user_permissions, controller: 'admin/user_permissions'
```
這個路由會交給 `Admin::UserPermissions` 控制器處理。
只支持目錄形式。如果使用 Ruby 常量形式,例如 `controller: 'Admin::UserPermissions'`,會導致路由報錯。
#### 4.2 指定約束
可以使用 `:constraints`選項指定 `id` 必須滿足的格式。例如:
```
resources :photos, constraints: {id: /[A-Z][A-Z][0-9]+/}
```
這個路由聲明限制參數 `:id` 必須匹配指定的正則表達式。因此,這個路由能匹配 `/photos/RR27`,不能匹配 `/photos/1`。
使用代碼塊形式可以把約束應用到多個路由上:
```
constraints(id: /[A-Z][A-Z][0-9]+/) do
resources :photos
resources :accounts
end
```
當然了,在資源式路由中也能使用非資源式路由中的高級約束。
默認情況下,在 `:id` 參數中不能使用點號,因為點號是格式化路由的分隔符。如果需要在 `:id` 中使用點號,可以添加一個約束條件。例如,`id: /[^\/]+/` 可以接受除斜線之外的所有字符。
#### 4.3 改寫具名幫助方法
`:as` 選項可以改寫常規的具名路由幫助方法。例如:
```
resources :photos, as: 'images'
```
能識別以 `/photos` 開頭的請求,交給 `PhotosController` 處理,但使用 `:as` 選項的值命名幫助方法:
| HTTP 方法 | 路徑 | 控制器#動作 | 具名幫助方法 |
| --- | --- | --- | --- |
| GET | /photos | photos#index | images_path |
| GET | /photos/new | photos#new | new_image_path |
| POST | /photos | photos#create | images_path |
| GET | /photos/:id | photos#show | image_path(:id) |
| GET | /photos/:id/edit | photos#edit | edit_image_path(:id) |
| PATCH/PUT | /photos/:id | photos#update | image_path(:id) |
| DELETE | /photos/:id | photos#destroy | image_path(:id) |
#### 4.4 改寫 `new` 和 `edit` 片段
`:path_names` 選項可以改寫路徑中自動生成的 `"new"` 和 `"edit"` 片段:
```
resources :photos, path_names: { new: 'make', edit: 'change' }
```
這樣設置后,路由就能識別如下的路徑:
```
/photos/make
/photos/1/change
```
這個選項并不能改變實際處理請求的動作名。上述兩個路徑還是交給 `new` 和 `edit` 動作處理。
如果想按照這種方式修改所有路由,可以使用作用域。 T> T> T>`ruby TIP: scope path_names: { new: 'make' } do TIP: # rest of your routes TIP: end TIP:`
#### 4.5 為具名路由幫助方法加上前綴
使用 `:as` 選項可在 Rails 為路由生成的路由幫助方法前加上前綴。這個選項可以避免作用域內外產生命名沖突。例如:
```
scope 'admin' do
resources :photos, as: 'admin_photos'
end
resources :photos
```
這段路由會生成 `admin_photos_path` 和 `new_admin_photo_path` 等幫助方法。
要想為多個路由添加前綴,可以在 `scope` 方法中設置 `:as` 選項:
```
scope 'admin', as: 'admin' do
resources :photos, :accounts
end
resources :photos, :accounts
```
這段路由會生成 `admin_photos_path` 和 `admin_accounts_path` 等幫助方法,分別映射到 `/admin/photos` 和 `/admin/accounts` 上。
`namespace` 作用域會自動添加 `:as` 以及 `:module` 和 `:path` 前綴。
路由幫助方法的前綴還可使用具名參數:
```
scope ':username' do
resources :articles
end
```
這段路由能識別 `/bob/articles/1` 這種請求,在控制器、幫助方法和視圖中可使用 `params[:username]` 獲取 `username` 的值。
#### 4.6 限制生成的路由
默認情況下,Rails 會為每個 REST 路由生成七個默認動作(`index`,`show`,`new`,`create`,`edit`,`update` 和 `destroy`)對應的路由。你可以使用 `:only` 和 `:except` 選項調整這種行為。`:only` 選項告知 Rails,只生成指定的路由:
```
resources :photos, only: [:index, :show]
```
此時,向 `/photos` 能發起 GET 請求,但不能發起 `POST` 請求(正常情況下由 `create` 動作處理)。
`:except` 選項指定**不用**生成的路由:
```
resources :photos, except: :destroy
```
此時,Rails 會生成除 `destroy`(向 `/photos/:id` 發起的 `DELETE` 請求)之外的所有常規路由。
如果程序中有很多 REST 路由,使用 `:only` 和 `:except` 指定只生成所需的路由,可以節省內存,加速路由處理過程。
#### 4.7 翻譯路徑
使用 `scope` 時,可以改寫資源生成的路徑名:
```
scope(path_names: { new: 'neu', edit: 'bearbeiten' }) do
resources :categories, path: 'kategorien'
end
```
Rails 為 `CategoriesController` 生成的路由如下:
| HTTP 方法 | 路徑 | 控制器#動作 | 具名幫助方法 |
| --- | --- | --- | --- |
| GET | /kategorien | categories#index | categories_path |
| GET | /kategorien/neu | categories#new | new_category_path |
| POST | /kategorien | categories#create | categories_path |
| GET | /kategorien/:id | categories#show | category_path(:id) |
| GET | /kategorien/:id/bearbeiten | categories#edit | edit_category_path(:id) |
| PATCH/PUT | /kategorien/:id | categories#update | category_path(:id) |
| DELETE | /kategorien/:id | categories#destroy | category_path(:id) |
#### 4.8 改寫單數形式
如果想定義資源的單數形式,需要在 `Inflector` 中添加額外的規則:
```
ActiveSupport::Inflector.inflections do |inflect|
inflect.irregular 'tooth', 'teeth'
end
```
#### 4.9 在嵌套資源中使用 `:as` 選項
`:as` 選項可以改自動生成的嵌套路由幫助方法名。例如:
```
resources :magazines do
resources :ads, as: 'periodical_ads'
end
```
這段路由會生成 `magazine_periodical_ads_url` 和 `edit_magazine_periodical_ad_path` 等幫助方法。
### 5 路由審查和測試
Rails 提供有路由審查和測試功能。
#### 5.1 列出現有路由
要想查看程序完整的路由列表,可以在**開發環境**中使用瀏覽器打開 `http://localhost:3000/rails/info/routes`。也可以在終端執行 `rake routes` 任務查看,結果是一樣的。
這兩種方法都能列出所有路由,和在 `routes.rb` 中的定義順序一致。你會看到每個路由的以下信息:
* 路由名(如果有的話)
* 使用的 HTTP 方法(如果不響應所有方法)
* 匹配的 URL 模式
* 路由的參數
例如,下面是執行 `rake routes` 命令后看到的一個 REST 路由片段:
```
users GET /users(.:format) users#index
POST /users(.:format) users#create
new_user GET /users/new(.:format) users#new
edit_user GET /users/:id/edit(.:format) users#edit
```
可以使用環境變量 `CONTROLLER` 限制只顯示映射到該控制器上的路由:
```
$ CONTROLLER=users rake routes
```
拉寬終端窗口直至沒斷行,這時看到的 `rake routes` 輸出更完整。
#### 5.2 測試路由
和程序的其他部分一樣,路由也要測試。Rails [內建了三個斷言](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/RoutingAssertions.html),可以簡化測試:
* `assert_generates`
* `assert_recognizes`
* `assert_routing`
##### 5.2.1 `assert_generates` 斷言
`assert_generates` 檢測提供的選項是否能生成默認路由或自定義路由。例如:
```
assert_generates '/photos/1', { controller: 'photos', action: 'show', id: '1' }
assert_generates '/about', controller: 'pages', action: 'about'
```
##### 5.2.2 `assert_recognizes` 斷言
`assert_recognizes` 是 `assert_generates` 的反測試,檢測提供的路徑是否能陪識別并交由特定的控制器處理。例如:
```
assert_recognizes({ controller: 'photos', action: 'show', id: '1' }, '/photos/1')
```
可以使用 `:method` 參數指定使用的 HTTP 方法:
```
assert_recognizes({ controller: 'photos', action: 'create' }, { path: 'photos', method: :post })
```
##### 5.2.3 `assert_routing` 斷言
`assert_routing` 做雙向測試:檢測路徑是否能生成選項,以及選項能否生成路徑。因此,綜合了 `assert_generates` 和 `assert_recognizes` 兩個斷言。
```
assert_routing({ path: 'photos', method: :post }, { controller: 'photos', action: 'create' })
```
### 反饋
歡迎幫忙改善指南質量。
如發現任何錯誤,歡迎修正。開始貢獻前,可先行閱讀[貢獻指南:文檔](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