在Rails中运行rake任务时方法命名空间冲突

Blo*_*ngs 13 rake ruby-on-rails

使用Rails 2.3.10如果我的lib/tasks看起来像这样

lib/tasks
- a.rake
- b.rake
Run Code Online (Sandbox Code Playgroud)

a.rake看起来像这样:

namespace :a do
    desc "Task A"
    task(:a=>:environment)do
      msg('I AM TASK A')
    end

    def msg(msg)
      puts "MSG SENT FROM Task A: #{msg}"
    end
end
Run Code Online (Sandbox Code Playgroud)

b.rake看起来像这样

namespace :b do
    desc "Task B"
    task(:b=>:environment)do
      msg('I AM TASK B')
    end

    def msg(msg)
      puts "MSG SENT FROM Task B: #{msg}"
    end
end
Run Code Online (Sandbox Code Playgroud)

然后,当我运行任务a

rake a RAILS_ENV=sandbox
Run Code Online (Sandbox Code Playgroud)

输出是"MSG SENT FROM Task B:I am TASK A"

因此,a.rake中定义的msg()辅助方法不会被调用.而是在b.rake中定义的那个被调用.(更重要的是,如果我有一个c.rake - 那么当我运行任务时,会调用msg辅助方法.

此方法命名空间是否与已知行为发生冲突?我原本以为命名空间会阻止这个.

谢谢

Bor*_*aMa 16

您观察到的是rake文件命名空间中的方法重新定义了以前定义的具有相同名称的方法.原因是Rake namespace与Ruby命名空间(类或模块)非常不同,实际上它们只是作为其中定义的任务名称的命名空间,而不是其他任何东西.因此,如果放在命名空间中,任务a就变成了任务a:a,a但任务之外的其他代码共享同一个全局命名空间.

这个事实以及Rake在运行给定任务之前加载所有任务这一事实解释了该方法重新定义的原因.

TL; DR:名称冲突的解决方案/提示

您不能指望两个具有相同名称(或任何其他代码)的方法放在单独的namespaces但外部任务中才能正常工作.不过,这里有几个提示可以解决这种情况:

  • 将方法放在任务中.如果两个msg方法都在a:ab:b任务中定义,则两个rake任务都将正常运行并显示预期的消息.

  • 如果您需要namespace在多个rake任务中使用rake的代码,请将方法/代码提取到真正的Ruby命名空间(例如两个模块)以及include需要它的任务中的代码.考虑一下样本耙的重写:

    # lib/tasks/a.rake:
    module HelperMethodsA
      def msg(msg)
        puts "MSG SENT FROM Task A: #{msg}"
      end
    end
    
    namespace :a do
      desc "Task A"
      task(:a => :environment) do
        include HelperMethodsA
        msg('I AM TASK A')
      end
    end
    
    # lib/tasks/b.rake:
    module HelperMethodsB
      def msg(msg)
        puts "MSG SENT FROM Task B: #{msg}"
      end
    end
    
    namespace :b do
      desc "Task B"
      task(:b => :environment) do
        include HelperMethodsB
        msg('I AM TASK B')
      end
    end
    
    Run Code Online (Sandbox Code Playgroud)

    因为这两个模块具有不同的名称,并且因为它们分别include位于相应的任务中,所以两个rake任务将再次按预期运行.

现在,让我们在源代码的帮助下证明上述声明......

证明Rake首先加载所有任务以及为什么这样做

这个很容易.在主要内容中,Rakefile您总能找到以下行:

Rails.application.load_tasks
Run Code Online (Sandbox Code Playgroud)

此方法最终从Rails引擎调用以下代码:

def run_tasks_blocks(*) #:nodoc:
  super
  paths["lib/tasks"].existent.sort.each { |ext| load(ext) }
end
Run Code Online (Sandbox Code Playgroud)

因此,它在lib/tasks目录中搜索所有rake文件,并按排序顺序一个接一个地加载它们.这就是为什么b.rake文件将被加载后a.rake,内部的任何内容都可能重新定义代码a.rake和所有以前加载的rake文件.

Rake必须加载所有rake文件,因为rake namespace名称不必与rake文件名相同,因此无法从任务/命名空间名称推断rake文件名.

证明rake的namespace不构成真正的类Ruby命名空间

加载rake文件后,执行Rake DSL语句,以及namespace方法.该方法获取在其中定义的代码块并对象的上下文中执行它(使用yield),该Rake.application对象Rake::Application是在所有rake任务之间共享的类的单个对象.没有为命名空间创建动态模块/类,它只是在主对象上下文中执行.

# this reuses the shared Rake.application object and executes the namespace code inside it
def namespace(name=nil, &block)
  # ...
  Rake.application.in_namespace(name, &block)
end

# the namespace block is yielded in the context of Rake.application shared object
def in_namespace(name)
  # ...
  @scope = Scope.new(name, @scope)
  ns = NameSpace.new(self, @scope)
  yield(ns)
  # ...
end
Run Code Online (Sandbox Code Playgroud)

请在此处此处查看相关来源.

Rake任务DO​​构成ruby命名空间

有了Rake任务本身,情况就不同了.对于每个任务,创建Rake::Task类(或类似类)的单独对象,并在该对象的上下文中运行任务代码.对象的创建在任务管理器中的intern方法中完成:

def intern(task_class, task_name)
  @tasks[task_name.to_s] ||= task_class.new(task_name, self)
end
Run Code Online (Sandbox Code Playgroud)

引用来自Rake的作者

最后,所有这些都得到了关于github的有趣讨论的证实,该讨论涉及一个非常相似和相关的问题,我们可以从中引用Rake的原作者Jim Weirich:

由于名称空间不引入实际方法范围,因此范围的唯一真正可能性是DSL模块.

...

也许,有一天,Rake命名空间将成为完整的类范围实体,并且可以挂起懒惰的定义,但我们还没有.


Soh*_*han 0

使用如下所示的命名空间:

namespace :rake_a do
  desc "Task A"
  task(:a=>:environment)do
    msg('I AM TASK A')
  end

  def msg(msg)
    puts "MSG SENT FROM Task A: #{msg}"
  end
end
Run Code Online (Sandbox Code Playgroud)