Oh!Coder

Coding Life

Inherited Resources Gem简介

| Comments

今天介绍的gem名字叫Inherited Resources。这次之所以介绍这个gem,是因为最近的工作中使用到了。使用的时候只是临时性的完成了工作任务,但对gem本身的了解并不多,现在抱着全面了解的心态重新做一次简单的学习。

在文档的最开始,作者写了一段简要的弃用通知,大意是说自从Rails 3推出以后,他就不再使用此gem,并且也不再积极的维护,替代方案是使用一个名为Responders的gem,另外结合Rails的respond_with特性。不过,虽然原作者作出了这样的声明,但从代码的提交时间上来看,还是有很多热心的开源爱好者不断的进行维护,所以就当前来看,还是不用过于担心。

基本简介

Inherited Resources加速了开发速度,让controller继承所有restul的action,作为开发者只需要专注于某个重要的action即可。同时这让controller变的更强大和简洁。

此外,这可以让controller遵守一定的规则,遵照胖model和瘦controller的约定写出更好的代码。文档中给出了两个screencast:

安装

针对Rails 3版本,在Gemfile中添加如下一行:

1
gem 'inherited_resources'

然后执行bundler命令:

1
bundle install

HasScope

自Inherited Resources 1.0以后,has_scope就成为了一个独立的gem,而不再是核心的一部分。如果你想使用此功能,可以查看相应的文档:

安装此gem执行如下命令:

1
gem install has_scope

Responders

自Inherited Resources 1.0以后,responders就不再是核心的一部分,但是Inherited Resources依然对其进行依赖并且被InheritedResources默认使用。如果想知道这部分如何改变你的应用,最好去读一下这部分的文档:

安装此gem执行如下命令:

1
gem install resonders

使用responders可以更改:notice:alert的flash消息。可以通过如下配置进行更改:

1
InheritedResources.flash_keys = [ :success, :failure ]

注意,使用InheritedResources的CollectionResponder不会有效,因为InheritedResources进行hardcode的路径是基于当前scope的(类似于belongs to,polymorphic association等)。

基本使用

使用Inherited Resources只需要继承即可:

1
2
class ProjectsController < InheritedResources::Base
end

此时所有默认action都被定义好了,核实一下吧!projects的collection实例变量(在名为index的action下)@projects已经被创建好了,project的resource(其他所有action下)的实例变量@project也被创建好了。

下一步要定义的是controller可以respond哪些mime类型:

1
2
3
class ProjectsController < InheritedResources::Base
  respond_to :html, :xml, :json
end

除了上面这种定义方式以外,还可以在每个action里单独进行定义:

1
2
3
4
5
class ProjectsController < InheritedResources::Base
  respond_to :html, :xml, :json
  respond_to :js, :only => :create
  respond_to :iphone, :except => [ :edit, :update ]
end

对于每一个request,首先会检查”controller/action.format”文件是否可用(比如”projects/create.xml”),如果不可用,会检查resource的respond_to :to_format(在此例子中为:to_xml)。如果都没有则返回404.

另外一个可选项是指定哪一个action将会继承自InheritedResources::Base:

1
2
3
class ProjectsController < InheritedResources::Base
  actions :index, :show, :new, :create
end

或者:

1
2
3
class ProjectsController < InheritedResources::Base
  actions :all, :except => [ :edit, :update, :destroy ]
end

在view层,会得到如下辅助方法:

1
2
3
resource        #=> @project
collection      #=> @projects
resource_class  #=> Project

正如你所希望的,collection(@projects实例变量)只在index的action下可用。

如果由于某些原因你不能继承自InheritedRresources::Base,你可以在class的范围内调用inherit_resources:

1
2
3
class AccountsController < Applicationcontroller
  inherit_resources
end

一个使用inherit_resources宏的原因是,确保你的controller永远不会respond类型为html的mime。InheritedResources::Base已经可以respond的类型有:html,而respond_to宏严格指定respond的类型。因此,如果你想要创建一个controller,比如只respond一种:js类型,需要如下书写代码:

1
2
3
4
class AccountsController < ApplicationController
  respond_to :js
  inherit_resources
end

覆盖默认

无论什么时候继承自InheritedResources,总是会有一些默认的设定。比如说你有一个controller名为AccountsController来对account进行管理,而resource名为User:

1
2
3
class AccountsController < InheritedResources::Base
  defaults :resource_class => User, :collection_name => 'users', :instance_name => 'user'
end

针对于上面的例子,在view层你会得到两个变量,@users@user,但是route依然是accounts_urlaccount_url。如果你同时打算改变route,可以使用:route_collection_name:route_instance_name

