更多內容 rubyonrails.org: 更多內容

Rails 路由:深入淺出

本篇介紹與使用者息息相關的路由功能。

讀完本篇,您將了解:

1 Rails 路由器的目的

Rails 路由器(router)識別網址,分配給對應的 Controller 動作處理。Rails 路由器同時也可用來產生路徑與網址,避免在 View 裡面用字串來把路徑與網址寫死。

1.1 路由器如何分派請求

當 Rails 收到如下請求時:

GET /patients/17

會詢問路由器,匹配的 Controller 動作是那個。若第一筆匹配的路由為:

get '/patients/:id', to: 'patients#show'

則請求會分派給 PatientsControllershow 動作處理,且 params 裡有 { id: '17' } 參數。

1.2 產生路徑與網址

Rails 路由器也可以產生路徑與網址。若上例的路由改寫為:

get '/patients/:id', to: 'patients#show', as: 'patient'

且應用程式 Controller 裡有以下程式碼:

@patient = Patient.find(17)

並有對應的 View:

<%= link_to 'Patient Record', patient_path(@patient) %>

則路由器便會給 patient_path(@patient) 產生路徑 /patients/17。這使得程式碼更容易了解。注意,使用路由輔助方法(patient_path)時,無需指定 ID。

2 資源式路由:Rails 的預設路由

資源式路由可替資源式 Controller 快速宣告出所有常見的路由。與其挨個替每個動作( indexshowneweditcreateupdate 以及 destroy)宣告路由,資源式路由宣告一行解決。

2.1 Web 世界裡的資源

瀏覽器向 Rails 請求頁面時,透過使用具體的 HTTP 動詞,如 GETPOSTPATCHPUT 以及 DELETE,往 URL 發出請求。每個動詞都是對資源的一種操作。資源式路由將請求對應到 Controller 的動作。

當 Rails 應用程式收到下面這個請求時:

DELETE /photos/17

會詢問路由器,路由器會決定該交給那個 Controller 的那個動作處理。若第一筆匹配的路由為:

resources :photos

Rails 會將請求分派給 PhotosControllerdestroy 方法,且 params 裡有 { id: '17' } 參數。

2.2 CRUD、HTTP 動詞以及動作

在 Rails 裡,資源式路由提供 HTTP 動詞、URL、Controller 動作,這三者的對應關係。按照慣例,每個動作會對應到資料庫特定的 CRUD 操作。假設路由檔案裡有一條路由宣告為:

resources :photos

會建立出七筆不同的路由,皆對應到 PhotosController

HTTP 動詞 路徑 Controller#動作 用途
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 對應到七種不同的動作。

路由依據宣告的順序來匹配。若在 get 'photos/poll' 之前宣告了 resources :photos,則 show 動作會先匹配到 resources :photos。若想將 get 'photos/poll' 的匹配順序提前,提到 resources :photos 之前即可。

2.3 路徑與 URL 的輔助方法

新建一筆資源式的路由,同時會給應用程式裡的 Controller 加入一些輔助方法。以 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),*_url 會回傳完整的路徑,包含了主機、埠口以及路徑。

2.4 同時定義多筆資源

如需同時給多個資源建立路由,可以用一行 resources 宣告完成,可節省些打字的時間:

resources :photos, :books, :videos

等同於:

resources :photos
resources :books
resources :videos

2.5 單數資源

有些資源無需 ID 便能查詢。舉個例子,希望 /profile 顯示目前登入使用者的個人檔案。可以用單數資源(Singular resource),把 /profile(而不是 /profile/:id)對應到 show 動作:

get 'profile', to: 'users#show'

:to 選項傳入字串要使用 controller#action 的形式,傳入符號會直接對應到動作名稱。

get 'profile', to: :show

以下這筆單數資源式路由:

resource :geocoder

會建立出六筆不同的路由,皆對應到 GeocodersController

