2007-06-27
Rails源码研究之ActionController:八,resources
关键字: ActionController resources 源码
深入了解一下ActionController的Resources--RESTful Rails
1,ActionController的resources用来实现REST api,一个单独的resource基于HTTP verb(method)有不同的行为(action),如:
2,对于map.resources :messages将生成如下named routes和helpers:
3,由于浏览器不支持PUT和DELETE,我们需要添加:method参数,如:
4,resources方法有一些参数:
:controller -- specify the controller name for the routes.
:singular -- specify the singular name used in the member routes.
:path_prefix -- set a prefix to the routes with required route variables.
对于如下routes:
我们可以使用嵌套写法:
使用的时候多加一个:article_id参数即可:
:name_prefix -- define a prefix for all generated routes, usually ending in an underscore.
使用前缀来避免named routes名字冲突:
:collection -- add named routes for other actions that operate on the collection.
比如对于如下route:
生成的named route为rss_messages,生成的helper方法为rss_messages_path,url则为/messages;rss
该参数形式为#{action} => #{method}的hash,method为:get/:post/:put/:delete/:any,如果method无所谓则可以使用:any
:member -- same as :collection, but for actions that operate on a specific member.
即:collection参数为对多个对象操作的方法,:member参数则为对单个对象操作的方法
:new -- same as :collection, but for actions that operate on the new resource action.
5,map.resource的参数以及用法与map.resources差不多,只是map.resource为一个单独的resource生成named routes
源码全在resources.rb文件里,Rails经常这样弄的一个文件几百行甚至上千行代码,可读性很不好,不过倒也不错,不用到处找关联的文件
Resources模块定义了Resource类和SingletonResource类,前者表示多个资源,后者则表示单个资源
resources方法和resource方法分别调用map_resource方法和map_singleton_resource方法
map_resource和map_singleton_resource方法分别用Resource和SingletonResource类初始化对象
最后分别调用map.named_route和map.connect来生成各自的routes
注意resources和resource方法对生成的routes作了HTTP method上的限制,如果我们在request时提供的method参数有误则会抛出RoutingError异常
这样看来,resources只不过是为我们生成了一些named routes而已,简化了我们的工作,提高了效率
我们在看源码时带着问题去看,就会非常有收获
1,ActionController的resources用来实现REST api,一个单独的resource基于HTTP verb(method)有不同的行为(action),如:
map.resources :messages
class MessagesController < ActionController::Base
# GET messages_url
def index
# return all messages
end
# GET new_message_url
def new
# return an HTML form for describing a new message
end
# POST message_url
def create
# create a new message
end
# GET message_url(:id => 1)
def show
# find and return a specific message
end
# GET edit_message_url(:id => 1)
def edit
# return an HTML form for editing a specific message
end
# PUT message_url(:id => 1)
def update
# find and update a specific message
end
# DELETE message_url(:id => 1)
def destroy
# delete a specific message
end
end
2,对于map.resources :messages将生成如下named routes和helpers:
Named Routes Helpers
messages messages_url, hash_for_messages_url,
messages_path, hash_for_messages_path
message message_url(id), hash_for_message_url(id),
message_path(id), hash_for_message_path(id)
new_message new_message_url, hash_for_new_message_url,
new_message_path, hash_for_new_message_path
edit_message edit_message_url(id), hash_for_edit_message_url(id),
edit_message_path(id), hash_for_edit_message_path(id)
3,由于浏览器不支持PUT和DELETE,我们需要添加:method参数,如:
<% form_for :message, @message, :url => message_path(@message), :html => {:method => :put} do |f| %>
4,resources方法有一些参数:
:controller -- specify the controller name for the routes.
:singular -- specify the singular name used in the member routes.
:path_prefix -- set a prefix to the routes with required route variables.
对于如下routes:
map.resources :articles map.resources :comments, :path_prefix => '/articles/:article_id'
我们可以使用嵌套写法:
map.resources :articles do |article| article.resources :comments end
使用的时候多加一个:article_id参数即可:
comment_url(@article, @comment) comment_url(:article_id => @article, :id => @comment)
:name_prefix -- define a prefix for all generated routes, usually ending in an underscore.
使用前缀来避免named routes名字冲突:
map.resources :tags, :path_prefix => '/books/:book_id', :name_prefix => 'book_' map.resources :tags, :path_prefix => '/toys/:toy_id', :name_prefix => 'toy_'
:collection -- add named routes for other actions that operate on the collection.
比如对于如下route:
map.resources :messages, :collection => { :rss => :get }
生成的named route为rss_messages,生成的helper方法为rss_messages_path,url则为/messages;rss
该参数形式为#{action} => #{method}的hash,method为:get/:post/:put/:delete/:any,如果method无所谓则可以使用:any
:member -- same as :collection, but for actions that operate on a specific member.
即:collection参数为对多个对象操作的方法,:member参数则为对单个对象操作的方法
:new -- same as :collection, but for actions that operate on the new resource action.
5,map.resource的参数以及用法与map.resources差不多,只是map.resource为一个单独的resource生成named routes
源码全在resources.rb文件里,Rails经常这样弄的一个文件几百行甚至上千行代码,可读性很不好,不过倒也不错,不用到处找关联的文件
module ActionController
module Resources
class Resource
attr_reader :collection_methods, :member_methods, :new_methods
attr_reader :path_prefix, :name_prefix
attr_reader :plural, :singular
attr_reader :options
def initialize(entities, options)
@plural = entities
@singular = options[:singular] || plural.to_s.singularize
@options = options
arrange_actions
add_default_actions
set_prefixes
end
def controller
@controller ||= (options[:controller] || plural).to_s
end
def path
@path ||= "#{path_prefix}/#{plural}"
end
def new_path
@new_path ||= "#{path}/new"
end
def member_path
@member_path ||= "#{path}/:id"
end
def nesting_path_prefix
@nesting_path_prefix ||= "#{path}/:#{singular}_id"
end
end
class SingletonResource < Resource
alias_method :member_path, :path
alias_method :nesting_path_prefix, :path
end
def resources(*entities, &block)
options = entities.last.is_a?(Hash) ? entities.pop : { }
entities.each { |entity| map_resource entity, options.dup, &block }
end
def resource(*entities, &block)
options = entities.last.is_a?(Hash) ? entities.pop : { }
entities.each { |entity| map_singleton_resource entity, options.dup, &block }
end
private
def map_resource(entities, options = {}, &block)
resource = Resource.new(entities, options)
with_options :controller => resource.controller do |map|
map_collection_actions(map, resource)
map_default_collection_actions(map, resource)
map_new_actions(map, resource)
map_member_actions(map, resource)
if block_given?
with_options(:path_prefix => resource.nesting_path_prefix, &block)
end
end
end
def map_singleton_resource(entities, options = {}, &block)
resource = SingletonResource.new(entities, options)
with_options :controller => resource.controller do |map|
map_collection_actions(map, resource)
map_default_singleton_actions(map, resource)
map_new_actions(map, resource)
map_member_actions(map, resource)
if block_given?
with_options(:path_prefix => resource.nesting_path_prefix, &block)
end
end
end
def map_collection_actions(map, resource)
resource.collection_methods.each do |method, actions|
actions.each do |action|
action_options = action_options_for(action, resource, method)
map.named_route("#{resource.name_prefix}#{action}_#{resource.plural}", "#{resource.path};#{action}", action_options)
map.named_route("formatted_#{resource.name_prefix}#{action}_#{resource.plural}", "#{resource.path}.:format;#{action}", action_options)
end
end
end
def map_default_collection_actions(map, resource)
index_action_options = action_options_for("index", resource)
map.named_route("#{resource.name_prefix}#{resource.plural}", resource.path, index_action_options)
map.named_route("formatted_#{resource.name_prefix}#{resource.plural}", "#{resource.path}.:format", index_action_options)
create_action_options = action_options_for("create", resource)
map.connect(resource.path, create_action_options)
map.connect("#{resource.path}.:format", create_action_options)
end
def map_default_singleton_actions(map, resource)
create_action_options = action_options_for("create", resource)
map.connect(resource.path, create_action_options)
map.connect("#{resource.path}.:format", create_action_options)
end
def map_new_actions(map, resource)
resource.new_methods.each do |method, actions|
actions.each do |action|
action_options = action_options_for(action, resource, method)
if action == :new
map.named_route("#{resource.name_prefix}new_#{resource.singular}", resource.new_path, action_options)
map.named_route("formatted_#{resource.name_prefix}new_#{resource.singular}", "#{resource.new_path}.:format", action_options)
else
map.named_route("#{resource.name_prefix}#{action}_new_#{resource.singular}", "#{resource.new_path};#{action}", action_options)
map.named_route("formatted_#{resource.name_prefix}#{action}_new_#{resource.singular}", "#{resource.new_path}.:format;#{action}", action_options)
end
end
end
end
def map_member_actions(map, resource)
resource.member_methods.each do |method, actions|
actions.each do |action|
action_options = action_options_for(action, resource, method)
map.named_route("#{resource.name_prefix}#{action}_#{resource.singular}", "#{resource.member_path};#{action}", action_options)
map.named_route("formatted_#{resource.name_prefix}#{action}_#{resource.singular}", "#{resource.member_path}.:format;#{action}",action_options)
end
end
show_action_options = action_options_for("show", resource)
map.named_route("#{resource.name_prefix}#{resource.singular}", resource.member_path, show_action_options)
map.named_route("formatted_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}.:format", show_action_options)
update_action_options = action_options_for("update", resource)
map.connect(resource.member_path, update_action_options)
map.connect("#{resource.member_path}.:format", update_action_options)
destroy_action_options = action_options_for("destroy", resource)
map.connect(resource.member_path, destroy_action_options)
map.connect("#{resource.member_path}.:format", destroy_action_options)
end
def conditions_for(method)
{ :conditions => method == :any ? {} : { :method => method } }
end
def action_options_for(action, resource, method = nil)
default_options = { :action => action.to_s }
require_id = resource.kind_of?(SingletonResource) ? {} : { :requirements => { :id => Regexp.new("[^#{Routing::SEPARATORS.join}]+") } }
case default_options[:action]
when "index", "new" : default_options.merge(conditions_for(method || :get))
when "create" : default_options.merge(conditions_for(method || :post))
when "show", "edit" : default_options.merge(conditions_for(method || :get)).merge(require_id)
when "update" : default_options.merge(conditions_for(method || :put)).merge(require_id)
when "destroy" : default_options.merge(conditions_for(method || :delete)).merge(require_id)
else default_options.merge(conditions_for(method))
end
end
end
end
ActionController::Routing::RouteSet::Mapper.send :include, ActionController::Resources
Resources模块定义了Resource类和SingletonResource类,前者表示多个资源,后者则表示单个资源
resources方法和resource方法分别调用map_resource方法和map_singleton_resource方法
map_resource和map_singleton_resource方法分别用Resource和SingletonResource类初始化对象
最后分别调用map.named_route和map.connect来生成各自的routes
注意resources和resource方法对生成的routes作了HTTP method上的限制,如果我们在request时提供的method参数有误则会抛出RoutingError异常
这样看来,resources只不过是为我们生成了一些named routes而已,简化了我们的工作,提高了效率
我们在看源码时带着问题去看,就会非常有收获
- 01:58
- 浏览 (2437)
- 论坛浏览 (2544)
- 评论 (4)
- 分类: Ruby
- 相关推荐
评论
hideto 写道
不知道你报什么错,能具体描述一下么?
不好意思,偶昨天写得匆忙,没有把错误写上来,测试代码如下:
require File.dirname(__FILE__) + '/../test_helper'
require 'forums_controller'
class ForumsController; def rescue_action(e) raise e end; end
class ForumsControllerTest < Test::Unit::TestCase
fixtures :forums
def setup
@controller = ForumsController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
def test_named_route_path
get :index
path = forum_path(forums(:forum_1))
assert_equal "/forums/1", path
end
def test_named_route_path_with_query_string
get :index
path = forum_path(forums(:forum_1), :page => 1)
assert_equal "/forums/1?page=1", path
end
end
routes.rb和你前面贴的一样:
map.resources :forums do |forum|
forum.resources :topics do |topic|
topic.resources :posts
end
end
第一个测试不带query string的没有问题,第二个测试报错是:
引用
You have a nil object when you didn't expect it!
The error occurred while evaluating nil.to_sym
不过偶后来发现这个问题和resource无关,是偶不知道named route产生的_path/_url方法如何接受query string,很好奇你的代码怎么可以运行通过
===编辑分隔线===
发现你的代码不同点了,你是用_path(:id => xxx, :page => xxx)这种方式,测试代码改成:
path = forum_path(:id => forums(:forum_1), :page => 1) 就可以通过了,偶再去看看文档或者源代码哪里有涉及到这2种方式的不同说明...
对于嵌套资源:
在_url/_path方法中直接加query参数即可,比如:page参数会作为link_to方法的options参数,比如:
或
关键是controller里处理:page参数:
:page参数会被paginate方法自动捕获:
map.resources :forums do |forum|
forum.resources :topics do |topic|
topic.resources :posts
end
end
在_url/_path方法中直接加query参数即可,比如:page参数会作为link_to方法的options参数,比如:
<% link_to "page 2", topic_url(@forum, @topic, :page => 2) %>
或
<% for topic in @topics %> <%= link_to "First Page", topic_path(:forum_id => @forum, :id => topic, :page = topic.first_page) %> <%= pagination_links topic.post_pages, :window_size => 10, :link_to_current_page => true %> <%= link_to "Last Page", topic_path(:forum_id => @forum, :id => topic, :page = topic.last_page) %> <% end %>
关键是controller里处理:page参数:
post_pages, posts = paginate(:posts, :per_page => 5, :order => 'posts.created_at', :conditions => ['posts.topic_id = ?', topic.id]) topic.post_pages = post_pages
:page参数会被paginate方法自动捕获:
def paginator_and_collection_for(collection_id, options) klass = options[:class_name].constantize page = params[options[:parameter]] count = count_collection_for_pagination(klass, options) paginator = Paginator.new(self, count, options[:per_page], page) collection = find_collection_for_pagination(klass, options, paginator) return paginator, collection end
发表评论
该博客是同时发布到论坛的,无法评论在论坛已被锁定的帖子
- 浏览: 723128 次
- 性别:

- 来自: BJ

- 详细资料
搜索本博客
我的相册
screenshot
共 1 张
共 1 张
最近加入圈子
最新评论
-
Why OO sucks
看成去QQ SUCK 了
-- by xhanxhanxhan -
Rails的富文本编辑器插件 ...
有没有那个像textmate那种语法高亮的富文本编辑器?
-- by zllicho -
翻译www.djangobook.com之 ...
weiertzw 写道 1. >>> from django. ...
-- by chenjihua75 -
PHP、CakePHP哪凉快哪呆 ...
这孩子被java毒害太深。。。跳出java,你会发现外面的世界真的很大。
-- by woodless -
学习svn命令
只会用 apt-get 不是好孩子。
-- by smartly






评论排行榜