构建一个 Ruby Gem 第十二章 Rails 钩子

如果你熟悉 Rails 的话,你知道最主要的三个组件是 models,controllers 和 views。 没什么好吃惊的,有对应的 Ruby 模块,它们是: ActiveRecord , ActionController 和 ActionView。

Rails 提供了钩子来访问这些类库在初始化的时候。通过添加或修改这些类库的代码,Rails 栈做任何事。 在本章中,我们会看看最常见的方式去添加 gem的代码到一个 Rails 应用和 ActiveSupport 是如何帮助的。

Railties

Railties 是对于配置和加载 Rails 框架的类库的钩子。它们允许我们很方便的添加或修改在 Rails 中已经存在的功能。 使用一个 Railtie 让我们有能力去添加 view 帮助方法,controller 方法,模型方法,生成器和一些列其他有用的特性。 Railtie 的文档提供了这些任务的示例代码。文档很少考虑到 Rails 在表面之下做的事情。 然后,事实上我们可以钩子初始化进程,这意味着如果我们知道方法处于哪个类中,我们可以改变或者扩展这个目标类。

看看第一个例子

class MyRailtie < Rails::Railtie
  initializer "my_railtie.configure_rails_initialization" do
    # some initialization behavior
  end
end

在 Railtie 中,Initializers 并不是在 config/initializers 目录下的那类东西。 它们是特殊的代码块来钩子 Rails 初始化进程的。 为了实现这个功能,一个 initializer 需要一个专门的名字(就像上面那个)并且相关的代码被放在接踵而至的块中。 在我们深入创建一个 Railtie 之前,让我们探索一些扩展已有 Ruyb 类的方法。

Ruby Extensions

假设我们有如下的类:

module MegaLotto
  class HolidayDrawing
    def draw
      puts "This drawing is holiday-worthy!"
    end
  end
end

再想象一下我们想要添加 #jackpot 方法,但是类已经被定义了。Ruby 给了我们打猴子补丁的能力,正如我们在核心扩展中看到的那样。 最简单的实现方法就是在一个新的单独的文件中重新打开 MegaLotto::HolidayDrawing 类并且添加新方法:

module MegaLotto
  class HolidayDrawing
    def jackpot
      puts "You've won the big one!"
    end
  end
end

现在我们既可以访问原先已经定义的 #draw 方法,也可以访问新的 #jackpot 方法。

MegaLotto::HolidayDrawing.new.draw # => This drawing is holiday-worthy!
MegaLotto::HolidayDrawing.new.jackpot # => You've won the big one!