HTTP 動詞 路徑 Controller#動作 用途
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)想交給同樣的 Controller 處理,或是把單數資源對應到複數 Controller 上。舉個例子,resource :photoresources :photos 同時建立出單數與複數的路由,皆對應到 PhotosController

單數的資源式路由會產生以下輔助方法:

輔助方法 用途
new_geocoder_path 回傳 /geocoder/new
edit_geocoder_path 回傳 /geocoder/edit
geocoder_path 回傳 /geocoder

和複數資源的路由相同,皆有對應的 *_url 形式,會回傳完整的路徑,包含了主機、埠口以及路徑。

有一個存在已久的 Bug 導致 form_for 無法自動處理好單數資源。解決辦法是給表單明確指定 URL:

form_for @geocoder, url: geocoder_path do |f|

2.6 Controller 命名空間與路由

有時可能想把一堆 Controllers 放在同個命名空間下管理。最常見的場景是需要把管理用的 Controller 放在 Admin:: 命名空間下。首先將這些 Controllers 搬到 app/controllers/admin 資料夾底下,接著在路由裡宣告:

namespace :admin do
  resources :articles, :comments
end

會給 ArticlesComments Controllers 在 Admin:: 命名空間下建出路由。比如 Admin::ArticlesController,Rails 會產生以下路由:

HTTP 動詞 路徑 Controller#動作 輔助方法
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)

若想把路徑拿掉 /admin 前綴,則可以這麼宣告:

scope module: 'admin' do
  resources :articles, :comments
end

如只有一筆資源,則可簡寫為:

resources :articles, module: 'admin'

若路由希望是 /admin/articles,但想拿掉 Admin:: 的前綴,可以這麼宣告:

scope '/admin' do
  resources :articles, :comments
end

如只有一筆資源,則可簡寫為:

resources :articles, path: '/admin/articles'

以上這些例子,若沒有使用 scope,則輔助方法保持不變。看看上面最後一個使用 scope 的例子(與前個表格對比看看那裡不一樣),Rails 會產生以下路由:

HTTP 動詞 路徑 Controller#動作 輔助方法
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 區塊裡,使用不同的命名空間。可以指定 Controller 的絕對路徑:get '/foo' => '/foo#index'

2.7 嵌套資源

資源是其它資源的子資源是很常見的情況。舉例來說,假設應用程式有雜誌與廣告兩個 Model:

class Magazine < ActiveRecord::Base
  has_many :ads
end

class Ad < ActiveRecord::Base
  belongs_to :magazine
end

這種關係可以用嵌套路由來描述。在這個情況裡,可以這麼宣告路由:

resources :magazines do
  resources :ads
end

上面會建立 MagazinesController 的路由,也會給 AdsController 建立路由。Ad 的路徑裡會需要引用 Magazine 資源:

HTTP 動詞 路徑 Controller#動作 用途
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 寫出一篇流行的文章,文章總結了好的 Rails 的設計經驗準則:

嵌套資源永遠不要超過 1 層。

2.7.2 淺層嵌套

避免多層嵌套的方法之一,是將 Controller 的集合動作放在父資源的作用域底下,這樣可以有階層的概念,但不需要嵌套的成員動作。也就是說,只用最少的資源資訊來表示路由,像是:

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 可以在 member path 前加上指定的前綴:

scope shallow_path: "sekret" do
  resources :articles do
    resources :comments, shallow: true
  end
end

comments 資源會有下列路由:

HTTP Verb Path Controller#Action Named Helper
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 選項則是給 named helpers 加上前綴:

scope shallow_prefix: "sekret" do
  resources :articles do
    resources :comments, shallow: true
  end
end

comments 資源會有下列路由:

HTTP Verb Path Controller#Action Named Helper
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 允許將常見的路由宣告為可重用的,可在其他資源與路由裡使用。定義一個 Routing Concerns:

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 時,可以傳入 MagazineAd 的實體,而不需要傳入 ID:

