edge
更多信息请访问 rubyonrails.org: 更多 Ruby on Rails

Classic to Zeitwerk HOWTO

本指南记录了如何将Rails应用程序从“classic”模式迁移到“zeitwerk”模式。

阅读完本指南后,您将了解:

1 什么是“classic”和“zeitwerk”模式?

从一开始,一直到Rails 5,Rails使用了Active Support中实现的自动加载器。这个自动加载器被称为“classic”,在Rails 6.x中仍然可用。Rails 7不再包含这个自动加载器。

从Rails 6开始,Rails使用一种新的更好的自动加载方式,它委托给Zeitwerk gem。这就是“zeitwerk”模式。默认情况下,加载6.0和6.1框架默认值的应用程序在“zeitwerk”模式下运行,这是Rails 7中唯一可用的模式。

2 为什么要从“classic”切换到“zeitwerk”?

“classic”自动加载器非常有用,但是在某些情况下,它存在一些问题,使得自动加载有时变得有些棘手和令人困惑。Zeitwerk就是为了解决这个问题而开发的,还有其他一些动机

升级到Rails 6.x时,强烈建议切换到“zeitwerk”模式,因为它是一个更好的自动加载器,“classic”模式已经被弃用。

Rails 7结束了过渡期,不再包含“classic”模式。

3 我很害怕

不用担心 :).

Zeitwerk的设计目标是尽可能与经典自动加载器兼容。如果您的应用程序今天能够正确自动加载,那么切换应该很容易。许多项目,无论大小,都报告了非常顺利的切换过程。

本指南将帮助您自信地更改自动加载器。

如果您遇到任何您不知道如何解决的情况,请随时在rails/rails中提出问题,并标记@fxn

4 如何激活“zeitwerk”模式

4.1 运行Rails 5.x或更早版本的应用程序

在运行6.0之前的Rails版本的应用程序中,不可用“zeitwerk”模式。您至少需要运行Rails 6.0。

4.2 运行Rails 6.x的应用程序

在运行Rails 6.x的应用程序中,有两种情况。

如果应用程序正在加载Rails 6.0或6.1的框架默认值,并且正在运行“classic”模式,则必须手动退出。您必须有类似于以下内容的内容:

# config/application.rb
config.load_defaults 6.0
config.autoloader = :classic # 删除此行

如上所述,只需删除覆盖,zeitwerk模式是默认模式。

另一方面,如果应用程序正在加载旧的框架默认值,则需要显式启用“zeitwerk”模式:

# config/application.rb
config.load_defaults 5.2
config.autoloader = :zeitwerk

4.3 运行Rails 7的应用程序

在Rails 7中,只有“zeitwerk”模式,您不需要做任何操作来启用它。

实际上,在Rails 7中,setter config.autoloader= 甚至不存在。如果config/application.rb中使用了它,请删除该行。

5 如何验证应用程序是否在“zeitwerk”模式下运行?

要验证应用程序是否在“zeitwerk”模式下运行,请执行以下操作:

bin/rails runner 'p Rails.autoloaders.zeitwerk_enabled?'

如果打印出true,则启用了“zeitwerk”模式。

6 我的应用程序是否符合Zeitwerk约定?

6.1 config.eager_load_paths

符合性测试仅适用于急切加载的文件。因此,为了验证Zeitwerk的符合性,建议将所有自动加载路径都放在急切加载路径中。

默认情况下已经是这样,但是如果项目配置了自定义的自动加载路径,就像这样:

config.autoload_paths << "#{Rails.root}/extras"

它们不会被急切加载,也不会被验证。将它们添加到急切加载路径很容易:

config.autoload_paths << "#{Rails.root}/extras"
config.eager_load_paths << "#{Rails.root}/extras"

6.2 zeitwerk:check

一旦启用了“zeitwerk”模式并且仔细检查了急切加载路径的配置,请运行:

bin/rails zeitwerk:check

成功的检查结果如下:

% bin/rails zeitwerk:check
Hold on, I am eager loading the application.
All is good!