controller的命名空间不在上述设定范围内,但是如果你需要设定一个不同的路由前缀,需要像下面这样进行修改:

1
2
3
class Administrators::PeopleController < InheritedResources::Base
  defaults :route_prefix => 'admin'
end

命名之后的route将会是:admin_people_urladmin_people_url将会替换掉adminnistrators_people_urladministrators_person_url

如果想自定义取回resource的方法,可以覆盖collection和resource方法。前一个是在名为index的action中调用,后一个是其他的action中调用。让我们假设你想在projects的collection中添加pagination,实现方式如下:

1
2
3
4
5
6
class ProjectsController < InheritedResources::Base
  protected
    def collection
      get_collection_ivar || set_collection_ivar(end_of_association_chain.paginate(:page => params[:page]))
    end
end

其中end_of_association_chain返回嵌套所有association和scope之后的resource(详情可参加下文)。

InheritedResources还介绍了另一个称为begin_of_association_chain的方法。当你想要创建基于@current_user的resource时,可能会用到,并且会生成类似于”account/projects”的url。这种情况下,需要在action中调用@current_user.projects.find@current_user.projects.build方法。

可以进行如下操作:

1
2
3
4
5
6
class ProjectController < InheritedResources::Base
  protected
    def begin_of_association_chain
      @current_user
    end
end

覆盖action

假设删除一个project之后,你想跳转到root链接而不是到projects的url。需要做如下操作:

1
2
3
4
5
6
7
class ProjectsController < InheritedResources::Base
  def destroy
    super do |format|
      format.html{ redirect_to root_url }
    end
  end
end

重新打开action,并定义父级的action一个新的行为。另外,我不得不赞同的一件事是调用super真的是没有什么可读性。这也是为什么所有的方法都会有匿名。这相当于:

1
2
3
4
5
6
7
class ProejctsController < InheritedResources::Base
  def destroy
    destroy! do |format|
      format.html { redirect_to root_url }
    end
  end
end

既然大部分情况下修改create,update或者destroy的action都是因为想改变url的重定向,那么这里提供了一个简洁的修改方法。你可以这样做:

1
2
3
4
5
class ProjectsController < InheritedResources::Base
  def destroy
    destroy! { root_url }
  end
end

如果你想针对特定的action简单的修改flash消息,可以使用:notice:alert关键字进行设定(正如你使用flash一样):

1
2
3
4
5
class ProjectsController < InheritedResources::Base
  def create
    create!(:notice => "Dude! Nice job creating that project.")
  end
end

正如上面提到的,还可以传入block来修改重定向路径:

1
2
3
4
5
class ProjectsController < InheritedResources::Base
  def create
    create!(:notice => "Dude! Nice job creating that project.") { root_url }
  end
end

现在让我们假设已经创建了一个项目,你需要做一些特殊的操作,但你并不想为此创建一个before filter,那么进行如下操作:

1
2
3
4
5
6
7
class ProjectsController < InheritedResources::Base
  def create
    @project = Project.new(params[:proect])
    @project.something_special!
    create!
  end
end

好吧,即使这么简单!最帅的部分是你只是对@project变量重新做了设定,而不是再次重新创建一个新的project。

对于update的action也是如此:

1
2
3
4
5
6
7
class ProjectsController < InheritedResources::Base
  def update
    @project = Project.find(params[:id])
    @project.something_special!
    update!
  end
end

在结束这个话题之前,我们想再多讨论一件事:”success/failure blocks”。假设当我们想更新project,为了防止失败,我们想重新跳转到一个url而不是重新渲染edit的template。

最直观的企图是:

1
2
3
4
5
6
7
8
9
class ProjectsController < InheritedResources::Base
  def update
    update! do |format|
      unless @project.errors.empty? # failure
        format.html { redirect_to project_url(@project) }
      end
    end
  end
end

结果看起来有点啰嗦,是吧?其实我们可以直接这样做:

1
2
3
4
5
6
7
class ProjectsController < InheritedResources::Base
  def update
    update! do |success, failure|
      failure.html { redirect_to project_url(@project) }
    end
  end
end

现在好很多了!解释一下这一切:当你设置带有一个参数的block时,将会在两种情况下执行:success和failure。但是如果你给这个block传递了两个参数,那么第一个只会在success的情况下执行,第二个会在failure情况下执行。你可以在同一个action里确保这一切都简洁和有组织。

聪明的重定向

虽然上面的重定向代码很简短,但也不需要你频繁的对重定向进行修改,因为Inherited Resources可以进行聪明的重定向(自1.2版以后)。action中的重定向计算方式取决于已经存在的controller方法。

