利用 ActiveRecord Serializers 构建 JSON 格式输出 已翻译 100%

Wyatt 投递于 2013/08/09 09:13 (共 6 段, 翻译完成于 08-14)
阅读 3932
收藏 35
4
加载中

在本文中,我将教你如何在Rails app中用不到40行的代码来解决你自己的json格式输出问题。这个办法利用了基本的面向对象技巧(继承和钩子方法),创造性地使用了Rails自带的序列化功能。如果你不喜欢逐步的解说,可以直接跳转到这里(我创建的一个详细的gist,包括一个基类和一个示例的子类)。

为什么?

Medeo这个app在Rails后端代码和客户端Javascript之间通过json对象进行数据传输,有三条途径:经过AJAX请求,通过websockets以及渲染在html页面中的属性值。这通常意味着:在app的不同地方和不同的上下文环境中,会反复利用序列化的代码。因此,基于JSON DSLs的这些流行的模板(比如 RABLjBuilder没有一个是适合我们的。ActiveModel::Serializersgem 更适合我们的需要,但是它太复杂而让我舍弃了它。最后我们终于找到了优雅的解决方案,它能很好地为我们服务。

无争
翻译于 2013/08/09 16:40
2

一个例子

现在用一个简单的例子来说明我们是怎样找到这个解决方案,以及这个方案的运行原理:一个允许评论的博客app。评论是由用户来写的,所以app中可能会有一些这样的ActiveRecord模型:

class Comment < ActiveRecord::Base
  attr_accessible :body
  belongs_to :user

  def html_body
      "<p>#{body}</p>"
  end
end

class User < ActiveRecord::Base
  attr_accessible :name
  has_many :comments
end

基本思想

为了用AJAX在客户端来创建评论和渲染UI,我们需要返回一个由评论的各个属性(也包括评论人的各属性)组成的JSON reponse。这个任务可以由Rails的Serialization模块中的as_json方法轻松搞定。如果你看过相关文档,你就会知道as_json方法接受一个哈希类型的参数,它会利用这个参数来格式化返回的JSON。关于这个参数们主要是注意一下几点:

  • :only 限制json包含的属性
  • :methods 包含model中对应的方法和值
  • :include 包含关联的对象

我们能够利用这些选项获得想要的JSON,如下:

>>> @comment.as_json :only => %w(id created_at), :methods => %w(html_body), :include => { 'user' => { 'only' => %w(id name) } }
=> {
  "id"=>1,
  "html_body"=>"lorem ipsum dolor...",
  "created_at"=>"2013-07-26T10:38:47-07:00",
  "user"=>{
      "id"=>1,
      "name"=>"Matthew"
  }
}
无争
翻译于 2013/08/09 17:19
2
这正是我们想要的结果,但是它还不是一个非常理想的解决方案。as_json方法绝对是有点儿难以理解的。如果我们想要复用这段序列化代码,就需要复制传递给 as_json方法的所有参数,而它们散布在整个代码中。当我们需要修改代码的时候,这将会成为一个噩梦。清除这一问题的合理方法是,把对comment的序列化这一职责进行封装:
class CommentSerializer
  attr_reader :comment

  def initialize(comment)
      @comment = comment
  end

  def as_json(options={})
      comment.as_json(:only => attributes, :methods => methods, :include => include).merge(options)
  end

  private

      def attributes
          %w(id created_at)
      end

      def include
          { 'user' => { 'only' => %w(id name) } }
      end

      def methods
          %w(html_body)
      end
end

现在我们可以通过创建一个我们的序列化器实例:CommentSerializer.new(@comment).as_jsonNow,来序列化comments。这样做的额外好处是,使得我们可以很容易的对我们的序列化代码进行单独测试。个人认为,测试这种序列化代码非常重要,因为它促成了连接你的rails app和你的javascript代码的接口。

lwei
翻译于 2013/08/09 16:03
2

一个可复用的模式

最后,我们将要序列化一些模型不包括其注释,能在这些实例中复用该模式将会很惬意。一种方式是把泛型序列化那部分逻辑挪到一个抽象基类中去:

class BaseSerializer
  attr_reader :serialized_object

  def initialize(serialized_object)
      @serialized_object = serialized_object
  end

  def as_json(options={})
      serialized_object.as_json(:only => attributes, :methods => methods, :include => includes).merge(options)
  end

  private

      def attributes ; end
      def includes   ; end
      def methods    ; end
end
现在我们可以简单的通过继承 BaseSerializer类并重写attribute,includesandmethodshook方法,来创建序列化器。利用该基类,我们的CommentSerializer最后看起来会像这个样子:
class CommentSerializer < BaseSerializer

  private

      def attributes
          %w(id created_at)
      end

      def includes
          { 'user' => { 'only' => %w(id name) } }
      end

      def methods
          %w(html_body)
      end
end
lwei
翻译于 2013/08/09 15:25
2

更复杂的JSON属性

到目前为止,我们的解决方案只限于我们序列化的类里面定义的属性,方法和关联。显然,如果我们突破了这种约束,那将是极好的,这就可以用无包装方式来打包那些不属于我们模式的更复杂的JSON属性。我经常会遇到了一个要求往往是JSON响应,包括资源的URL。为此我们可以简单的在我们的基类里包括Rails URL helper,然后用它们来增加我们的派生类来调用 tosuperfrom 的返回值:

class BaseSerializer
  include Rails.application.routes.url_helpers
  # everything the same...
end

class CommentSerializer < BaseSerializer
  # everything the same...

  def as_json(options={})
      super(options).tap do |c|
          c['link'] = comment_path(serialized_object)
      end
  end
end

如果留心的话,你可能会注意到上述变化实际上打破了一些规矩。在此之前,它提供它的 ownas_jsonmethod, ourCommentSerializerwas 能够序列化个人意见以及收集的评论和注释。这是因为Rails提供了一个实现ofas_jsonfor bothArrays andActiveRecord::的关系,基本上是由callingas_jsonon来集合映射每个成员。

4
翻译于 2013/08/14 20:25
2
我们可以这样来完成同样的事情:在as_json方法中加入一个间接层,创建另一个方法来序列化个体实例:
class BaseSerializer
  # everything the same...

  def as_json(options={})
      if serialized_object.respond_to?(:to_ary)
          serialized_object.map { |object| serialize(object, options) }
      else
          serialize(serialized_object, options)
      end
  end

  def serialize(object, options={})
      object.as_json({:only => attributes, :methods => methods, :include => includes}.merge(options))
  end
end

上面的代码检测通过测试serialized_object是否响应to_ary(即是否可以强制转换成一个数组)来检测所序列化的是一个个体模型还是一个集合。如果它是一个集合,它返回的是一个把集合中所有元素序列化生成的数组。

结论

这几乎是整个解决方案了。如果你想完全了解BaseSerializer 和 CommentSerializer 类,请查阅这个要点。像往常一样,我很想听到你的任何意见/建议。

赵亮-碧海情天
翻译于 2013/08/13 12:09
2
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
加载中

评论(3)

黄金比
的确,序列化真的很重要!
w
wanax

引用来自“tomisacat”的评论

写的好,翻译的也好

是的
asdjflls
asdjflls
写的好,翻译的也好
返回顶部
顶部