根据应用程序的配置,可能会有其他输出,但是最后的“All is good!”是您要寻找的。 如果在前一节中解释的双重检查确定实际上需要在急加载路径之外设置一些自定义自动加载路径,任务将会检测并警告它们。然而,如果测试套件成功加载这些文件,那就没问题了。

现在,如果有任何一个文件没有定义预期的常量,任务将会告诉你。它会逐个文件进行检查,因为如果它继续进行,加载一个文件失败可能会导致其他与我们要运行的检查无关的失败,错误报告会令人困惑。

如果报告了一个常量,请修复该特定常量,然后再次运行任务。重复此过程,直到获得"All is good!"。

以以下为例:

% bin/rails zeitwerk:check
Hold on, I am eager loading the application.
expected file app/models/vat.rb to define constant Vat

VAT是欧洲的一种税收。文件app/models/vat.rb定义了VAT,但自动加载程序期望的是Vat,为什么?

6.3 首字母缩略词

这是您可能遇到的最常见的差异类型,它与首字母缩略词有关。让我们了解为什么会出现这个错误消息。

经典的自动加载程序能够自动加载VAT,因为它的输入是缺失常量的名称VAT,在其上调用underscore,得到vat,然后查找名为vat.rb的文件。它可以工作。

新的自动加载程序的输入是文件系统。给定文件vat.rb,Zeitwerk在vat上调用camelize,得到Vat,并期望该文件定义常量Vat。这就是错误消息的含义。

修复这个问题很简单,您只需要告诉inflector关于这个首字母缩略词:

# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
  inflect.acronym "VAT"
end

这样做会全局影响Active Support的词形变化。这可能没问题,但如果您愿意,您也可以将覆盖项传递给自动加载程序使用的词形变化器:

# config/initializers/zeitwerk.rb
Rails.autoloaders.main.inflector.inflect("vat" => "VAT")

通过这个选项,您可以更好地控制,因为只有名为vat.rb的文件或名为vat的目录才会被词形变化为VAT。名为vat_rules.rb的文件不受影响,可以很好地定义VatRules。如果项目存在这种命名不一致的情况,这可能很方便。

有了这个设置,检查通过了!

% bin/rails zeitwerk:check
Hold on, I am eager loading the application.
All is good!

一切正常后,建议在测试套件中继续验证项目。在测试套件中检查Zeitwerk的兼容性部分解释了如何做到这一点。

6.4 关注点

您可以从具有concerns子目录的标准结构中进行自动加载和急加载,如下所示:

app/models
app/models/concerns

默认情况下,app/models/concerns属于自动加载路径,因此它被认为是一个根目录。因此,默认情况下,app/models/concerns/foo.rb应该定义Foo,而不是Concerns::Foo

如果您的应用程序使用Concerns作为命名空间,您有两个选择:

  1. 从这些类和模块中删除Concerns命名空间,并更新客户端代码。
  2. 通过从自动加载路径中删除app/models/concerns来保持现状:
  # config/initializers/zeitwerk.rb
  ActiveSupport::Dependencies.
    autoload_paths.
    delete("#{Rails.root}/app/models/concerns")

6.5 在自动加载路径中包含app

一些项目希望像app/api/base.rb这样的文件定义API::Base,并将app添加到自动加载路径中以实现这一目的。

由于Rails会自动将app的所有子目录(有几个例外)添加到自动加载路径中,因此我们有了另一种情况,其中存在嵌套的根目录,类似于app/models/concerns的情况。然而,这种设置不再起作用。

但是,您可以保持该结构,只需在初始化程序中从自动加载路径中删除app/api

# config/initializers/zeitwerk.rb
ActiveSupport::Dependencies.
  autoload_paths.
  delete("#{Rails.root}/app/api")

请注意,没有要自动加载/急加载的文件的子目录。例如,如果应用程序具有用于ActiveAdmin的资源的app/admin,则需要忽略它们。对于assets和其他类似的目录也是如此:

# config/initializers/zeitwerk.rb
Rails.autoloaders.main.ignore(
  "app/admin",
  "app/assets",
  "app/javascripts",
  "app/views"
)

