# 2.3 深入路由(routes)
## 概要:
本課時詳細解讀如何設置復雜情況下的路由(routes),以及路由文件中常用方法。
## 知識點:
1. routes 定義
1. 嵌套(nested)
1. namespace
1. concern
1. 參數
1. 測試
## 正文
### 2.3.1 定義路由(routes)
上一節,我們講了 Rails 通過 routes,來實現 REST 風格的架構。本節我們講詳細介紹下如何使用 routes,定義我們想要的地址(URL)。
我們先為項目,創建一個 controller:
~~~
rails g controller home index welcome about contact
~~~
在我們專門講解 controller 前,先簡單解釋下:
- g 是 generate 的縮寫,我想你已經在 2.1.1 里看到了。
- controller,說明我們創建的是一個 controller,也可以是 model。
- home 是 controller 的名字。
- index... 和其他幾個名字,是 controller 中的方法,并且會自動創建對應的 views 文件。
好了,我們在它上面做一些簡單的例子,打開routes,你可以看到它已經增加了幾個定義:
~~~
get 'home/index'
get 'home/welcome'
get 'home/about'
get 'home/contact'
~~~
我們訪問 `http://localhost:3000/home/index` 可以看到它。但是,如果我想訪問 `http://localhost:3000/` 就進入到 index 方法呢?
~~~
get '/', to: 'home#index'
get '/welcome', to: 'home#welcome'
~~~
如上,我們自己定義了訪問和方法之間的對應關系。其實我們更經常使用 root 來定義地址:
~~~
root 'home#index'
~~~
運行 `rake routes`,我們可以看到
| Prefix | Verb | URI Pattern | Controller#Action |
|-----|-----|-----|-----|
| home_contact | GET | /home/contact(.:format) | home#contact |
| | GET | / | home#index |
| welcome | GET | /welcome(.:format) | home#welcome |
| root | GET | / | home#index |
我們也可以用其他的 Verb 來定義非 GET 請求,比如
~~~
put '/haha', to: 'home#index'
delete '/hehe', to: 'home#index'
patch '/wawa', to: 'home#index'
~~~
routes 中我們可以拋開資源的要求(非 REST 風格),直接設定一個訪問地址:
~~~
get '/something/:controller/:name/:action'
~~~
這時我們訪問 `http://localhost:3000/home/aaa/index` 也會進入到 `'home#index'` 中,因為 Rails 會 這樣解析:
- something 是個前綴
- 訪問的 controller 是 home
- name 參數是 aaa
- 方法是 index
建議你看一下的終端:
~~~
Started GET "/something/home/aaa/index" for ::1 at 2015-02-19 17:10:26 +0800
Processing by HomeController#index as HTML
Parameters: {"name"=>"aaa"}
~~~
Rails 已經將你的請求轉移到對應的 controller 中了。
如果一個地址,即可以接收 post 請求,也可以接收 get 等請求,我們可以使用 match 方法:
~~~
match ':controller/:action/:id', via: [:get, :post]
~~~
提示:在開發(development)環境中,修改 routes 是不需要重啟服務的。
#### 2.3.1.1 擴展 resources
前面我們已經定義了一個 `resource :products`,這在實際開發中還是不夠的,比如,一個 Product 下如果查看評論,比如,顯示賣的最好的十個 Products:
~~~
resources :products do
collection do
get :top # 排行榜功能
end
member do
post :buy # 添加到購物車
end
end
~~~
運行 `rake routes` 可以看到:
| Prefix | Verb | URI Pattern | Controller#Action |
|-----|-----|-----|-----|
| top_products | GET | /products/top(.:format) | products#top |
| buy_product | POST | /products/:id/buy(.:format) | products#buy |
不同的是,collection 用于 products 中增加方法,member 給具體一個 product 增加方法。
補充一點,我們可以在一行里,定義多個 resources,比如:
~~~
resources :photos, :books, :videos
~~~
雖然方便,但不夠靈活,實踐中還是要按照需求調整的。
我們在這里提出了兩個功能需求:top 排行榜,和添加到購物車。這里我使用 trello.com 來記錄這兩個需求。

