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

Rails: Auto-reload gem files used in dummy app

在开发 gem 时,我经常使用需要 gem 的虚拟 rails 应用程序,以便在我进行时尝试 gem 更改。此外,我使用相同的虚拟应用程序进行集成测试。

通常,我在

中有gem
~/rails/foo_gem

和关联的虚拟应用程序位于:

~/rails/foo_gem/spec/dummy_app

使用 zeitwerk 代码加载器,如何配置虚拟应用程序 不仅在更改时重新加载虚拟应用程序 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")

我尝试将 gem 添加到自动加载路径,但代码不会在文件系统更改时重新加载。

无效:Zeitwerk::Loaderenable_reloading

# ~/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 才能重新加载 gem 类.

无效:Zeitwerk::Loader#reloadrequire

如果 gem 的文件在 gem 中是必需的,例如

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

require 'foo_gem/bar`

然后文件 ~/rails/foo_gem/lib/foo_gem/bar.rbZeitwerk::Loader 忽略。调用 gem_loader.reload 不会重新加载此文件。

没有工作:Zeitwerk::Loader#reload 用于 gem 个文件的 zeitwerk 加载程序

如果不需要手动 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 由两个独立的 zeitwerk 加载器管理:foo_gem.rb 中的 loaderdevelopment.rb 中的 gem_loader。这显然是 zeitwerk 不允许的,它抱怨 Zeitwerk::Error:

loader ... wants to manage directory ~/rails/foo_gem/lib, which is already managed by ...

无效: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 来观察变化,但是,至少在我的 dockerized 设置中,重新加载代码的块从未被执行。

要在 file-system 更改时重新加载 gem 代码,我需要执行三个步骤:

  1. 取消注册在 foo_gem.rb 中定义的处理不同 gem 文件加载的 zeitwerk 加载程序。
  2. 在虚拟应用程序的 development.rb 中定义一个新的 zeitwerk 加载器,该应用程序配置有 enable_reloading
  3. 设置 file-system 观察器并在 gem 文件更改时触发重新加载。

我确信有更简单、更清洁的解决方案。随意 post 它作为一个单独的答案。我会 post 我的解决方案在这里,但请考虑它是一种解决方法。

Zeitwerk::Loaderfoo_gem.rb

# ~/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 加载程序
  • 这个加载器没有 enable_reloading 因为否则,只要使用 gem,就会为这个 gem 启用重新加载,而不仅仅是在开发 gem 时。
  • 我已经给加载器一个 tag,它允许稍后在 Zeitwerk::Registry 中找到这个加载器,以便 un-register 它。
  • 除了在 foo_gem.rb 中使用 zeitwerk,还可以在 like the devise gem does 中使用 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 文件。需要一个 file-system 观察者来触发对 file-system 更改的重新加载。我没有设法让 ActiveSupport::FileUpdateChecker working. Instead, I've used the listen gem 作为 file-system 观察者。

使用此设置,当使用虚拟应用程序的 rails serverrails console 或使用虚拟应用程序进行集成测试时,gem 文件会在编辑后重新加载,这意味着那一个不再需要重新启动 rails server 来获取更改。

参考资料