# 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