2019 年 2 月 22 日,星期五

Rails 6 中的 Zeitwerk 集成 (Beta 2)

由 fxn 发布

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

来选择退出。

API 状态

虽然 Zeitwerk 模式的第一个 API 正在趋于稳定,但目前仍有一些探索性。如果您使用的 Rails 版本比 6.0.0.beta2 更新,请查看当前文档。

自动加载路径

自动加载路径的配置点仍然是 config.autoload_paths,并且如果您在应用程序初始化期间手动将内容推送到 ActiveSupport::Dependencies.autoload_paths,也会正常工作。

require_dependency

所有已知的 require_dependency 用例都已被消除。原则上,您应该直接删除代码库中的所有这些调用。另请参阅关于 STI 的下一节。

STI (Single Table Inheritance)

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

通过预加载树的叶子节点,自动加载将负责向上加载整个层级(通过父类)。

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

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 定义了酒店的混合模块,那么 Hotel 类必须使用 class 关键字定义,而不能使用 Hotel = Class.new { ... }Hotel = Struct.new { ... } 或其他类似方式。这些习惯用法在不作为命名空间的类和模块中是允许的。

  • 一个文件应该只在其命名空间中定义一个常量(但可以定义内部常量)。因此,如果 app/models/foo.rb 定义了 Foo,同时也定义了 Bar,那么 Bar 将不会被重新加载,而是在 Foo 被重新加载时被重新打开。然而,这在经典模式下也已强烈不推荐,约定是使用与文件名匹配的单个主常量。您可以拥有内部常量,因此 Foo 可以定义一个辅助内部类 Foo::Woo