> Weeks of programming can save you hours of planning. – Unknown
不同於靜態網頁的路由是直接對應於檔案的目錄結構,一個Web開發框架會將路由功能納入其中,來獲得最大的彈性。也就是您可以指定任意URL對應到任一個Controller的Action。另一方面,我們也不在Views中直接寫死URL網址,而是透過Helper輔助方法根據你的路由設定來產生URL,這樣也可以確定該網址一定有對應的Controller和Action,不然就會出現NoMethodError找不到Helper方法的錯誤。
也就是,路由系統做幾件事情:
1\. 辨識HTTP Request的URL網址,然後對應到設定的Controller Action。
2\. 處理網址內的參數字串,例如:/users/show/123送到Users controller的show action時,會將`params[:id]` 設定為 123
3\. 辨識link_to和redirect_to的參數產生URL字串,例如
~~~
link_to 'hola!', { :controller=> 'welcome', :action => 'say' }
~~~
會產生
~~~
<a href="/welcome/say">hola!</a>
~~~
Rails這麼彈性的路由功能,可以怎麼用呢?例如設計一個部落格網站,如果是沒有使用框架的CGI或PHP網頁開發,會長得這樣:
~~~
http://example.org/?p=123
~~~
但是如果我們想要將編號放在網址列中呢?
~~~
http://example.org/posts/123
~~~
或是希望根據日期:
~~~
http://example.org/posts/2011/04/21/
~~~
或者是根據不同作者加上文章的標籤(將關鍵字放在網址中有助於SEO):
~~~
http://example.org/ihower/posts/123-ruby-on-rails
~~~
這些在Rails只需要修改config/routes.rb這一個路由檔案,就可以完全自由自定。讓我們看看有哪些設定方式吧:
## 一般路徑Regular Routes
~~~
get 'meetings/:id', :to => 'events#show'
post 'meetings', :to => 'events#create'
~~~
這裡的`events#show`表示指向events controller的show action。通常會簡寫成:
~~~
get 'meetings/:id' => 'events#show'
~~~
其中有冒號`:id`的部分,會被轉成一個參數`params[:id]`傳進Controller裡。
> 注意到在routes.rb中,越上面越優先。是如果有網址同時符合多個規則,會使用最上面的規則。
## 外卡路由
~~~
match ':controller(/:action(/:id(.:format)))', :via => :all
~~~
這是我們在上一章所使用的方式,也是Rails 3.0之前版本的預設方式。其中的括弧用法表示這部份可有可無,也就是上述這一行設定就包括六種路徑方式:
~~~
match '/:controller', via: :all
match '/:controller/:action', via: :all
match '/:controller/:action/:id', via: :all
match '/:controller.:format', via: :all
match '/:controller/:action.:format', via: :all
match '/:controller/:action/:id.:format', via: :all
~~~
例如,像這樣的網址`http://localhost:3000/welcome/say`便會對應到welcome controller的say action。外卡路由是一種非常簡便的對應方式。這種方式的缺點當網站的Action變多的時候,會容易讓Controller的設計變得混亂沒有規則。稍後介紹的RESTful路由則是Rails對此提出的組織路由方案。
還有,`(.format)`這一段則會讓路由可以接受`.json`、`.xml`等有副檔名的網址,並且轉成`params[:format]`參數傳進Controller裡,搭配`respond_to`而回傳不同的格式。
## 命名路由Named Routes
Named Routes可以幫助我們產生URL helper如`meetings_url`或`meetings_path`,而不需要用`{:controller => 'meetings', :action => 'index'}`的方式:
~~~
get '/meetings' => 'events#index', :as => "meetings"
~~~
其中`:as`的部份就會產生一個`meetings_path`和`meetings_url`的Helpers,`_path`和`_url`的差別在於前者是相對路徑,後者是絕對路徑。一般來說比較常用`_path`方法,除非像是在Email信件中,才必須用`_url`提供包含Domain的完整網址。
> 雖然RESTful已經是設計Rails最常見的路徑模式,但是在一些特殊的情況、不符合CRUD模型的情結就不一定適用了,例如有多重步驟的表單(又叫作Wizard) 時,使用命名路由反而會比較簡潔,例如`step1_path, step2_path, step3_path`等。
## Redirect
在路由中可以直接設定轉向:
~~~
get "/foo" => redirect("/bar")
get "/ihower" => redirect("http://ihower.tw")
~~~
## 設定首頁
要設定網站的首頁,請設定:
~~~
root :to => 'welcome#show'
~~~
## HTTP動詞(Verb)限定
可以透過 :via 參數指定 HTTP Verb 動詞
~~~
match "account/overview" => "account#overview", :via => :get
match "account/setup" => "account#setup", :via => [:get, :post]
match "account/overview" => "account#overview", :via => :all
~~~
或是
~~~
get "account/overview" => "account#overview"
get "account/setup" => "account#setup"
post "account/setup" => "account#setup"
~~~
## Scope 規則
`scope`方法可以讓我們DRY我們的路由規則,將共通的controller、constraints、網址前置path和URL Helper前置名稱移到`scope`成為參數。例如
~~~
get 'foo/meetings/:id', :to => 'events#show'
post 'foo/meetings', :to => 'events#create'
~~~
可以改寫成
~~~
scope :controller => "events", :path => "/foo", :as => "bar" do
get 'meetings/:id' => :show, :as => "meeting"
post 'meetings' => ':create , :as => "meetings"
end
~~~
其中`as`會產生URL helper是`bar_meeting_url`和`bar_meetings_url`。
### Scope Module
Module參數則可以讓Controller分Module,例如
~~~
scope :path => '/api/v1/', :module => "api_v1", :as => 'v1' do
resources :projects
end
~~~
如此controller會是`ApiV1::ProjectsController`,網址如/api/v1/projects,而URL Helper如`v1_projects_path`這樣的形式。
### 領域名稱Namespace
Namespace是Scope的一種特定應用,特別適合例如後台介面,這樣就整組`controller`、網址`path`、URL Helper前置名稱`都影響到:
~~~
namespace :admin do
resources :projects
end
~~~
如此controller會是`Admin::ProjectsController`,網址如/admin/projects,而URL Helper如`admin_projects_path`這樣的形式。
## 特殊條件限定
我們可以利用`:constraints`設定一些參數限制,例如限制`:id`必須是整數。
~~~
match "/events/show/:id" => "events#show", :constraints => {:id => /\d/}
~~~
另外也可以限定subdomain子網域:
~~~
namespace :admin do
constraints subdomain: 'admin' do
resources :photos
end
end
~~~
甚至可以限定IP位置:
~~~
constraints(:ip => /(^127.0.0.1$)|(^192.168.[0-9]{1,3}.[0-9]{1,3}$)/) do
match "/events/show/:id" => "events#show"
end
~~~
## RESTful路由
我們在第六章介紹過RESTful路由的來龍去脈,接下來仔細看看其中的設定。
### 複數資源
~~~
resources :events
~~~
### 單數資源Singular Resoruce
除了一般複數型Resources,在單數的使用情境下也可以設定成單數Resource:
~~~
resource :map
~~~
特別之處在於那就沒有index action了,所有的URL Helper也皆為單數形式,顯示出來的網址也是單數。
> 但是Singular resource的檔案命名仍為複數,例如maps_controller.rb
### 套疊Nested Resources
當一個Resource一定會依存另一個Resource時,我們可以套疊多層的Resources,例如以下是任務一定屬於在專案底下:
~~~
resources :projects do
resources :tasks
end
~~~
如此產生的URL Helper如`project_tasks_path(@project)`和`project_task_path(@project, @task)`,它的網址會如projects/123/tasks和projects/123/tasks/123。
> 實務上不建議設計超過兩層,一來是路由會太長,二來也是不必要的依賴。
### 指定Controller
resource預設採用同名的controller,我們可以改指定,例如
~~~
resources :projects do
resources :tasks, :controller => "project_tasks"
end
~~~
### 自定群集路由Collection
除了慣例中的七個Actions外,如果你需要自定群集的Action,可以這樣設定:
~~~
resources :products do
collection do
get :sold
post :on_offer
end
# 或
get :sold, :on => :collection
post :on_offer, :on => :collection
end
~~~
如此便會有`sold_products_path`和`on_offer_products_path`這兩個URL Helper,產生出如products/sold和products/on_offer這樣的網址。
### 自定特定元素路由Member
如果需要自定對特定元素的Action:
~~~
resources :products do
member do
get :sold
end
# 或
get :sold, :on => :member
end
~~~
如此會有`sold_product_path(@product)`這個URL Helper,產生出如products/123/sold這樣的網址。
### 限定部分支援
透過`except`或`only`參數,我們不一定要啟用預設的七個Resource路由,例如
~~~
resource :events, :except => [:index, :show]
resource :events, :only => :create
~~~
### PATCH v.s. PUT
PATCH是一個相對新的HTTP verb,Rails為了保持相容性這兩個HTTP verbs都會進到update action之中。而編輯表單預設則是用PATCH。在REST語意上的差別是:
* PATCH 用於修改部分資料
* PUT 用來替換資料(replace)
對HTTP API設計有興趣的讀者,可以參考[http://ihower.tw/blog/archives/6483](http://ihower.tw/blog/archives/6483)一文。
## rake routes
如果你不清楚這些路由設定到底最後的規則是什麼,你可以執行:
~~~
rake routes
~~~
這樣就會產生出所有URL Helper、URL 網址和對應的Controller Action都列出來。
## 常見錯誤
### Routing Error
當URL找不到任何路由規則可以符合時,會出現這個錯誤。例如一個GET的路由,你用`button_to`送出POST,這樣就不符合規則。
### ActionController::UrlGenerationError
當一個路由Helper的參數不夠的時候,會出現這個錯誤。例如`event_path(event)`這個方法的event參數不能是`nil`。如果你打錯成`event_path(@events)`而`@events`是個`nil`,就會出現這個錯誤。
## 結論
透過RESTful和Named Route,我們就不再需要透過外卡路由的Hash來指定路由了。所有的路由規則都可以在routes.rb一目了然。
## 線上參考資料
* [Rails Routing from the Outside In](http://guides.rubyonrails.org/routing.html)