Rails 路由学习笔记
参考 RailsGuides中的Rails Routing from the Outside In
简介
Rails 路由会通过你配置的路由规则将发送来的 URL 分发到对应的 action 中。它同时会生成 paths 和 urls 来避免你在视图中使用硬编码。
Rails 的路由器是通过 HTTP 动词和 url 地址来匹配路由的。Rails 支持四种 HTTP 动词,包括get,put,post,delete。其中 post 和 delete 浏览器本身不支持,是通过 js 来实现的。Rails 路由器生成一个 params 的散列表其中的 :controller 和 :action 就决定了相应的控制器和相应的动作来处理该请求,其余的通常作为参数传递给该动作,该动作使用。例如:
match ':controller/:action/:id/with_user/:user_id'
生成的 params 是:
{ :controller => “photos”, :action => “show”, :id => “1”, :user_id => “2” }
Rails 匹配地址时是在你指定的所有路由地址中从上倒下匹配的。如果存在如下代码:
resources :pohots
get 'photos/poll'
当传来 'photos/poll' 时会先匹配到 resources 路由的 show 动作上,而不会匹配到 get 那个地址。
其次,同时意味着你可以把 root 地址放在 rout 文件的开头,因为这是很可能匹配最多的地址。
资源路由
基本使用
Rails 资源路由有两种,resources 和 resource。其中后者针对单件资源的路由,前者更加常见。
其中使用方法如下:
resources :photos
resource :photo
分别生成七个路由地址和六个路由地址。(很简单,不解释)
对动作进行限制
默认地, Rails 会在应用中为每一个 RESTful 路由建立7个动作。不过你可以用 :only 或 :except 选项来针对你的路由进行调整:
resources :photos, :only => [:index, :show]
resources :photos, :except => :destroy
如果你的应用程序有很多 RESTful 路由,使用 :only 和 :except 可以只生成有用的路径,这能够减少匹配所需时间和内存。
添加额外的动作
成员路由
加入一个成员路由只需要在你的 resource 代码块中加入你一个 member 代码块就好:
resources :photos do
member do
get 'preview'
end
end
这样将会把 /photos/1/preview 的 GET 动作识别出来,然后路由会匹配到 PhotosController 控制器下面的 preview 行为中。它同样会建立两个Helper:preview_photo_url 和 preview_photo_path。
当你使用了路由中的 Member 代码块时,你可以任意指定一个HTTP动词: get, patch/put, post, 或者 delete 。如果你并没有太多的 Member 路由规则,可以用 :on 作为后缀来把代码块替换掉:
resources :photos do
get 'preview', :on => :member
end
集合路由
加入集合路由和加入成员路由的格式完全一样,只不过是用 collection 的方法:
resources :photos do
collection do
get 'search'
end
end
上面这段代码将会让 Rails 用 GET 方法匹配路径 /photos/search。并且这个 search 行为将会匹配到 PhotosController下,它将会创建 search_photos_url 和 search_photos_path 两个路由Helper。
同成员路由一样,你可以传入一个 :on 选项。
resources :photos do
get 'search', :on => :collection
end
指定控制器
:controller 选项能够让你制定使用的控制器。例如:
resources :photos, :controller => "images"
它没有改变匹配的 URL 和生成的 helper,只是将控制器指定为 images。
指定格式限制
:constraints 选项可以让你限定 id 的格式。例如:
resources :photos, :constraints => {:id => /[A-Z][A-Z][0-9]+/}
你可以用一个代码块来把单个正则限制运用到多条规则上去。
constraints(:id => /[A-Z][A-Z][0-9]+/) do
resources :photos
resources :accounts
end
改变 Helper
:as 选项能让你把默认命名的路由 Helper 重命名成定制的字符串。例如:
resources :photos, :as => "images"
这样就会生成像 new_image_path 这样的 Helper。
改变匹配路径动的动作
:path_name 选项能够让你把生成路径中的 "new" 和 "edit" 覆盖:
resources :photos, :path_names => { :new => 'make', :edit => 'change' }
这将会让 Rails 能够匹配这些请求:
/photos/make
/photos/1/change
这个选项并不会改变控制器中的行为名称,这两个路径依然会被匹配到控制器中的 new 和 edit 路径中去。
如果你想一次性地把一条设置应用到多个规则上,你可以用 scope 。
scope :path_names => { :new => "make" } do
resources :photos
resources :accounts
end
改变匹配的路径的资源名
:path 选项能够让你把生成路径中的资源名称覆盖:
resources :categories, :path => "kategorien"
resources :posts, :path => "/admin/posts"
这样路由就会匹配这样的路径:/kategorien/:id/edit,而不再是:/categories/:id/edit,但是并不会改变 Helper 。
有时,你会把同类的控制器都放在同一个目录下,例如: app/controllers/admin 。这时,你可以这样组织路由:
scope :path => "/admin" do
resources :posts, :comments
end
通常写为:
scope "/admin" do
resources :posts, :comments
end
同样,你还可以用下面的控制器模块和命名空间这两种方法。
HTTP Verb Path action name helper
GET admin/photos posts#index photos_path
GET admin/photos/new posts#new new_photos_path
POST admin/photos posts#create photos_path
GET admin/photos/:id posts#show photo_path(:id)
GET admin/photos/:id/edit posts#edit edit_photo_path(:id)
PUT admin/photos/:id posts#update photo_path(:id)
DELETE admin/photos/:id posts#destroy photo_path(:id)
控制器模块
:module 可以让你的控制器匹配到一个特定的模块下(可能包含有多个控制器)。
resources :posts, :module => "admin"
这样把路由 /posts (不以 /admin 作前缀) 匹配到 Admin::PostsController。
当然也可以组织多个路由:
scope :module => "admin" do
resources :posts, :comments
end
其中 posts 的路由地址如下:
HTTP Verb Path action name helper
GET /photos admin/posts#index photos_path
GET /photos/new admin/posts#new new_photos_path
POST /photos admin/posts#create photos_path
GET /photos/:id admin/posts#show photo_path(:id)
GET /photos/:id/edit admin/posts#edit edit_photo_path(:id)
PUT /photos/:id admin/posts#update photo_path(:id)
DELETE /photos/:id admin/posts#destroy photo_path(:id)
控制器命名空间
namespace 将会自动为你添加 :as , :module 和 :path 前缀。特别的用来处理下面提到的这种情况:
控制器命名空间可以帮助你通过一个命名空间管理多个控制器。例如,有多个 controllers 在同一个命名空间 Admin::namespace 下。 你或许是将这些 controllers 文件都放在了 app/controllers/admin 的目录之下。这时,你可以这样组织你的路由:
namespace :admin do
resources :photos, :comments
end
其中 photos 的路由地址如下:
HTTP Verb Path action name helper
GET /admin/photos admin/posts#index admin_photos_path
GET /admin/photos/new admin/posts#new new_admin_photos_path
POST /admin/photos admin/posts#create admin_photos_path
GET /admin/photos/:id admin/posts#show admin_photo_path(:id)
GET /admin/photos/:id/edit admin/posts#edit edit_admin_photo_path(:id)
PUT /admin/photos/:id admin/posts#update admin_photo_path(:id)
DELETE /admin/photos/:id admin/posts#destroy admin_photo_path(:id)
嵌套路由
通常,一个 Resource 常常有数个逻辑上的子 Resource 。例如,你的应用有这样一个模型:
class Magazine < ActiveRecord::Base
has_many :ads
end
class Ad < ActiveRecord::Base
belongs_to :magazine
end
嵌套路由允许你这样嵌套路由:
resources :magazines do
resources :ads
end
在这里,对于每一个 magazines 的路径下面都需要能够有 ad 作为 Resource 匹配到AdsController。 这时候每一组 ad 都需要指定一个 magzine 作为前缀。
HTTP Verb Path action name helper
GET /magazines/:magazine_id/ads index magazine_ads_path
GET /magazines/:magazine_id/ads/new new new_magazine_ads_path
POST /magazines/:magazine_id/ads create magazine_ads_path
GET /magazines/:magazine_id/ads/:id show magazine_ad_path(:id)
GET /magazines/:magazine_id/ads/:id/edit edit edit_magazine_ad_path(:id)
PUT /magazines/:magazine_id/ads/:id update magazine_ad_path(:id)
DELETE /magazines/:magazine_id/ads/:id destory magazine_ad_path(:id)
嵌套路由允许你进行多层的嵌套,但实际上你的嵌套永远不要超过一层。
对象和路径URL
在 Rails 中你可以把对象的实例传入作参数来作为ID。例如:
<%= link_to "Ad details", magazine_ad_path(@magazine, @ad) %>
你也可以用 url_for 带上一组对象, Rails 会自动生成正确的地址。
<%= link_to "Ad details", url_for([@magazine, @ad]) %>
在这里,Rails 将会把 @magazine 对应到 Magazine, @ad 对应到 Ad 上去,然后决定使用 magazine_ad_path 这个 Helper。而如果你用的是 link_to 这样的Helper,你可以和调用 url_for 一样用对象作参数:
<%= link_to "Ad details", [@magazine, @ad] %>
如果你并没有用嵌套式的路由,只要这样就可以访问一个对象的 show 方法:
<%= link_to "Magazine details", @magazine %>
如果你需要指定动作的对象的话,你也可以这样:
<%= link_to "Edit Ad", [:edit, @magazine, @ad] %>
非资源路由
虽然 Resourceful 的路由规则功能强大,但是在很多时候使用一个简单路由规则更为合适。如果你觉得简单路由规则更合适,你并没有必要把应用中的每个规则都往 Resourceful 框架上套。
默认路由
match 为 Rails 的默认路由,你需要提供一组符号字面量来让 Rails 匹配收到的 HTTP 请求,其中有两个符号量尤其的特别::controller 位置会匹配到你应用中名字对应的控制器,:action 有匹配到你应用中对应的控制器中的行为。例如:
match ':controller(/:action(/:id))'
如果收到的请求是 /photos/show/1 (前提是它还没有被其他的前面的路由规则匹配),那么按照这个规则 Rails 将会调用 PhotosController 控制器中的 show 行为,然后把后面的参数 "1" 放到 params[:id] 中以供使用。因为 :action 和 :id 被圆括号包围,所以,他俩可以被忽略,所以这条规则同样能够将 /photos 匹配到 PhotosController#index 。
动态部分
你可以设置一条路由规则有任意多的动态部分,除了:controller 或 :action ,其他的片段都将会被转换成 params 的一部分。如果你写了这样的一条路由:
match ':controller/:action/:id/:user_id'
如果收到了 /photos/show/1/2 这样的请求,将会分发到 PhotosController 下的 show 行为中,而 params[:id] 会是 "1", params[:user_id] 是 "2".
静态部分
你可以在创建路由规则的时候指定一个静态的部分:
match ':controller/:action/:id/with_user/:user_id'
这个路由将会生成像这样的路径 /photos/show/1/with_user/2。在这个例子里, params 是 { :controller => “photos”, :action => “show”, :id => “1”, :user_id => “2” }.
携带附加参数
你也可以在一个 urls 中携带任何的参数,例如:
match ':controller/:action/:id'
如果接收到的路径为 /photos/show/1?user_id=2 ,那么 Rails 会把它分发到 Photos 控制器下面的 show 行为中去,params 会是 { :controller => “photos”, :action => “show”, :id => “1”, :user_id => “2” }.
设置默认值
如果你需要一个默认的匹配值,你可以用一个没有指定 :controller 和 :action 的路由规则来实现。像这样:
match 'photos/:id' => 'photos#show'
Rails 将会因为这条规则而把接收到的路径 /photos/12 ,匹配到 PhotosController 下面的 show 里面去。
你还可以通过提供一个 :defaults 的散列来指定一个你默认存在的动态片段。例如:
match 'photos/:id' => 'photos#show', :defaults => { :format => 'jpg' }
这样 photos/12 的路径就会匹配到 PhotosController 的 show 行为, params[:format] 会被设置为 "jpg"。
命名路由
你可以用 :as 参数来定义一个路由的名字。
match 'exit' => 'sessions#destroy', :as => :logout
这样在应用程序中产生的 Helper 就将会是 create logout_path 和 logout_url。而调用了 logout_path 就会返回 /exit 路径。
HTTP 动词约定
你可以用 :via 选项来限定一个请求能相应的一个或者多个 HTTP 方法:
match 'photos/show' => 'photos#show', :via => :get
简写作:
get 'photos/show'
你也可以将多个动词行为绑定到一条路由规则上去:
match 'photos/show' => 'photos#show', :via => [:get, :post]
部分正则约定
你可以使用 :constraints 选项来对动态路径部分强制性的匹配:
match 'photos/:id' => 'photos#show', :constraints => { :id => /[A-Z]\d{5}/ }
这个路由将会匹配像 /photos/A12345 这样的路由。你可以用如此更简洁的方式来写出这个规则:
match 'photos/:id' => 'photos#show', :id => /[A-Z]\d{5}/
在 :constraints 选项中使用正则表达式有一个限制,就是在正则匹配中无法使用锚元素来匹配,像这样的路由规则是无效的:
match '/:id' => 'posts#show', :constraints => {:id => /^\d/}
总之,确定你没有在路由正则中使用锚元素。因为每个路由规则在匹配的时候就已经用到了锚元素。
例如,下面的路由 posts 和 users 控制器共享了一个命名空间,他们根据 to_param 值的首字符是否是数字来区分,像 1-hello-world 这样的会匹配到 posts 下面去,而像 david 就会匹配到 users 下面去。
match '/:id' => 'posts#show', :constraints => { :id => /\d.+/ }
match '/:username' => 'users#show'
请求限制 && 高级限定
这里不是狠懂,设计到了 Request 对象,暂时留下以后解决。
通配路由匹配
通配路由规则能够指定一个参数让任意部分匹配。例如:
match 'photos/*other' => 'photos#unknown'
这样这个路由将会匹配 photos/12 或 /photos/long/path/to/12,并且把 params[:other] 设置成 "12" 或 "long/path/to/12".
通配字符串可以放在路由规则的任意部分,例如:
match 'books/*section/:title' => 'books#show'
这样将会把像 books/some/section/last-words-a-memoir 这样的字符串匹配后并将 params[:section] 设置为 "some/section", params[:title] 设置为 "last-words-a-memoir"。
从技术上来说你是可以将超过一个统配的字符串加在你的路由规则中的,但是请记住,通配符总是会用贪婪地(尽可能多地匹配)将字符串匹配到路径上。例如:
match '*a/foo/*b' => 'test#index'
将会把 zoo/woo/foo/bar/baz 的参数 params[:a] 设置成 "zoo/woo", 然后参数 params[:b] 设置成 "bar/baz"。
从 Rails 3.1 开始,通配路由总是会自动地将格式字符串默认匹配,例如下面这个路由:
match '*pages' => 'pages#show'
如果你发起了'/foo/bar.json'这样的一个请求,你的参数 params[:pages] 将会是 "foo/bar" ,而你的请求格式会识别为 JSON 。 如果你想要在旧的 3.0.x 中有同样的特性,你需要提供一个散列参数:format => false,像这样:
match '*pages' => 'pages#show', :format => false
如果你想要强制地被指定一种格式,(其无法被忽略),你可以像这样加上 :format => true 参数:
match '*pages' => 'pages#show', :format => true
重定向
你可以在路由规则中通过 redirect 指定一个路径重定向到另一个路由中的路径:
match "/stories" => redirect("/posts")
你同样可以把一条 match 命令中的动态部分的错误处理(rescue)进行重定向:
match "/stories/:name" => redirect("/posts/%{name}")
你同样可以为你的重定向函数加入一个代码块,代码块接收的变量是 params 和 请求对象(后者是可选的):
match "/stories/:name" => redirect {|params| "/posts/#{params[:name].pluralize}" }
match "/stories" => redirect {|p, req| "/posts/#{req.subdomain}" }
请注意,这里的重定向都将是 301 标志 “永久重定向”。在某些浏览器或者代理服务器中这个标识码将会使旧的页面失效。
路由定向到 Rack Applications
你可以将 urls 的地址交给任何一个 Rack application 来处理。只需要在 match 后面指定相应的 Rack application 来取代一个类似与 "posts#index" 的字符串。
match "/application.js" => Sprockets
这样 Sprockets 将会处理这个请求并且返回 [status, headers, body], 但是这对于路由器来说是完全透明的,它不能分辨两者。
值得一提的是,"post#index" 通常会自动的扩展为 PostsController.action(:index),而它会返回一个可以有效的 Rack application。
跟目录
你可以用 root 方法来指定 Rails 如何匹配 "/" 路由:
root :to => 'pages#main'
简写为:
root 'pages#main'
你可以将 root 规则放在文件的最上方,这样这条规则能第一个进行匹配,因为这恐怕将会是最常用的的规则了。你同样需要删除文件 public/index.html 来让这个规则发挥效用。
对路由进行检查和测试
Rails 提供了检查和测试路由的工具。
通过 rake 查看存在的路由规则
如果你想要一整套应用的完整可用的路由规则列表,运行 rake routes 命令就好。
路由测试
你的测试中应该加入路由测试(就和你应用程序的其他部分一样)。Rails 提供三个 内建的断言 被设计来帮助你进行路由的测试。
- assert_generates
- assert_recognizes
- assert_routing
assert_generates 能够对是否生成指定的路径进行断言:
assert_generates "/photos/1", { :controller => "photos", :action => "show", :id => "1" }
assert_generates "/about", :controller => "pages", :action => "about"
你可以提供一个 :method 参数来指定使用的 HTTP 行为:
assert_recognizes 是 assert_generates 的反向断言. 它能够通过指定的位置判断是否与存在的路由规则匹配。
assert_recognizes({ :controller => "photos", :action => "show", :id => "1" }, "/photos/1")
你可以提供一个 :method 参数来指定使用的 HTTP 行为:
assert_recognizes({ :controller => "photos", :action => "create" }, { :path => "photos", :method => :post })
assert_routing 对一个路由双向地进行测试: 它测试的一条路径是否与选项匹配,之后测试选项是否生成路径。因此,这条断言结合了前两个测试方法 assert_generates 和 assert_recognizes.
assert_routing({ :path => "photos", :method => :post }, { :controller => "photos", :action => "create" })
其他
- 默认情况下 :id 参数不接收 点 ———— 这是因为 点已经被用作了格式分割符。如果你需要在 :id 中的正则中匹配出 点,你需要这样复写正则表达式,例如::id => /[^/]+/ ,这样会允许除了斜杠之外的任何字符。