🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
# 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&gt; 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' =&gt; '/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&gt; I&gt; I&gt;`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&gt; I&gt; I&gt;`ruby NOTE: get '*pages', to: 'pages#show', format: false NOTE:` I&gt; NOTE: 如果必须指定格式,可以指定 `format: true` 选项,如下所示: I&gt; I&gt; I&gt;`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&gt; T&gt; T&gt;`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)。