论坛首页 编程语言技术论坛

Rails源码研究之ActionController:八,resources

浏览 5746 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2007-06-27  
深入了解一下ActionController的Resources--RESTful Rails

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而已,简化了我们的工作,提高了效率

我们在看源码时带着问题去看,就会非常有收获
   发表时间:2007-07-10  
采用resource定义产生的named routes,如果想要在后面加上额外的query string,要怎么写?

以论坛的主题贴作为嵌套资源举例子:
topic_url(@forum, @topic)

偶想在产生的url基础上,多加一个值,比如说分页:
/forum/1/topic/1?page=2

偶试了下这样的代码会报错:
link_to "page 2", topic_url(@forum, @topic, :page => 2)


看它的源代码和文档好像也没有牵涉到query string
0 请登录后投票
   发表时间:2007-07-11  
对于嵌套资源:
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  
0 请登录后投票
   发表时间:2007-07-11  
不知道你报什么错,能具体描述一下么?
0 请登录后投票
   发表时间:2007-07-11  
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种方式的不同说明...
0 请登录后投票
论坛首页 编程语言技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics