Rails:自动重新加载虚拟应用程序中使用的gem文件

4

在开发gem时,我经常使用一个虚拟的Rails应用程序,需要引用这个gem以便在进行修改时尝试它。同时,我也会将同一个虚拟应用程序用于集成测试。

通常情况下,我将gem放置在

~/rails/foo_gem

以及相关的虚拟应用程序在:

~/rails/foo_gem/spec/dummy_app

使用 zeitwerk 代码加载器,我该如何配置虚拟应用程序(dummy app) 才能在更改时不仅重新加载虚拟应用程序的 Ruby 文件,还能够更新 gem 文件中的更改?否则,在开发 gem 过程中,每次更改 gem 文件都需要重新加载虚拟应用程序的 Rails 服务器。
# ~/rails/foo_gem/spec/dummy_app/config/environments/development.rb

config.cache_classes = false
config.eager_load = false

# TODO: Add ~/rails/foo_gem/lib to the list 
# of watched and auto-reloaded directories.

没有生效:config.autoload_paths

# ~/rails/foo_gem/spec/dummy_app/config/environments/development.rb

gem_root_path = Pathname.new(File.expand_path(Rails.root.join("../..")))
config.autoload_paths << gem_root_path.join("lib")

我尝试将宝石添加到自动加载路径中,但是代码没有在文件系统更改时重新加载。

无效:具有enable_reloadingZeitwerk::Loader

# ~/rails/foo_gem/spec/dummy_app/config/environments/development.rb

gem_root_path = Pathname.new(File.expand_path(Rails.root.join("../..")))
gem_loader = Zeitwerk::Loader.new
gem_loader.push_dir gem_root_path.join("lib")
gem_loader.enable_reloading
gem_loader.log!
gem_loader.setup

添加一个单独的Zeitwerk加载器是没有用的;据我所了解,该加载器不会带有文件系统监听器;因此需要调用gem_loader.reload来重新加载宝石类。

Zeitwerk::Loader#reloadrequire一起使用无效

如果在宝石中需要引入宝石自身的文件,例如:

# ~/rails/foo_gem/lib/foo_gem.rb

require 'foo_gem/bar`

若使用Zeitwerk::Loader,则文件~/rails/foo_gem/lib/foo_gem/bar.rb将被忽略。调用gem_loader.reload不会重载此文件。

Zeitwerk::Loader#reload在使用Zeitwerk加载器加载gem文件时无效

如果不手动引用gem文件,而是使用不同的Zeitwerk加载器,例如:

# ~/rails/foo_gem/lib/foo_gem.rb

require "zeitwerk"
loader = Zeitwerk::Loader.new
loader.push_dir(__dir__)
loader.setup

如果目录 ~/rails/foo_gem/lib 同时被 foo_gem.rb 中的 loaderdevelopment.rb 中的 gem_loader 两个独立的 zeitwerk 加载器管理,则会导致问题。这显然不符合 zeitwerk 的规则,会抛出 Zeitwerk::Error 异常:

loader ... 希望管理目录 ~/rails/foo_gem/lib,但该目录已经被 ... 所管理。

无效: ActiveSupport::FileUpdateChecker

# ~/rails/foo_gem/spec/dummy_app/config/environments/development.rb

gem_root_path = Pathname.new(File.expand_path(Rails.root.join("../..")))
gem_loader = Zeitwerk::Loader.new
gem_loader.push_dir gem_root_path.join("lib")
gem_loader.enable_reloading
gem_loader.log!
gem_loader.setup

gem_files = gem_root_path.glob("lib/**/*.rb")
gem_update_checker = ActiveSupport::FileUpdateChecker.new(gem_files) do
  gem_loader.reload  # This line is never executed
end
ActiveSupport::Reloader.to_prepare do
  gem_update_checker.execute_if_updated
end

我曾尝试使用ActiveSupport::FileUpdateChecker来监视文件变化,但是,至少在我的Docker环境中,重载代码的块永远不会被执行。


你好,我正在尝试做同样的事情,并找到了你的答案,以及https://dev59.com/NX3aa4cB1Zd3GeqPg7RM#66730720。我还没有尝试过,但如果它有效,它似乎比你的方法简单一些。 - sly7_7
2个回答

