2009年8月31日,星期一

喜欢 ActionController::Responder 的三个理由

作者:José Valim

几周前,我写了一篇关于新添加的 ActionController::Responder 的文章,它只需在一个地方就能总结出您特定格式的应用程序行为。例如,默认的 HTML 行为是这样写的:


class ActionController::Responder
  def to_html
    if get?
      render
    elsif has_errors?
      render :action => (post? ? :new : :edit)
    else
      redirect_to resource
    end
  end
end

以下是这个新层可以提供的灵活性的三个例子。

1) HTTP 缓存

Responder 的一个简单但功能强大的用途是轻松地为您的所有资源应用 HTTP 缓存。为简单起见,我们只缓存那些不处理集合的 GET 请求。


module CachedResponder
  def to_html
    if get? && resource.respond_to?(:updated_at)
      return if controller.fresh_when(:last_modified => resource.updated_at.utc)
    end
    super
  end
end

2) I18n 闪存消息

一个常见的做法是,当创建、更新和销毁操作成功处理时,都会向用户显示一个闪存消息。我们可以很容易地将闪存消息从控制器中移除,让 Responder 借助 I18n 框架来处理它们。实现起来非常直接。


module FlashResponder
  # If it's not a get request and the object has no errors, set the flash message
  # according to the current action. If the controller is users/pictures, the
  # flash message lookup for create is:
  #
  #   flash.users.pictures.create
  #   flash.actions.create
  #
  def to_html
    unless get? || has_errors?
      namespace = controller.controller_path.split('/')
      namespace << controller.action_name
      flash[:success] = I18n.t(namespace.join("."), :scope => :flash,
       :default => "actions.#{controller.action_name}", :resource => resource.class.human_name)
    end
    super
  end
end

这时就会出现第一个问题:如果我不想在特定情况下添加闪存消息怎么办?这可以通过选项来解决,因为传递给 respond_with 的所有选项都会传递给 responder,我们也可以利用这一点。


class MyResponder < ActionController::Responder
  def to_html
    unless get? || has_errors? || options.delete(:flash) == false
      namespace = controller.controller_path.split('/')
      namespace << controller.action_name
      flash[:success] = I18n.t(namespace.join("."), :scope => :flash,
       :default => "actions.#{controller.action_name}", :resource => resource.class.human_name)
    end
    super
  end
end

我们可以这样调用它:


class PostsController < ApplicationController
  def create
    @post = Post.create(params[:post])
    respond_with(@post, :flash => false)
  end
end

3) 即时分页

有些人会从头开始编写分页功能,有些人会在某个时候添加。尽管如此,分页更像是一种规则而不是例外。Rails 3 能处理这个问题吗?首先,让我们用 respond_with 来看看一个 index 操作。


class PostsController < ApplicationController
  def index
    @posts = Post.all
    respond_with(@posts)
  end
end

现在,当我们调用 Post.all 时,它返回一个 Post 集合数组,所以分页应该在检索集合之前完成。感谢 Emilio 以及 他集成 ActiveRelation 与 ActiveRecord 的工作Post.all 将返回一个 ActiveRecord::Relation,它将被发送给 responder。


module PaginatedResponder
  # Receives a relation and sets the pagination scope in the collection
  # instance variable. For example, in PostsController it would
  # set the @posts variable with Post.all.paginate(params[:page]).
  def to_html
    if get? && resource.is_a?(ActiveRecord::Relation)
      paginated = resource.paginate(controller.params[:page])
      controller.instance_variable_set("@#{controller.controller_name}", paginated)
    end
    super
  end
end

然而,上面的代码肯定有问题。设置分页范围似乎更像是控制器的职责。所以我们可以这样重写:


module PaginatedResponder
  def to_html
    if get? && resource.is_a?(ActiveRecord::Relation)
      controller.paginated_scope(resource)
    end
    super
  end
end

class ApplicationController < ActionController::Base
  def paginated_scope(relation)
    instance_variable_set "@#{controller_name}", relation.paginate(params[:page])
  end
  hide_action :paginated_scope
end

和以前一样,您可以使用一些选项来定制默认的分页行为。

总结

上面所有的例子都包含在模块中,这意味着我们的实际 responder 还没有被创建。


class MyResponder < ActionController::Responder
  include CachedResponder
  include FlashResponder
  include PaginatedResponder
end

要激活它,我们只需要在我们的应用程序控制器中重写 responder 方法。


class ApplicationController < ActionController::Base
  def paginated_scope(relation)
    instance_variable_set "@#{controller_name}", relation.paginate(params[:page])
  end
  hide_action :paginated_scope

  protected
  def responder
    MyResponder
  end
end

尽管这些例子很简单,但它们展示了如何使用 Responder 轻松地使您的代码更加 DRY。您呢?是否已经想到了一个有趣的用例?