<%= link_to 'Ad details', magazine_ad_path(@magazine, @ad) %>

也可以使用 url_for,搭配一組物件,則 Rails 會自動決定要用那個路由:

<%= link_to 'Ad details', url_for([@magazine, @ad]) %>

這個情況裡,Rails 看到 @magazine@ad,會使用 magazine_ad_path 輔助方法。在像是 link_to 的輔助方法,可以直接指定物件,省略 url_for

<%= link_to 'Ad details', [@magazine, @ad] %>

若想要只連到雜誌:

<%= link_to 'Magazine details', @magazine %>

要連到不同的動作,在參數陣列的第一個元素,指定動作名稱即可: erb <%= link_to 'Edit Ad', [:edit, @magazine, @ad] %>

這種用法可以將 Model 的實體當做 URL 看待,是使用資源式路由的主要優勢之一。

2.10 新增更多資源式路由

不受限於七個預設產生的資源式路由。還可以新增更多集合路由、成員路由。

2.10.1 新增成員路由

要新增成員路由,只需要在 resources 區塊裡加入 member 區塊:

resources :photos do
  member do
    get 'preview'
  end
end

這會識別出 /photos/1/preview 的 GET 請求,交給 PhotosControllerpreview 動作處理,相片的 ID 會存在 params[:id]。也會新增 preview_photo_pathpreview_photo_url 輔助方法。

在成員路由的區塊裡,可以指定使用特定的 HTTP 動詞。可用的有:getpatchputpostdelete。若成員路由只有一筆,可以使用 :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

這會使 Rails 識別出發送到 /photos/search 的 GET 請求,交給 PhotosControllersearch 動作處理。也會新增 search_photos_pathsearch_photos_url 輔助方法。

和成員路由相同,可以傳入 :on 選項:

resources :photos do
  get 'search', on: :collection
end

2.10.3 給額外的新動作新增路由

要新增額外的 new 動作,可以使用 :on 選項:

resources :comments do
  get 'preview', on: :new
end

這會使 Rails 識別出發送到 /comments/new/preview 的 GET 請求,交給 CommentsControllerpreview 動作處理。也會新增 preview_new_comment_pathpreview_new_comment_url 輔助方法。

若發現給資源新增了許多額外的動作,停下來想想是不是要拆成另一個資源。

3 非資源式路由

除了資源式路由之外,將隨意的 URL 對應到動作,Rails 提供了強大的支持。這一節不像資源式路由,會獲得一組自動產生的路由。反而是自己在應用程式裡設定每一條路由。

雖然通常應該要使用資源式路由,但仍有許多簡單的路由更合適的場景。也不用整個應用程式都得用資源式風格的路由才行,選擇最合適的解決方案。

簡單的路由使得把傳統的 URL 對應到 Rails 動作變得特別簡單。

3.1 綁定參數

設定一般的路由時,提供一系列的符號給 Rails,Rails 會根據這些符號來對進來的 HTTP 請求做匹配。有兩個特殊符號::controller 會對應到應用程式裡的 Controller 名稱,而 :action 則是對應到該 Controller 的動作。舉例來說,看下面這條路由:

get ':controller(/:action(/:id))'

若發送到 /photos/show/1 的請求由這條路由處理(路由檔案裡沒有其它匹配的路由),則會呼叫 PhotosControllershow 動作,並將 params{:id] 設為 "1"。這條路由也會處理發送到 /photos 的請求,將請求交給 PhotosController#index 處理。因為 :action:id 放在括號裡代表是可選參數。

3.2 動態片段

一般的路由裡可以設定多個動態片段。路由裡任何不是 :controller:action 的選項,都會變成 params 的一部分。若有以下路由:

get ':controller/:action/:id/:user_id'

任何至 /photos/show/1/2 的請求會被分配給 PhotosControllershow 動作處理,params[:id] 則會設為 "1",而 params[:user_id] 則是 "2"