1
为了在文件系统更改时重新加载 gem 代码,我需要执行三个步骤:
  1. 取消注册在 foo_gem.rb 中定义的 zeitwerk 加载器,该加载器处理加载不同的 gem 文件。
  2. 在虚拟应用程序的 development.rb 中定义一个新的 zeitwerk 加载器,并配置 enable_reloading
  3. 设置文件系统监视器,并在 gem 文件更改时触发重新加载。
我相信有一个更简单、更清晰的解决方案。请随意将其作为单独的答案发布。我会在这里发布我的解决方案,但请将其视为一种解决方法。

foo_gem.rb 中的 Zeitwerk::Loader

# ~/rails/foo_gem/lib/foo_gem.rb

# require 'foo_gem/bar`  # Did not work. Instead:

# (a) use zeitwerk:
require "zeitwerk"
loader = Zeitwerk::Loader.new
loader.push_dir File.join(__dir__)
loader.tag = "foo_gem"
loader.setup

# or (b) use autoload:
module FooGem
  autoload :Bar, "foo_gem/bar"
end

注意:

  • 过去,我只是从一个名为与 gem 相同的索引文件中使用 require 加载 gem 的所有 ruby 文件,例如: foo_gem.rb。但这在这里不起作用,因为 zeitwerk 似乎会忽略已经使用 require 加载过的文件。相反,我需要为 gem 创建一个单独的 zeitwerk loader。
  • 这个 loader 没有 enable_reloading,否则每次使用 gem 时都会启用重新加载,而不仅仅是在开发 gem 时。
  • 我给 loader 添加了一个 tag,以便稍后可以在 Zeitwerk::Registry 中找到此 loader 并取消注册。
  • 除了在 foo_gem.rb 中使用 zeitwerk 外,还可以像 devise gem 一样使用 autoload。如果要支持早于 6 版本的 rails,则这是最佳方法,因为 zeitwerk 需要 rails 6+。在这里使用 autoload 也使下一节中的步骤 1 不必要。

在虚拟应用程序的development.rb中使用Zeitwerk::Loader

# ~/rails/foo_gem/spec/dummy_app/config/environments/development.rb

# 1. Unregister the zeitwerk loader defined in foo_gem.rb that handles loading
#    the different gem files.
#
Zeitwerk::Registry.loaders.detect { |l| l.tag == "foo_gem" }.unregister

# 2. Define a new zeitwerk loader in the development.rb of the dummy app
#    that is configured with enable_reloading.
#
gem_root_path = Pathname.new(File.expand_path(Rails.root.join("../..")))
gem_loader = Zeitwerk::Loader.new
gem_loader.push_dir gem_root_path.join("lib")
gem_loader.enable_reloading
gem_loader.setup

# 3. Setup a file-system watcher and trigger a reload when a gem file changes.
#
Listen.to gem_root_path.join("lib"), only: /\.rb$/ do
  gem_loader.reload
end.start

注意:

  • Zeitwerk 不允许两个加载器管理相同的文件。因此,我需要注销之前定义的标记为 "foo_gem" 的加载器。
  • 虚拟应用程序中使用的新加载器具有 enable_reloading。因此,在使用虚拟应用程序时,包括 rails serverrails console 或运行规范时,可以重新加载 gem 文件。
  • zeitwerk 不会自动重新加载 gem 文件。需要一个文件系统监视器来触发文件系统更改时的重新加载。我无法使 ActiveSupport::FileUpdateChecker 正常工作。相反,我使用了 listen gem 作为文件系统监视器。

通过这种设置,当使用虚拟应用程序的 rails serverrails console 或集成测试时,编辑后的 gem 文件将被重新加载,这意味着不再需要重新启动 rails server 来获取更改。

参考资料


0
使用Zeitwerk进行自动重新加载应该很简单,只需在引擎的根目录下运行Rails服务器,而不是在/test/dummy目录中运行,就像这样:
# This:
/Users/lewis/work/myengine $ rails server

# NOT this:
/Users/lewis/work/myengine/test/dummy $ rails server

# NOT this:
/Users/lewis/work/myengine $ test/dummy/bin/rails server

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接