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" })

其他

  1. 默认情况下 :id 参数不接收 点 ———— 这是因为 点已经被用作了格式分割符。如果你需要在 :id 中的正则中匹配出 点,你需要这样复写正则表达式,例如::id => /[^/]+/ ,这样会允许除了斜杠之外的任何字符。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容