路徑片段有 :controller 時,無法與 :namespace:module 一起使用。若需要這麼做的話,對 :controller 使用約束條件,明確指定要匹配的命名空間,譬如:

get ':controller(/:action(/:id))', controller: /admin\/[^\/]+/

動態片段預設不接受 . ── 這是因為 . 是格式化路由的分隔符。如需要在動態片段裡使用 .,用約束條件來處理 ── 例如,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 進來的請求會分配給 PhotosControllershow 動作處理,params 的內容為 { controller: 'photos', action: 'show', id: '1', user_id: '2' }

3.5 定義預設值

路由裡不需要明確使用 :controller:action。可以指定預設值:

get 'photos/:id', to: 'photos#show'

有了這條路由之後,Rails 會分配 /photos/12PhotosControllershow 動作。

也可以傳 Hash 給 :defaults 選項,來給路由新增預設值。這對於沒有寫動態片段的路由也適用。舉例來說:

get 'photos/:id', to: 'photos#show', defaults: { format: 'jpg' }

Rails 會匹配 photos/12 to the show action of PhotosController, and set params[:format] to "jpg".

3.6 命名路由

可以使用 :as 選項給任何路由取名字:

get 'exit', to: 'sessions#destroy', as: :logout

會建立出 logout_pathlogout_url 這兩個具名輔助方法。呼叫 logout_path 會回傳 /exit

也可以使用 :as 來覆寫 resources 預設定義的方法:

get ':username', to: 'users#show', as: :user

會定義 user_path 方法,在 Controller、輔助方法、以及 View 裡都可用,會回傳像是 /bob 的路徑。在 UsersControllershow 動作裡,params[:username] 會有使用者的 username,如不喜歡參數名稱取名為 :username 可以修改這個值。

3.7 HTTP 動詞約束條件

通常應該使用 getpostputpatch 以及 delete 方法來限制路由只處理特定的動詞。match 方法與 :via 選項可以一直匹配多個動詞:

match 'photos', to: 'photos#show', via: [:get, :post]

路由要匹配所有的動詞也可以,via: :all

match 'photos', to: 'photos#show', via: :all

GETPOST 請求路由到單一的動作有安全隱憂。除非有很好的理由,通常應該要避免將所有 HTTP 動詞對應到一個動作上。

Rails 裡執行 GET 不會檢查 CSRF token。永遠不要讓 GET 請求可以執行資料庫寫入動作,更多資訊請參考《安全指南》 關於 〈CSRF countermeasures〉 一節。

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 雖然接受正規表示法,但不能使用錨點(anchors)。比如以下路由不會正常工作:

get '/:id', to: 'articles#show', constraints: {id: /^\d/}

但其實不需要使用錨點,因為所有的路由皆從頭開始匹配。

舉個例子,下面的路由,若 articles 呼叫 to_param 的值像是 1-hello-world,以數字開頭,就會把請求交給 ArticlesControllershow 動作處理;而 to_param 的值不以數字開頭,像是 david,則會交給 UsersControllershow 動作處理。

get '/:id', to: 'articles#show', constraints: { id: /\d.+/ }
get '/:username', to: 'users#show'

3.9 基於請求的約束條件

也可以使用 request 物件裡,任何會回傳字串的方法,來宣告路由的約束條件。

基於 request 物件的約束條件的宣告方式與片段約束條件相同:

get 'photos', to: 'photos#index', constraints: { subdomain: 'admin' }

約束條件也可以使用區塊形式:

namespace :admin do
  constraints subdomain: 'admin' do
    resources :photos
  end
end

3.10 進階約束條件

如有更進階的約束條件,可以傳入一個回應 matches? 的物件。假設需要將所有黑名單的使用者交給 BlackListController 處理,可以這麼做:

class BlacklistConstraint
  def initialize
    @ips = Blacklist.retrieve_ips
  end

  def matches?(request)
    @ips.include?(request.remote_ip)
  end
