2019 年 2 月 22 日星期五

Zeitwerk 与 Rails 6(Beta 2)集成

由 fxn 发布

Rails 6 即将发布第二个测试版,其中集成了 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 设置。

Zeitwerk 模式是 Rails 6 中 CRuby 的默认模式,自动通过以下命令启用

load_defaults "6.0"

in config/application.rb

应用可以选择退出将以下内容放在

config.autoloader = :classic

加载默认值的那一行之后。

API 状态

Zeitwerk 模式的第一个 API 正在融合中,这目前仍处于探索阶段。如果你的 Rails 版本高于 6.0.0.beta2,请查阅当前的文档。

自动加载路径

自动加载路径的配置点仍为 config.autoload_paths,如果你在应用初始化期间手动推送到 ActiveSupport::Dependencies.autoload_paths,这种方式同样可行。

require_dependency

已消除已知的 require_dependency 的所有用途。原则上,只需在代码库中删除所有这些调用即可。另请参见下一部分关于 STI 的内容。

STI

为了生成正确的 SQL,Active Record 需要充分加载 STI 层次结构。预加载到 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

通过预加载树叶,自动加载将沿父类处理整个层次结构向上。

这些文件将在启动时和每次重新加载时进行预加载。

Rails.autoloaders

在 Zeitwerk 模式中,Rails.autoloaders 是一个可枚举的,它有两个 Zeitwerk 实例,分别称为 mainonce。前者管理应用程序,后者管理加载为 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 中启动时不存在的目录将被忽略。这里我们仅指数组的实际元素,而不指其子目录。在启动时已存在的自动加载路径的新子目录将一如既往地正确选择。(这可能会更改以达到最终状态。)

  • 将类或模块用作命名空间的文件需要使用 classmodule 关键字来定义类或模块。例如,如果你有 app/models/hotel.rb 来定义 Hotel 类,且 app/models/hotel/pricing.rb 为酒店定义了一个 mixin,则 Hotel 类必须使用 class 定义,而不能使用 Hotel = Class.new { ... }Hotel = Struct.new { ... } 或类似代码进行定义。这些惯用语适用于不充当命名空间的类和模块中。

  • 一个文件在其命名空间中应仅定义一个常量(但可以定义内部常量)。因此,如果 app/models/foo.rb 定义了 Foo,还定义了 Bar,则当 Foo 重新加载时,将不会重新加载 Bar,而会重新打开它。不过,无论如何,在经典模式中强烈不推荐这样做,惯例是拥有一个与文件名匹配的单个主常量。你可以拥有内部常量,因此 Foo 可以定义一个辅助内部类 Foo::Woo