当我的gem被激活时,如何自动运行一些代码?

时间:2013-09-17 19:45:31

标签: ruby plugins rubygems gem monkeypatching

我创建了一个gem,它本质上是现有Ruby应用程序的插件/扩展。该应用程序使用捆绑器对此进行了一些考虑;它在启动时会自动执行Bundle.require :misc

我已将我的Gem添加到:misc中的Gemfile组,我的gem正按预期添加到加载路径中。挑战在于,为了使我的扩展能够正常工作,我需要修补一些现有的类。

我所有的猴子补丁代码都包含在gem中的ruby文件中(比方说lib/mygem/patch.rb)。如果我手动编辑基础应用的源代码,以便在require 'mygem/patch'的现有行之后调用Bundle.require :misc,那么一切都很有效。然而,这很草率,每次重新安装宝石或移动到新机器时都需要为基础应用编辑已安装的宝石。

# Currently I can load my gem and execute the monkey patch in 2 lines
gem 'mygem'
require 'mygem/patch'

# Or with bundler (mygem is in the :misc group)
Bundle.require :misc
require 'mygem/patch'

# I want to achieve the same result in only one line
gem 'mygem'

# or
Bundle.require :misc

我希望在从基础应用程序激活时,从我的扩展gem中自动运行一些代码,而不必手动要求该文件。当使用gem 'mygem'语句单独激活gem时,或者作为Bundle.require :misc中的包组的一部分时,解决方案应该都可以工作。

这可能吗?是否有更好的模式来解决“插件/扩展”gem中的猴子修补问题?

谢谢!

2 个答案:

答案 0 :(得分:1)

您希望代码运行时并不十分清楚。你希望它在Ruby启动程序时运行,还是Bundler为应用程序构建gem的列表?

如果你在谈论Ruby何时加载代码来运行它......

Ruby支持在变量初始化之前运行的BEGIN {...}END {...}块。它们在看到它们时运行,但显然,END {...}将在程序结束时运行,就在解释器退出之前。

请参阅"Programming Ruby"

  

BEGIN和END块

     

每个Ruby源文件都可以声明在加载文件时运行的代码块(BEGIN块)以及程序执行完毕后(END块)。

BEGIN {
  begin code
}


END {
  end code
}
  

程序可能包含多个BEGIN和END块。 BEGIN块按它们遇到的顺序执行。 END块以相反的顺序执行。

您还可以将代码添加到gem,类或模块文件中,该文件将在加载文件时运行,以初始化动态定义的变量/常量。对此没有特殊要求,只是不要将其放在classdef中。主要级别的代码将按预期运行,包括它是否在模块中 - 这是“主要级别”部分,这很重要。

请仔细执行这些操作,因为您不希望影响宝石的加载或退出时间,从而对程序产生负面影响。

另外,要非常小心猴子修补Ruby中的现有类。如果更改现有方法的功能,则可能会严重破坏用户的Ruby应用程序。如果添加的方法名称与用户定义的方法名称冲突,则也会出现相同的情况,并且在任何一种情况下,都很难跟踪和修复。


详细说明:

当Ruby需要类或模块文件时,它会加载它,然后运行它在那里找到的代码。初始化任何类或常量,并运行主级别的任何代码。在文件的底部,Ruby将继续下一个“require”语句。

考虑一个包含以下内容的文件:

class Foo
  def initialize
    puts "Inside Foo.new()"
  end
end

运行它将不返回任何内容,也不执行任何操作。该类将被解析,但由于没有任何内容调用Foo.new,我们甚至看不到输出。这就是为什么我们不嵌入我们想要在类中自动运行的代码,因为除非显式调用它,否则它不会被运行。要求它会有相同的结果。

将代码更改为以下内容并运行它会输出“Inside Foo.new()”:

class Foo
  def initialize
    puts "Inside Foo.new()"
  end
end

Foo.new()
# >> Inside Foo.new()

明确调用类初始值设定项。

如果另一个文件需要第一个代码,那么在运行时不会发生任何事情,因为在需要代码或随后加载的文件中的某些内容告诉Ruby运行它之前,不会调用该类。运行类初始化程序会导致:

Inside Foo.new()

在任何情况下,方法或类定义都不会在fooFoo.new()调用之前运行,然后导致定义新实例,从而允许初始化程序运行。 / p>

如果OP希望在自己的宝石中运行某些内容,将其置于defclass内将隐藏它。它必须是Ruby运行它的主要级别。而且,这又假设问题是关于Ruby加载脚本时运行的代码,而不是代码Rubygems或Bundler可以在安装时运行。

答案 1 :(得分:1)

不幸的是,这不是一个真正受支持的功能。

但是,当您的gem的lib目录添加到加载路径时,您可以通过在lib目录中添加同名文件来搭载其他需求。

例如,如果您要进行monkeypatching的类依赖于另一个需要erb的类,则创建一个文件lib/erb.rb。由于erb位于标准库中,因此需要使用erb.rb文件,因为宝石出现在标准库之前的加载路径中。

您还需要确保您还需要原始文件。因此,您可以将erb.rb的内容设置为以下内容:

filename = File.basename(__FILE__)
$LOAD_PATH.map { |path|
  Dir.glob("#{path}/*#{filename}").map { |file|    
    require file unless file.include? GEM_NAME
  }
}

puts 'Custom file within gem was required'

然后构建/重新安装您的gem。当被劫持的require被调用时:

$ bundle exec irb
2.0.0 :001 > require 'erb'
Custom file within gem was required
 => true

这种方法只有在你的补丁需要生效之前可以劫持的require时(以及你宝石之后的加载路径中存在的文件)才能工作。