end

Rails.application.routes.draw do
  get '*path', to: 'blacklist#index',
    constraints: BlacklistConstraint.new
end

約束條件也可用 lambda 宣告:

Rails.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-memoirparams[:section] 會設為 "some/section",而 params[:title] 則是 "last-words-a-memoir"

技術上來說,路由可有多個通配片段。匹配器會以直觀的方式來將參數賦值給片段,比如:

get '*a/foo/*b', to: 'test#index'

會匹配 zoo/woo/foo/bar/bazparams[:a] 會設為 'zoo/woo',而 params[:b] 則是 'bar/baz'

發送請求到 "/foo/bar.json" 時,params[:pages] 會設為 "foo/bar",請求格式為 JSON。若想使用 3.0.x 的行為,可以傳一個 format: false

get '*pages', to: 'pages#show', format: false

若想要設定格式為必要參數,則可以傳 format: true

get '*pages', to: 'pages#show', format: true

3.12 轉址

可以使用 redirect 輔助方法將甲路徑轉到乙路徑:

get '/stories', to: redirect('/articles')

轉址也可以重複使用匹配路由的動態片段:

get '/stories/:name', to: redirect('/articles/%{name}')

redirect 也可以以區塊形式定義,接受 path 參數與 request 物件:

get '/stories/:name', to: redirect { |path_params, req| "/articles/#{path_params[:name].pluralize}" }
get '/stories', to: redirect { |path_params, req| "/articles/#{req.subdomain}" }

Note: 轉址是 301 "Moved Permanently" 轉址。某些瀏覽器或代理伺服器會快取 301 轉址,導致舊的頁面無法存取。

以上所有的情況裡,若沒有提供主機(http://www.example.com),Rails 會從當下的請求裡取得。

3.13 路由到 Rack 應用程式

除了使用像是 "articles#index" 的字串(會交給 ArticlesControllerindex 動作處理),還可以指定任何 Rack 應用程式 作為端點(Endpoint):

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 指定 Rails 要把 "/" 路由到那裡:

root to: 'pages#main'
root 'pages#main' # 上例的縮寫

root 路由應放在 config/routes.rb 的最上方,因為這是最重要的路由,應該第一個匹配。

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

:controller 選項可明確指定資源要使用的 Controller:

resources :photos, controller: 'images'

會識別出以 /photos 開頭的請求,交給 Images Controller 處理:

HTTP 動詞 路徑 Controller#動作 具名輔助方法
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_pathnew_photo_path

具有命名空間的 Controller,指定時使用和目錄一樣的表示法即可:

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/1,而是會匹配 /photos/RR27

可以將約束條件應用至多條路由,使用以下的區塊形式:

constraints(id: /[A-Z][A-Z][0-9]+/) do
  resources :photos
  resources :accounts
end

當然這裡也可以使用非資源式路由的進階約束條件。

:id 預設不接受 . ── 這是因為 . 是格式化路由的分隔符。如需要在 :id 裡使用 .,用約束條件來處理 ── 例如,id: /[^\/]+/ 允許斜線以外的所有字元。

4.3 覆寫具名輔助方法

:as 選項可以覆寫具名輔助方法的名稱。比如:

resources :photos, as: 'images'

will recognize incoming paths beginning with /photos and route the requests to PhotosController, but use the value of the :as option to name the helpers.

HTTP 動詞 路徑 Controller#動作 具名輔助方法
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 覆寫 newedit 片段

:path_names 選項可以覆寫自動在路徑裡產生的 newedit 片段:

resources :photos, path_names: { new: 'make', edit: 'change' }

路由路徑變更為:

/photos/make
/photos/1/change

實際的動作名稱並沒有改變。這兩個路徑仍路由到 new and edit 動作。

若發現一直使用 :path_names 的話,考慮使用 scope

scope path_names: { new: 'make' } do
  # rest of your routes
end

4.5 給具名輔助方法加前綴

:as 選項可以給具名輔助方法加上前綴。使用此選項用來避免路由之間的命名衝突,例如:

scope 'admin' do
  resources :photos, as: 'admin_photos'
end

resources :photos

會產生像是 admin_photos_pathnew_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 的路由,並允許在 Controller、View 以及輔助方法使用 params[:username] 來存取路徑傳入的 username

4.6 限制建立出來的路由

Rails 預設會為以資源式風格宣告的路由的七個動作(index、show、new、create、edit、update 以及 destroy)建立路由。可用 :only 以及 :except 選項來調整這個行為。:only 選項告訴 Rails 只需要產生那些路由就好:

resources :photos, only: [:index, :show]

/photos 收到 GET 請求會正常工作,但 POST 請求(通常會路由到 :create 動作)則會失敗。

The :except 選項指定不要建立的路由:

resources :photos, except: :destroy

這個情況 Rails 會建立出除了 :destory (發送 DELETE 請求到 /photos/:id)之外所有的路由。

如應用程式有許多 RESTful 風格的路由,使用 :only:except 選項來產生需要的路由。這樣可以減少記憶體的使用量,並加速路由過程。

4.7 翻譯路徑

使用 scope,可以修改由 resources 產生的路徑名稱:

scope(path_names: { new: 'neu', edit: 'bearbeiten' }) do
  resources :categories, path: 'kategorien'
end

產生的路由:

HTTP 動詞 路徑 Controller#動作 具名輔助方法
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

這會產生出像是:edit_magazine_periodical_ad_pathmagazine_periodical_ads_url 等輔助方法。

4.10 覆寫具名路由參數

:param 選項可以覆寫辨識資源的變數,預設是 :id動態片段的名稱)。這個變數是用來產生路由的。可以在 Controller 使用 params[<:param>] 傳入片段變數。

