路由(Routing)
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("https://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
這樣的形式。
在Namespace下也可以設定它的首頁,例如:
namespace :admin do
root "projects#index"
end
就樣連http://localhost:3000/admin/
就會使用ProjectsController index action了。
特殊條件限定
我們可以利用: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路由,例如
resources :events, :except => [:index, :show]
resources :events, :only => :create
PATCH v.s. PUT
PATCH是一個相對新的HTTP verb,Rails為了保持相容性這兩個HTTP verbs都會進到update action之中。而編輯表單預設則是用PATCH。在REST語意上的差別是:
- PATCH 用於修改部分資料
- PUT 用來替換資料(replace)
對HTTP API設計有興趣的讀者,可以參考https://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一目了然。