路由(Routing)
Weeks of programming can save you hours of planning. – Unknown
不同于静态网页的路由是直接对应于档案的目录结构,一个Web开发框架会将路由功能纳入其中,来获得最大的弹性。也就是您可以指定任意URL对应到任一个Controller的Action。另一方面,我们也不在Views中直接写死URL网址,而是透过Helper辅助方法根据你的路由设定来产生URL,这样也可以确定该网址一定有对应的Controller和Action,不然就会出现NoMethodError找不到Helper方法的错误。
也就是,路由系统做几件事情:
辨识HTTP Request的URL网址,然后对应到设定的Controller Action。
处理网址内的参数字串,例如:/users/show/123送到Users controller的show action时,会将
params[:id]
设定为 123辨识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
的部份就会产生一个meetingspath
和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如projecttasks_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
如此便会有soldproducts_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
如此会有soldproduct_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的路由,你用buttonto
送出_POST,这样就不符合规则。
ActionController::UrlGenerationError
当一个路由Helper的参数不够的时候,会出现这个错误。例如eventpath(event)
这个方法的_event参数不能是nil
。如果你打错成event_path(@events)
而@events
是个nil
,就会出现这个错误。
结论
透过RESTful和Named Route,我们就不再需要透过外卡路由的Hash来指定路由了。所有的路由规则都可以在routes.rb一目了然。