resources :videos, param: :identifier

     videos GET  /videos(.:format)                  videos#index
            POST /videos(.:format)                  videos#create
 new_videos GET  /videos/new(.:format)              videos#new
edit_videos GET  /videos/:identifier/edit(.:format) videos#edit

Video.find_by(identifier: params[:identifier])

5 檢查與測試路由

Rails 有提供用來檢查與測試路由的工具。

5.1 列出現有的路由

要獲得應用程式完整可用的路由列表,在開發環境下啟動伺服器,瀏覽 http://localhost:3000/rails/info/routes。也可以在終端機裡執行 rake routes 命令。

以上兩種方法都會列出所有的路由,順序與 config/routes.rb 裡定義的一樣,每條路由的內容會有:

  • 路由名稱(有的話)
  • 使用的 HTTP 動詞(若不是回應所有動詞的路由)
  • 匹配的 URL 模式。
  • 路由的參數。

舉個例子,以下是某個 RESTful 路由的 rake routes 輸出的一小部分:

    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 的路由:

$ CONTROLLER=users rake routes

若將終端機視窗拉寬到無斷行,會發現 rake routes 輸出的可讀性更高。

5.2 測試路由

路由和其它部分的程式一樣,也需要測試。Rails 提供三個內建的斷言方法,專門為簡化路由測試而設計:

  • 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_recognizesassert_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_generatesassert_recognizes 的功能:

assert_routing({ path: 'photos', method: :post }, { controller: 'photos', action: 'create' })

反饋

歡迎幫忙改善指南的品質。

如發現任何錯誤之處,歡迎修正。開始貢獻前,可以先閱讀貢獻指南:文件

翻譯如有錯誤,深感抱歉,歡迎 Fork 修正,或至此處回報

文章可能有未完成或過時的內容。請先檢查 Edge Guides 來確定問題在 master 是否已經修掉了。再上 master 補上缺少的文件。內容參考 Ruby on Rails 指南準則來了解行文風格。

最後,任何關於 Ruby on Rails 文件的討論,歡迎至 rubyonrails-docs 郵件論壇