2

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

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

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

# 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 中的猴子修补问题?

谢谢!

4

2 回答 2

1

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

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

Ruby 支持BEGIN {...}END {...}块在变量初始化之前运行。它们按照所见的方式运行,但很明显,它们END {...}会在程序结束时运行,就在解释器退出之前。

请参阅“编程 Ruby”

BEGIN 和 END 块

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

BEGIN {
  begin code
}


END {
  end code
}

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

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

小心执行任何这些操作,因为您不想影响 gem 的加载或退出时间,从而对程序产生不利影响。

此外,在 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()

foo在任何情况下,方法或类定义在由or调用之前不会运行Foo.new(),这会导致定义新实例,从而允许初始化程序运行。

如果 OP 想在他自己的 gem 中运行某些东西,请将其放入 adefclass将其隐藏。它必须处于 Ruby 运行它的主要级别。而且,这再次假设问题是关于 Ruby 加载脚本时运行的代码,而不是关于 Rubygems 或 Bundler 可以在安装时运行的代码。

于 2013-09-17T20:05:33.680 回答
1

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

但是,由于libgem 的目录已添加到加载路径中,因此您可以通过在目录中添加同名文件来搭载其他需求lib

例如,如果您正在猴子补丁的类依赖于另一个需要的类erb,则创建一个文件lib/erb.rb. 与erb标准库中一样,您的erb.rb文件将是必需的,因为 gem 出现在标准库之前的加载路径中。

您需要确保您也需要原始文件。因此,您可以将内容设置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在你的补丁需要生效之前有一个你可以劫持的情况下才有效(并且对于你自己的 gem 之后存在于加载路径中的文件)。

于 2014-02-21T17:02:07.613 回答