我在“計劃”里增加了一個 card,在 checklist 中記錄了這兩個需求。當我們開始功能開發的時候,可以將 card 拖動到“進行中”,當我們完成一個功能的時候,可以在 checklist 的項目前打一個√,當我們完成一個 card 的任務后,可以講 card 拖動到“完成”中。
Rails 被很多開發團隊使用,在一些開發團隊中,經常會提到敏捷開發,trello 是一個很好的敏捷開發工具,可以方便的管理我們的日常工作,和記錄項目進展狀態。
#### 2.3.1.2 單個資源 resource
~~~
resource :settings
resource :profile
~~~
這是設定一個單數資源的方法,項目里,哪些是單數呢?比如系統設定,比如當前用戶的個人信息,運行 `rake routes` 可以看到,它是沒有 `:id` 這個參數的。
在這個例子里,我們還未給 settings 和 profile 創建 controller 和 view,不過這不妨礙 routes 產生我們想要的地址。
#### 2.3.1.3 選擇方法
`resources` 給我們創建了七個方法,但是不見得我們都要用到,為了代碼的整潔[1],我們可以做一些排除:
~~~
resources :users, only: [:index, :show]
resources :products, except: [:destroy]
~~~
`only` 表示我們需要的方法,`except` 表示我們不需要的方法。通常,我們的確會像上面這么做,比如我們的網站只提供用戶(User)的列表和查看功能,而管理功能(增刪改)要在管理界面進行,而它的地址一般不會是 `/users/1/edit` 這樣,而是 `/admin/users/1/edit`。
[1]這是個人癖好,有的人的確不愿意這么做,不過 Rails 給了我們讓項目變得“整潔”的方法。
#### 2.3.1.5 地址解析的輔助方法
剛才,我們講到了 `_path` 這個后綴,Rails 還有一個 `_url`。
| 地址 | 結果 |
|-----|-----|
| products_path | '/products' |
| products_url | '[http://localhost:3000/products](http://localhost:3000/products)' |
`_path` 和 `_url` 是 routes 的輔助方法,我們在下一章將詳細介紹。
### 2.3.2 嵌套的路由(routes)
在我們定義資源的時候,有時候一個資源會有它的子資源,比如一個商品(product)會有多個商品種類(variants),當我們購買一個商品的時候,也需要選擇哪個種類,比如T恤的種類氛圍尺碼,而每一個尺碼有不同的價格。
這時該如何定義 routes 呢?
~~~
resources :products do
resources : variants
end
~~~
運行 `rake routes`,可以看到一個商品(product)下,增加了這些routes:
| Prefix | Verb | URI Pattern | Controller#Action |
|-----|-----|-----|-----|
| product_variants | GET | /products/:product_id/variants(.:format) | variants#index |
| | POST | /products/:product_id/variants(.:format) | variants#create |
| new_product_variant | GET | /products/:product_id/variants/new(.:format) | variants#new |
| edit_product_variant | GET | /products/:product_id/variants/:id/edit(.:format) | variants#edit |
| product_variant | GET | /products/:product_id/variants/:id(.:format) | variants#show |
| | PATCH | /products/:product_id/variants/:id(.:format) | variants#update |
| | PUT | /products/:product_id/variants/:id(.:format) | variants#update |
| | DELETE | /products/:product_id/variants/:id(.:format) | variants#destroy |
我們為 variants 也使用一下 `scaffold`:
~~~
rails g scaffold variant product_id:integer price:decimal size
~~~
在運行 `rails s` 前,記得要更新數據庫:
~~~
rake db:migrate
~~~
記得,我們應該刪除 routes 中自動添加的 `resources :vriants`,因為我們不需要在 `http://localhost:3000/variants` 下看到它,不是么?我們可以在每一個商品(Product)頁面,比如:`http://localhost:3000/products/1` 中看到它了。
### 2.3.3 路由中的命名空間(namespace)
接下來我們說兩個項目中經常會見到的情形。
一個項目,肯定要有 admin 的,我們如何把管理地址都放到 [http://localhost:3000/admin/](http://localhost:3000/admin/) 這個目錄下?
~~~
namespace :admin do
resources :products
end
~~~
這時,這樣就足夠了,不過,它所使用的 controller 和 view 是在 admin 這個文件夾下面的,多說一點,它的controller 代碼也是在 Admin 這個 module 下的。如果你還對 Ruby 的 module 不熟悉,是時候補充下了。
它的代碼是:
~~~
class Admin::ProductsController < ApplicationController
...
end
~~~
這里,我們反過來想,能否讓 `/admin/articles` 下的代碼去訪問 ArticlesController ?這里不再是 `Admin::` 開頭的。這時我們用到 `scope`:
~~~
scope '/admin' do
resources :articles
end
~~~
對于 admin 下的資源管理,可以試試 active admin 這個 Gem。
[https://github.com/activeadmin/activeadmin](https://github.com/activeadmin/activeadmin)
### 2.3.4 concern 方法
再來看一個讓 routes 更簡潔,也很實用的方法。
~~~
concern :commentable do
resources :comments
end
concern :image_attachable do
resources :images, only: :index
end
resources :messages, concerns: :commentable
resources :articles, concerns: [:commentable, :image_attachable]
~~~
`concern` 定義好的資源,可以被其他 resource 里多次引用。
Rails 的原則之一:`不要重復自己(Don't Repeat Yourself)`
### 2.3.5 有用的參數
#### :as 別名
如果再上面地址后面,加上 `as` 參數,會直接創建一個別名的地址,比如
~~~
get 'home/welcome', as: :welcome
~~~
之前,我們在 views 或者 controller 中,連接到或跳轉到 `/home/index` 可以這么寫:`home_welcome_path`,增加了 `:as` 后,就變成了 `welcome_path` 了。好處是,如果我們某一天更改了對應的 action 甚至 controller,這個寫法 `welcome_path` 是不會變的,而只需要改動 routes 中的定義。
在定義 routes 時,要注意不要重復定義,因為:寫在上面的會覆蓋下面的。比如:
~~~
get 'home/index', to: 'home#welcome'
get 'home/index'
~~~
訪問 `http://localhost:3000/home/index` 會進入到 `welcome` 方法中。
下面在介紹幾個實用的參數。
#### shallow
這時 Rails 4 中增加的一個很實用的參數。
~~~
resources :products do
resources :comments, shallow: true
end
~~~
它把 index、new 和 create 方法保留在了 `products/:id` 這個資源下,而把其他方法,重新放回到 `/comments` 下。這樣的考慮是避免過多的實用嵌套 routes,并且讓代碼更簡潔。
#### constraints
我們可以給 routes 建立約束(Constraints),比如:
~~~
get 'photos/:id', to: 'photos#show', constraints: { id: /[A-Z]\d{5}/ }
~~~
這時,id 為 A 到 Z 開頭,且后面為5位數字的 id,才符合路由條件,轉入到 `show` 方法。而 `products/A123456` 將會提示 `No route matches`。
### 2.3.6 Rspec 測試
通常,我們會在 controller 中寫上測試,不過 Rspec 也為我們提供了測試路由的方法。我們在 spec 下建立一個routing 文件夾,并且添加一個 `products_routing_spec.rb` 的文件:
~~~
RSpec.describe ProductsController, type: :routing do
describe "routing" do
it "routes to #index" do
expect(:get => "/products").to route_to("products#index")
end
...
~~~
我們為它單獨運行測試,因為scaffold 自動為我們添加的測試代碼,我們將在后面的章節完成:
~~~
% rspec spec/routing/products_routing_spec.rb
~~~
routes 測試的參考,可以查看[這里](https://github.com/rspec/rspec-rails#routing-specs)。
好了,本章結束了,本節的內容多來自 Rails 手冊中的 [Rails Routing from the Outside In](http://guides.rubyonrails.org/routing.html),你也可以找到在 [這里](https://github.com/liwei78/rails-practice-code) 找到本章調試的代碼。下一章,我們將開始完成 shop 的頁面(views)代碼,希望它可以讓你更加了解 Rails。
- 寫在前面
- 第一章 Ruby on Rails 概述
- Ruby on Rails 開發環境介紹
- Rails 文件簡介
- 用戶界面(UI)設計
- 第二章 Rails 中的資源
- 應用 scaffold 命令創建資源
- REST 架構
- 深入路由(routes)
- 第三章 Rails 中的視圖
- 布局和輔助方法
- 表單
- 視圖中的 AJAX 交互
- 模板引擎的使用
- 第四章 Rails 中的模型
- 模型的基礎操作
- 深入模型查詢
- 模型中的關聯關系
- 模型中的校驗
- 模型中的回調
- 第五章 Rails 中的控制器
- 控制器中的方法
- 控制器中的邏輯
- 第六章 Rails 的配置及部署
- Assets 管理
- 緩存及緩存服務
- 異步任務及郵件發送
- I18n
- 生產環境部署
- 常用 Gem
- 寫在后面