如果没有进行这样的配置,应用程序将急加载这些目录。会因为其文件没有定义常量而出错,例如,会定义一个Views模块,这是一个不需要的副作用。

正如您所看到的,将app包含在自动加载路径中在技术上是可能的,但有点棘手。

6.6 自动加载的常量和显式命名空间

如果一个命名空间在文件中被定义,就像这里的Hotel一样: app/models/hotel.rb # 定义Hotel。 app/models/hotel/pricing.rb # 定义Hotel::Pricing。

必须使用classmodule关键字来设置Hotel常量。例如:

class Hotel
end

是正确的。

Hotel = Class.new

或者

Hotel = Struct.new

这样的替代方法是不可行的,子对象如Hotel::Pricing将无法找到。

这个限制只适用于显式的命名空间。没有定义命名空间的类和模块可以使用这些习惯用法来定义。

6.7 一个文件,一个常量(在同一个顶级命名空间)

classic模式下,你可以在同一个顶级命名空间下定义多个常量并进行重新加载。例如,给定以下代码:

# app/models/foo.rb

class Foo
end

class Bar
end

虽然Bar无法自动加载,但自动加载Foo会将Bar标记为已自动加载。

zeitwerk模式下不是这样的,你需要将Bar移动到它自己的文件bar.rb中。一个文件,一个顶级常量。

这只影响与上面示例中相同顶级命名空间的常量。内部类和模块没有问题。例如,考虑以下代码:

# app/models/foo.rb

class Foo
  class InnerClass
  end
end

如果应用程序重新加载Foo,它也会重新加载Foo::InnerClass

6.8 config.autoload_paths中的通配符

注意配置中使用通配符的情况,例如:

config.autoload_paths += Dir["#{config.root}/extras/**/"]

config.autoload_paths的每个元素都应该表示顶级命名空间(Object)。这样是行不通的。

要解决这个问题,只需删除通配符:

config.autoload_paths << "#{config.root}/extras"

6.9 对引擎中的类和模块进行装饰

如果你的应用程序对引擎中的类或模块进行装饰,那么很可能在某个地方做了类似以下的操作:

config.to_prepare do
  Dir.glob("#{Rails.root}/app/overrides/**/*_override.rb").sort.each do |override|
    require_dependency override
  end
end

这需要进行更新:你需要告诉main自动加载器忽略覆盖目录,并且需要使用load来加载它们。类似以下代码:

overrides = "#{Rails.root}/app/overrides"
Rails.autoloaders.main.ignore(overrides)
config.to_prepare do
  Dir.glob("#{overrides}/**/*_override.rb").sort.each do |override|
    load override
  end
end

6.10 before_remove_const

Rails 3.1 添加了对before_remove_const回调的支持,如果一个类或模块响应了这个方法并且即将重新加载,就会调用该回调。这个回调一直没有被记录下来,你的代码很可能不会使用它。

然而,如果确实使用了它,你可以将以下代码重写为:

class Country < ActiveRecord::Base
  def self.before_remove_const
    expire_redis_cache
  end
end

如下所示:

# config/initializers/country.rb
if Rails.application.config.reloading_enabled?
  Rails.autoloaders.main.on_unload("Country") do |klass, _abspath|
    klass.expire_redis_cache
  end
end

6.11 Spring和test环境

如果有代码发生更改,Spring会重新加载应用程序代码。在test环境中,你需要启用重新加载才能正常工作:

# config/environments/test.rb
config.cache_classes = false

或者,从Rails 7.1开始:

# config/environments/test.rb
config.enable_reloading = true

否则,你会得到以下错误:

reloading is disabled because config.cache_classes is true

或者

reloading is disabled because config.enable_reloading is false

这不会对性能产生影响。

6.12 Bootsnap

请确保至少依赖于Bootsnap 1.4.4。

7 在测试套件中检查Zeitwerk的兼容性

在迁移过程中,zeitwerk:check任务非常方便。一旦项目符合要求,建议自动化进行此检查。为了做到这一点,只需急切加载应用程序,这正是zeitwerk:check所做的。