另一个给 Ruby 类添加方法的选项是使用 extend 或者 include 方法。虽然这篇博文有点老了,但是这模式在今天依然和适合 Ruby。 通常当使用 extend 或者 include 时,对于要包含目标方法(在我们的例子用是 #jackpot)放在一个单独的 Ruby 模块中:

module MegaLotto
  module Jackpot
    def jackpot
      puts "You've won the big one!"
    end
  end
end

为了添加 #jackpo 方法到 MegaLotto::HolidayDrawing,我们再一次重构耐心打开 MegaLotto::HolidayDrawing 类并且包含那个模块:

module MegaLotto
  class HolidayDrawing
    include MegaLotto::Jackpot
  end
end

这产生了和之前一样的结果,但是具体实现被抽象到了一个模块:

MegaLotto::HolidayDrawing.new.jackpot # => You've won the big one!

再进一步,包含一个类方法到一个标准的 Ruby 类中,我们可以把5行代码缩减为:

MegaLotto::HolidayDrawing.include(MegaLotto::Jackpot)

不过,当我们尝试时,我们得到了下面的错误:

NoMethodError: private method `include` called for
MegaLotto::HolidayDrawing:Class from (irb):36
from /Users/bhilkert/.rbenv/versions/2.0.0-p247/bin/irb:12:in `<main>`

这说明包含了一个私有方法,所以用 Ruby 的 send 方法避开 Ruby 的可见性限制,这样就能访问到私有方法了。 send 方法接收一个方法名(符号或字符串)以及可选的参数。 看上去就像这样:

MegaLotto::HolidayDrawing.send(:include, MegaLotto::Jackpot)

调用 #jackpot 的结果和预期一样:

MegaLotto::HolidayDrawing.new.jackpot # => You've won the big one!

注意:extend 的用法类似,除了我们不需要使用 send 因为方法是 public 的。使用 extend 对源对象添加了类方法。所以它不适合我们这个例子。 这些加载的方式是需要一个 Rails 初始化钩子的。如果我们使用上面的例子,一个例子 Railtie 可以像这样:

class MyRailtie < Rails::Railtie
  initializer "my_railtie.configure_rails_initialization" do
    MegaLotto::HolidayDrawing.send(:include, MegaLotto::Jackpot)
  end
end

现在,MegaLotto::HolidayDrawing 命名空间不被 Rails 启动时包含,所以这个例子没有帮助。 然而,如果我们把它应用到 ActionView::Base 中,你可以看到它是如何立刻变的有用的:

class MyRailtie < Rails::Railtie
  initializer "my_railtie.configure_rails_initialization" do
    ActionView::Base.send(:include, MegaLotto::Jackpot)
  end
end

这是一个常见模式对于 gems 包含 view helpers。然后,我们可以通过改变 ActiveSupport 的 on_load 方法来做的更好。

Active Support Load Hooks

一个扩展原生 Rails 的方法是通过 Active Support gem。正如前面所提到的,Active Support 包含了一些列帮助和扩展方法。 Active Support 有一个 .on_load 方法来保持加载钩子的跟踪。 当一个特定的 Rails 类被加载时,它的相关的加载钩子会被执行。 .on_load 的源码只是一个标准的 Ruby Hash,包含一个 key 对于目标 Rails 类。 让我们看看和 Railtie 等价的 Active Support .on_load 方法。 对于我们的 initializer 它看起来像这样:

class MyRailtie < Rails::Railtie
  initializer "my_railtie.configure_rails_initialization" do
    # some initialization behavior
  end
end

使用 initializer,不需要使用 include 或者 extend 来添加方法到一个已经存在的类(就像我们上面看到的), 我们会触发包含的方法当一个特定的 Rails 类库加载时(这里的例子是 action_view):

class MyRailtie < Rails::Railtie
  initializer "my_railtie.configure_rails_initialization" do
    ActiveSupport.on_load(:action_view) do
      # some initialization behavior, but in the
      # context of the base `ActionView` class
    end
  end
end

这种方法的好处是它看上去更像原生的 Ruby 并且避开了不得不使用 send 来让类包含额外的模块。 我们将会对将要到来的 Rails 集成使用这种方式。

真实世界的例子

WillPaginate gem 提供了一个很好的例子关于 ActiveSupport.on_load 钩子的价值。 这是我见过的很复杂的 Railtie 之一。它让分页影响了 views,controllers 和 models 当查询和展示数据给用户看时。 注意 .on_load 钩子对于 ActionView,ActionController 和 ActiveModel 。 它说明了 ActionController 它自己使用 .on_load 方法来加载它的内部。 即使 Rails 内部使用 Railtie 钩子提供给了 gem 的作者!

总结

在 Rails 初始化时访问 Rails 类库给了我们很大的灵活性,让 gem 的创造者可以集成 Rails 栈的任何组件。 很明显 Rails 核心团队让 gem 集成和访问 Rails 内部有一个优先权。 这允许我们,作为 gem 开发者,通过分离开发类库来构建有价值的功能。 希望这个章节让我们知道了集成和 Rails 初始化进程加钩子是多么简单。 接着 Rails 集成的话题,下面我们会看看我们如何给我们的 gem 加上 Rails 的 view 帮助方法。