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 flash 消息

这是常见做法,当成功处理 create、update 和 destroy 操作时,所有控制器都会向用户显示一个 flash 消息。我们可以轻松地从我们的控制器中删除这些 flash 消息,然后使用 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

然后出现了第一个问题:如果我不想在具体情况下添加 flash 消息,该怎么办?这可以使用选项解决,因为发送到 respond_with 的所有选项都发送到响应器,我们也可以利用它


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,它将发送到响应器


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

同之前一样,你可以利用一些选项来自定义默认分页行为。

总结

以上所有示例都包含在模块中,这意味着我们的实际响应器尚未创建


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

要激活它,我们只需在应用程序控制器中覆盖响应器方法


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 轻松精简代码。你呢?是否已经想到一个有趣的用例?