7.1 持续集成

如果项目已经使用了持续集成,建议在其中运行测试套件时急切加载应用程序。如果由于某种原因无法急切加载应用程序,你肯定希望在持续集成中发现,而不是在生产环境中发现,对吧?

持续集成通常会设置一些环境变量来指示测试套件正在运行。例如,可以使用CI

# config/environments/test.rb
config.eager_load = ENV["CI"].present?

从Rails 7开始,默认情况下,新生成的应用程序已经配置成这样。

7.2 纯净的测试套件

如果项目没有持续集成,你仍然可以通过调用Rails.application.eager_load!来在测试套件中急切加载:

7.2.1 Minitest

require "test_helper"

class ZeitwerkComplianceTest < ActiveSupport::TestCase
  test "eager loads all files without errors" do
    assert_nothing_raised { Rails.application.eager_load! }
  end
end

7.2.2 RSpec

require "rails_helper"

RSpec.describe "Zeitwerk compliance" do
  it "eager loads all files without errors" do
    expect { Rails.application.eager_load! }.not_to raise_error
  end
end

8 删除所有require调用

根据我的经验,项目通常不会这样做。但我见过几个项目这样做,也听说过其他几个项目这样做。 在Rails应用程序中,您只能使用require来加载来自lib或第三方(如gem依赖项或标准库)的代码。绝不能使用require来加载可自动加载的应用程序代码。在classic模式中,可以在此处查看为什么这是一个坏主意。

require "nokogiri" # 正确
require "net/http" # 正确
require "user"     # 错误,请删除此行(假设是app/models/user.rb)

请删除所有此类require调用。

9 可以利用的新功能

9.1 删除require_dependency调用

已经使用Zeitwerk消除了所有已知的require_dependency用例。您应该在项目中使用grep命令并删除它们。

如果您的应用程序使用单表继承,请参阅自动加载和重新加载常量(Zeitwerk模式)指南中的单表继承部分

9.2 类和模块定义中现在可以使用限定名称

现在,您可以在类和模块定义中稳健地使用常量路径:

# 此类主体中的自动加载与Ruby语义匹配。
class Admin::UsersController < ApplicationController
  # ...
end

需要注意的是,根据执行顺序,经典自动加载程序有时可以自动加载Foo::Wadus

class Foo::Bar
  Wadus
end

这不符合Ruby语义,因为Foo不在嵌套中,并且在zeitwerk模式下根本不起作用。如果遇到这种特殊情况,您可以使用限定名称Foo::Wadus

class Foo::Bar
  Foo::Wadus
end

或者将Foo添加到嵌套中:

module Foo
  class Bar
    Wadus
  end
end

9.3 线程安全性无处不在

classic模式下,常量自动加载不是线程安全的,尽管Rails已经采取了锁定措施,例如使Web请求线程安全。

zeitwerk模式下,常量自动加载是线程安全的。例如,您现在可以在由runner命令执行的多线程脚本中自动加载。

9.4 预加载和自动加载一致

classic模式下,如果app/models/foo.rb定义了Bar,您将无法自动加载该文件,但是预加载将起作用,因为它会盲目地递归加载文件。如果您首先进行预加载测试,然后执行时自动加载可能会导致错误。

zeitwerk模式下,这两种加载模式是一致的,它们在相同的文件中失败和出错。

反馈

欢迎您帮助改进本指南的质量。

如果您发现任何拼写错误或事实错误,请贡献您的意见。 要开始,请阅读我们的 文档贡献 部分。

您还可能会发现不完整的内容或过时的内容。 请为主要内容添加任何缺失的文档。请先检查 Edge 指南,以验证问题是否已经修复或尚未修复。 请参阅 Ruby on Rails 指南准则 以了解样式和规范。

如果您发现需要修复但无法自行修复的问题,请 提交问题

最后但同样重要的是,欢迎您在 官方 Ruby on Rails 论坛 上讨论有关 Ruby on Rails 文档的任何问题。