路由(Routing)

路由(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 一目了然。

线上参考资料