Rails 6 的第二个 beta 版本即将发布,其中包含了 Zeitwerk 的集成。
虽然 Rails 6 最终版本将提供关于此功能的正式文档,但本文将帮助您在此期间理解这项新功能。
现在可以在类和模块定义中可靠地使用常量路径。
# Autoloading in this class' body matches Ruby semantics now.
class Admin::UsersController < ApplicationController
# ...
end
所有已知的 require_dependency 用例都已被消除。
自动加载的常量依赖于执行顺序的边界情况已不复存在。
自动加载在一般情况下是线程安全的,而不仅仅是对于目前支持的具有显式锁(如 Web 请求)的用例。例如,您现在可以编写多线程脚本并通过 bin/rails runner 运行,它们将能够正常自动加载。
此外,应用程序在付出相同成本的情况下还能获得一些性能提升。
自动加载常量不再涉及遍历自动加载路径在文件系统中查找匹配项。Zeitwerk 只进行一次扫描,并使用绝对文件名进行自动加载。此外,这次扫描会惰性地深入子目录,仅在命名空间被使用时进行。
当由于文件系统更改而重新加载应用程序时,作为 gem 加载的引擎的自动加载路径中的代码将不再被重新加载。
预加载不仅会预加载应用程序,还会预加载由 Zeitwerk 管理的任何 gem 的代码。
Rails 6 提供了两种自动加载模式::zeitwerk> 和 :classic>。它们使用新的配置点 config.autoloader 进行设置。
对于 CRuby,Zeitwerk 模式是 Rails 6 的默认模式,它通过
load_defaults "6.0"
config/application.rb 中的以下配置自动启用。
应用程序可以通过在加载默认设置的行之后添加
config.autoloader = :classic
来选择退出。
虽然 Zeitwerk 模式的第一个 API 正在趋于稳定,但目前仍有一些探索性。如果您使用的 Rails 版本比 6.0.0.beta2 更新,请查看当前文档。
自动加载路径的配置点仍然是 config.autoload_paths,并且如果您在应用程序初始化期间手动将内容推送到 ActiveSupport::Dependencies.autoload_paths,也会正常工作。
所有已知的 require_dependency 用例都已被消除。原则上,您应该直接删除代码库中的所有这些调用。另请参阅关于 STI 的下一节。
Active Record 需要完全加载 STI 层级才能生成正确的 SQL。Zeitwerk 中的预加载就是为此用例设计的。
# config/initializers/preload_vehicle_sti.rb
autoloader = Rails.autoloaders.main
sti_leaves = %w(car motorbike truck)
sti_leaves.each do |leaf|
autoloader.preload("#{Rails.root}/app/models/#{leaf}.rb")
end
通过预加载树的叶子节点,自动加载将负责向上加载整个层级(通过父类)。
这些文件将在启动和每次重新加载时进行预加载。
在 Zeitwerk 模式下,Rails.autoloaders 是一个可枚举对象,其中包含两个 Zeitwerk 实例,分别称为 main 和 once。前者负责管理您的应用程序,后者负责管理作为 gem 加载的引擎,以及 config.autoload_once_paths(其未来并不光明)中的任何内容。Rails 使用 main 进行重新加载,而 once 仅用于自动加载和预加载,但不会重新加载。
这些实例可以分别通过以下方式访问:
Rails.autoloaders.main
Rails.autoloaders.once
但由于 Rails.autoloaders 是一个可枚举对象,直接访问的用例应该不会太多。
如果您想查看自动加载器的工作情况,可以将以下内容添加到
Rails.autoloaders.logger = method(:puts)
config/application.rb 中,在设置完框架默认值之后。除了可调用对象外,Rails.autoloaders.logger= 还接受任何响应 debug 方法(参数为 1)的对象,就像常规日志记录器一样。
如果您想查看内存中所有 Zeitwerk 实例(包括 Rails 的和其他可能管理 gem 的实例)的活动,您可以设置
Zeitwerk::Loader.default_logger = method(:puts)
在 config/application.rb 的顶部,在 Bundle.require 之前。
对于标准 concerns 目录(例如 app/models/concerns)下的文件,Concerns 不能作为命名空间。也就是说,app/models/concerns/geolocatable.rb 应该定义 Geolocatable,而不是 Concerns::Geolocatable。
应用程序启动后,自动加载路径会被冻结。
ActiveSupport::Dependencies.autoload_paths 中在启动时不存在的目录将被忽略。我们这里指的是数组的实际元素,而不是它们的子目录。启动时存在的自动加载路径的新子目录会像往常一样被正常拾取。(这在最终版本中可能会有所更改。)
定义类或模块作为命名空间的文件,需要使用 class 和 module 关键字来定义类或模块。例如,如果 app/models/hotel.rb 定义了 Hotel 类,而 app/models/hotel/pricing.rb 定义了酒店的混合模块,那么 Hotel 类必须使用 class 关键字定义,而不能使用 Hotel = Class.new { ... } 或 Hotel = Struct.new { ... } 或其他类似方式。这些习惯用法在不作为命名空间的类和模块中是允许的。
一个文件应该只在其命名空间中定义一个常量(但可以定义内部常量)。因此,如果 app/models/foo.rb 定义了 Foo,同时也定义了 Bar,那么 Bar 将不会被重新加载,而是在 Foo 被重新加载时被重新打开。然而,这在经典模式下也已强烈不推荐,约定是使用与文件名匹配的单个主常量。您可以拥有内部常量,因此 Foo 可以定义一个辅助内部类 Foo::Woo。