【问题标题】:How can I automatically run some code when my gem is activated?如何在我的 gem 被激活时自动运行一些代码?
【发布时间】:2013-09-22 09:47:22
【问题描述】:

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

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

我所有的猴子补丁代码都包含在 gem 的一个 ruby​​ 文件中(比如说lib/mygem/patch.rb)。如果我在Bundle.require :misc 的现有行之后手动编辑基本应用程序的源以调用require 'mygem/patch',那么一切都很好。然而,这是草率的,并且每次我重新安装 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 中的猴子修补问题?

谢谢!

【问题讨论】:

    标签: ruby plugins rubygems gem monkeypatching


    【解决方案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 中。主级别的代码将按预期运行,包括它是否在模块中——重要的是“主级别”部分。

    请谨慎执行任何这些操作,因为您不想影响 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()
    

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

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

    【讨论】:

    • 胆小的狮子希望你详细说明,“..只是不要把它放在类或模块中..”,但不敢问。
    • “胆小的狮子”?在文学和电影界,不止一个角色叫“铁皮人”。为了详细说明我所说的,我想我必须添加一些示例代码来解释。
    • CL 说他的问题很笼统,关于加载脚本时运行的代码,并指出您可以在构建类时调用类方法(例如,class Cat; def self.speak; p "Meow"; end; speak; end => "Meow")。
    • @theTinMan 我的问题是 ruby​​gems 特定的,这都是一般的 ruby​​ 信息,根本没有解决它。我想知道当我的 gem 被 Rubygems 或 Bundler 激活/加载时如何触发代码运行。你明白吗?如果需要,我可以重新提出问题。
    • 我添加了一个更具体的例子,希望能澄清我的要求
    【解决方案2】:

    很遗憾,这并不是一个真正受支持的功能。

    但是,由于您的 gem 的 lib 目录已添加到加载路径,您可以通过在您的 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 之后存在于加载路径中的文件)。

    【讨论】:

      猜你喜欢
      • 2018-07-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多