在create和update的action中重定向的顺序依次如下:resource_urlcollection_urlparent_url(后面我们也会看到),最后是root_url。destroy的action中进行重定向的次序是collection_urlparent_urlroot_url

比如说:

1
2
3
4
class ButtonsController < InheritedResources::Base
  belongs_to :window
  actions :all, :except => [:show, :index]
end

此controller会在所有CUD action之后,重定向到父层window。

destroy中的success和failure场景

destroy的action也可能会失败,这通常需要在一个称之为before_destroy的callback中进行设置。不管怎样,为了能够告诉InheritedResources最后真正失败的结果,需要在model中添加错误消息。所以在model的before_destroy中应该做类似如下的操作:

1
2
3
4
5
6
def before_destroy
  if cant_be_destroyed?
    errors.add(:base, "not allowed")
    false
  end
end

Belongs to

最终,我们的Projects将会获得一些Task。随后你可以创建一个TasksController然后如下操作:

1
2
3
class TasksController < InheritedResources::Base
  belongs_to :project
end

belongs_to接受几个可选项能够配置这些关系。例如,如果你想让url像这个样子”/projects/:project_title/tasks”,你可以自定义如何让InheritedResources找到你的project:

1
2
3
class TaskController < InheritedResources::Base
  belongs_to :project, :finder => :find_by_title!, :param => :project_title
end

除此之外,还可以接受:route_name:parent_class:instance_name作为参数。更多详情可以参照lib/inherited_resources/class_methods.rb

嵌套belongs to

现在,我们的Task有一些Comment并且你需要嵌套的更深。好的实践经验告诉你嵌套的层级不要超过两层,但有时候你需要为一些安全问题作出妥协。这里有一个例子展示了你应该如何去做:

1
2
3
class CommentsController < InheritedResources::Base
  nested_belongs_to :project, :task
end

如果你需要配置其中任何的belongs to,你可以使用block嵌套它们:

1
2
3
4
5
class CommentsController < InheritedResources::Base
  belongs_to :project, :finder => :find_by_title!, :param => :project_title do
    belongs_to :task
  end
end

警告:调用多次belongs_to的效果和上面嵌套的相同:

1
2
3
4
class CommentsController < InheritedResources::Base
  belongs_to :project
  belongs_to :task
end

换句话说,上面的代码和调用nested_belongs_to的效果一样。

多态belongs to

我们可以更进一步。假设我们的Projects现在有Files,Messages和Tasks,并且它们都是可编辑的。以此为例,最好的解决方案是使用多态:

1
2
3
4
class CommentsController < InheritedResources::Base
  belongs_to :task, :file, :message, :polymorphic => true
  # polymorphic_belongs_to :task, :file, :message
end

甚至你可以使用嵌套resource:

1
2
3
4
5
class CommentsController < InheritedResources::Base
  belongs_to :project do
    belongs_to :task, :file, :message, :polymorphic => true
  end
end

上面的例子中url会是如下:

1
2
3
/project/1/task/13/comments
/project/1/file/11/comments
/project/1/message/9/comments

当你使用多态关联时,你会额外得到如下的辅助方法:

1
2
3
4
parent?         #=> true
parent_type     #=> :task
parent_class    #=> Task
parent          #=> @task

现在,Inherited Resources做了限制,不允许你把两个多态关联进行嵌套。

可选项belongs to

随后你决定创建一个view用来显示所有的comment,如果它们分别属于task,file或message,那么它们都是独立的。你可以如下重用多态controller:

1
2
3
4
class CommentsController < InheritedResources::Base
  belongs_to :task, :file, :message, :optional => true
  # optional_belongs_to :task, :file, :message
end

上面将会处理所有这些url:

1
2
3
4
/comment/1
/tasks/2/comment/5
/files/10/comment/3
/messages/13/comment/11

对待这部分就像是对待特殊的多态关联一样,这样所有的辅助方法都是可用的。正如你所预期的,当找不到父级的时候,辅助方法的返回如下:

1
2
3
4
parent?         #=> false
parent_type     #=> nil
parent_class    #=> nil
parent          #=> nil

单例

现在我们来为project添加一个manager。我们说Manager是一个单例的resource,因为一个Project只有一个manager。你应该在route中声明它为has_one(或resource)。

为了声明当前controller对应的resource为单例,只需要在defaults可选项中指定:singleton即可。

1
2
3
4
5
class ManagersController < InheritedResources::Base
  defaults :singleton => true
  belongs_to :project
  # singleton_belongs_to :project
end

现在你可以使用类似于”/projects/1/manager”的url。

在此例子的嵌套resource(它们当中的一些resource可以设置成单例)中可以分开声明:

1
2
3
4
5
class WorkersController < InheritedResources::Base
  # defaults :singleton => true  # if you have only single worker
  belongs_to :project
  belongs_to :manager, :singleton => true
end

相应的url类似于”/projects/1/manager/workers/1”。

它将再次处理所有一切并且把:index的action隐藏掉。

带有命名空间的controller

带有命名空间的controller不在作用域的范围内。

1
2
class Forum:PostsController < InheritedResources::Base
end

在当前情况下,Inherited Resources优先使用带有命名空间controller的默认的resource类:

1
2
3
Forum::Post
ForumPost
Post

URL辅助方法

当你使用InheritedResources的时候,它会为你创建一些URL辅助方法。这些方法会为你处理一切。:)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# /posts/1/comments
resource_url               # => [email protected]_param}
resource_url(comment)      # => /posts/1/comments/#{comment.to_param}
new_resource_url           # => /posts/1/comments/new
edit_resource_url          # => [email protected]_param}/edit
edit_resource_url(comment) # => /posts/1/comments/#{comment.to_param}/edit
collection_url             # => /posts/1/comments
parent_url                 # => /posts/1

# /projects/1/tasks
resource_url               # => [email protected]_param}
resource_url(task)         # => /projects/1/tasks/#{task.to_param}
new_resource_url           # => /projects/1/tasks/new
edit_resource_url          # => [email protected]_param}/edit
edit_resource_url(task)    # => /projects/1/tasks/#{task.to_param}/edit
collection_url             # => /projects/1/tasks
parent_url                 # => /projects/1

# /users
resource_url               # => [email protected]_param}
resource_url(user)         # => /users/#{user.to_param}
new_resource_url           # => /users/new
edit_resource_url          # => [email protected]_param}/edit
edit_resource_url(user)    # => /users/#{user.to_param}/edit
collection_url             # => /users
parent_url                 # => /

这些url辅助方法同样也接受hash可选项,就像routes中定义的一样。

1
2
# /projects/1/tasks
collection_url(:page => 1, :limit => 10) #=> /projects/1/tasks?page=1&limit=10

在多态的事例中,你还可以给父级的collection_url传递参数。

另外一个比较nice的地方是,这些url不是在运行的时候猜测的。它们都是在当你的应用程序加载的时候创建的(除了多态关联以外,它们都依赖于Rails的polymorphic_url)。

如何处理view层呢?

有时候只是DRY其中的controller层是不够的。如果你需要DRY你的view层,可以查看这个wiki页面:

https://github.com/josevalim/inherited_resources/wiki/Views-Inheritance

注意:在Rails 3.1中已经内建了view的继承。

一些DSL

对于一些DSL的爱好者而言,InheritedResources不会抛弃你们。你们可以直接在class的绑定中覆盖block中的success/failure。为此,你只需要在你的应用程序的controller中添加一个DSL的模块即可:

1
2
3
class ApplicationController < ActionController::Base
  include InheritedResources::DSL
end

随后你可以对此进行重写:

1
2
3
4
5
class ProjectsController < InheritedResources::Base
  update! do |success, failure|
    failure.html { redirect_to project_url(@project) }
  end
end

Strong Parameters

如果在你的controller里定义了一个名为permitted_params的方法,InheritedResources会在合适的地方调用它。这允许很简单的集成进strong_parameters的gem里:

1
2
3
def permitted_params
  params.permit(:widget => [:permitted_field, :other_permitted_field])
end

记住,如果你从client发往server的field中包含了一个array,你需要写成:permitted_field => [],而不能仅仅是:permitted_field

注意如果你使用strong_parameterrequire方法代替permit将不会正常工作,因为permit返回的是整个干净的hash参数,require返回的仅仅是之后列出的干净hash参数。

如果你需要params.require,可以像下面这样做:

1
2
3
def permitted_params
  {:widget => params.fetch(:widget, {}).permit(:permitted_field, :other_permitted_field)}
end

或者是更好的直接覆盖#build_resource_params:

1
2
3
def build_resource_params
  [params.fetch(:widget, {}).permit(:permitted_field, :other_permitted_field)]
end

对应的,你可以插入标准的Rails 4写法(就像rails的scaffold生成的那样):

1
2
3
def widget_params
  params.require(:widget).permit(:permitted_field, :other_permitted_field)
end

在这个例子中,你应该移除#permitted_params方法,因为它有更高的优先级。

更多

更多更详细的内容,可参见文档

Comments