Rails中升级到Zeitwerk的完整教程

525 阅读13分钟

Zeitwerk是与Rails 6集成的代码自动加载器和重新加载器。从Rails 7开始,它将是唯一的代码加载器选项。因此,升级到Zeitwerk将是让你的应用程序为下一版本的Rails做好准备的一个重要步骤。在这篇文章中,我们将讨论如何将你的Rails 6应用程序从经典模式升级到zeitwerk模式。

什么是Zeitwerk?

Zeitwerk是 "一个高效和线程安全的Ruby代码加载器"。它是由Xavier Noria创建的,允许任何Ruby项目自动加载代码,而不需要我们Ruby开发者所依赖的大量require 语句。

Zeitwerk库在2019年作为Rails 6的一部分被并入Rails,以取代其之前的codeloader。对于升级到Rails 6的旧应用程序,该框架仍然允许现有应用程序使用其 "经典代码 "加载器。然而,从Rails 7开始,Zeitwerk将是唯一的选择。

如果你已经在Rails中编程了一段时间,你会注意到你在应用目录中的代码会正确地自动加载。这样的情况已经有一段时间了,它使开发变得非常容易。让我们的类和模块在任何地方都可以使用--而不需要我们明确地添加require 语句或重新启动我们的开发服务器--有助于最大限度地减少开发时间和改善开发人员的体验。

这种 "魔力 "在很大程度上要归功于Rails内部打包的代码,它可以自动加载和重新加载你的源代码。

既然代码加载在Rails中没有被破坏,为什么Rails 6要用Zeitwerk取代自己的codeloader呢?要理解这一决定背后的理由,必须认识到代码加载器的作用。

应用程序需要在不同的环境中运行(例如,开发、测试和生产)。代码加载行为需要适应这些环境中的每一个,以满足这些环境的需要。

例如,在开发环境中,我们希望我们的代码加载器观察变化,这样我们就不需要在看到我们的更新之前重新启动我们的应用程序。

在测试环境中,我们可能只想有选择地加载我们正在测试的文件,以便我们的测试运行得更快。

最后,在生产环境中,我们希望我们的代码能一次性加载,这样我们的应用程序就能在内存中完全可用,以提高响应时间。

有了一个适当的、强大的代码加载器,我们可以满足这些不同的运行环境的需求。

常量名称查询在Rails中也是一个令人讨厌的问题。在Zeitwerk之前,嵌套的常量定义是这样定义的。

module Admin
  class UsersController < ApplicationController
    # ...
  end
end

在Zeitwerk之后,你可以选择像这样定义你的嵌套常量命名空间。

class Admin::UsersController < ApplicationController
  # ...
end

虽然这种差别看起来很小,但某些边缘情况使自动加载变得更加困难,特别是当东西被不按顺序加载时。因此,Rails选择了Zeitwerk,以便更优雅地处理代码加载,并简化其内部依赖关系。

与依赖于 "惯例而非配置 "的Rails类似,Zeitwerk假设你以某种组织方式定义你的类和模块名称。

如果我们遵循这些假设和规则,Zeitwerk将正确地加载文件,而不需要任何额外的require 语句。

# A hypothetical directory & subdirectory of ActiveRecord models
./app/models/website.rb           → Website
./app/models/website/setting.rb   → Website::Setting
./app/models/website/url.rb       → Website::Url
./app/models/website/url_settings → Website::UrlSettings

在这里,我们看到两个Zeitwerk特有的假设。

  • 如果你把一个文件放在一个子目录下,Zeitwerk 会假定它在它的父名字空间下(例如,setting.rb 中的类定义必须被定义为Website::Setting ,因为它被放在网站的一个子目录下)。
  • 常量定义遵循驼峰大写或Pascal大小写风格(例如:website/url_setting.rb翻译为Website::UrlSetting )。

在上面的代码例子中,如果你想把Website::Url 变成Website::URL ,你需要在自动加载器中定义一个自定义的转折点,后面会讨论

由于这些已经是Rails应用程序中的惯例,你的代码库应该已经在遵循它们。Zeitwerk也遵循这一经典Rails模式的惯例。

那么,有什么不同呢?

在经典的约定中,我们希望文件的文件名是其中定义的常量的下划线。

在Zeitwerk,我们希望常量是文件名的驼峰形式。

# Classic
MyModule -> my_module.rb

# Zeitwerk
my_module.rb -> MyModule

在执行一些Rails升级项目时,尤其是那些源自更早的Rails版本的项目,我们有时会看到代码库没有遵循这些惯例。

我们将在下文中讨论你可以用来修改这些规则以适应特殊情况的方法,以帮助缓解这些过渡。最终,你希望将你的应用程序的代码库与Rails的惯例重新结合起来,因为这些最佳实践是使Rails如此简单和有趣的原因之一。

将你的应用程序升级到Zeitwerk模式

要升级到Zeitwerk,你的应用程序需要运行Rails 6.0或6.1。

在撰写本文时,Rails 7.0还没有发布。对于Rails 7以后的版本,Zeitwerk是唯一可用的代码加载器。因此,如果你打算升级到Rails 7及以后的版本,你的应用程序必须能够在Zeitwerk模式下运行。

提示:如果你的代码库正在运行Rails 5.x或更低版本,建议你在升级到Rails 7之前先升级到Rails 6。这将简化你的升级过程,并尽量减少可能出现的问题。

如果你的应用还没有在Rails 6.0中运行,你首先需要升级它。需要这方面的帮助吗?请查看我们一系列的Rails升级博文,或者联系我们了解我们的升级服务。

当提到 "经典 "模式时,它只是指Rails的旧代码加载器。对于新的应用程序,Rails将默认为Zeitwerk模式,所以在这篇博文的其余部分,我们将假设你已经全部升级到Rails 6.0或6.1,并在经典模式下运行。

激活Zeitwerk

有两种方法可以激活Zeitwerk模式。

# ./config/application.rb

config.load_defaults 6.0 # Choosing Rails 6 defaults

config.autoloader = :zeitwerk # If you need to stick with the defaults of an older Rails version, you can choose to only activate Zeitwerk separate from the Rails 6.0 defaults

如果你刚刚将你的应用程序升级到Rails 6,并且由于其他原因不希望使用Rails 6.0的默认值,你可以通过只设置config.autoloader来选择性地激活Zeitwerk。

一旦这样做了,Zeitwerk模式就被激活了。

检查Zeitwerk的兼容性问题

接下来,我们需要检查我们应用程序的代码库是否有任何潜在的问题。

你可以通过输入以下命令来检查你的应用程序的现有代码加载状态。

bin/rails zeitwerk:check

如果一切看起来都很好,你会看到一个类似下面的信息。

> bin/rails zeitwerk:check

Hold on, I am eager loading the application.

WARNING: The following directories will only be checked if you configure
them to be eager loaded:

  /Projects/my-app/test/mailers/previews

You may verify them manually, or add them to config.eager_load_paths
in config/application.rb and run zeitwerk:check again.

Otherwise, all is good!

这个Rake任务试图做的是急于加载你的整个应用程序的代码并检查是否有错误。

如果你点击该Rake任务的源代码,你会看到它调用了Zeitwerk::Loader.eager_load_all

在内部,Zeitwerk跟踪所有它需要加载的目录,并对它们进行迭代。如果Zeitwerk检测到一个不规则的命名或定义的文件(即一个名为foo.rb 的文件,但在其中定义了一个名为Bar 的类),它将引发一个Zeitwerk::NameError

由于这个原因,如果你现有的代码偏离了这个预期,你会看到以下类型的错误。

> bin/rails zeitwerk:check

Hold on, I am eager loading the application.
expected file app/models/user.rb to define constant User

Zeitwerk为你提供关于它在哪里遇到错误以及如何修复的细节。

这通常涉及到以下的一个或多个方面。

  1. 重命名一个文件,和/或
  2. 重新命名一个类或模块(以及对它的所有引用),以符合驼峰规则或文件名的要求,或
  3. 添加自定义的转折词,以便 Zeitwerk 能正确地识别名称,或
  4. 配置Zeitwerk完全忽略该文件。

让我们来看看每一种类型的错误和它们的修复方法。

修复升级问题

1.和2.重命名文件和类定义

当应用程序从经典模式升级到zeitwerk模式时,最常见的错误类型之一是同时错误命名文件(和常量),并大量使用require 语句。

这些问题很容易解决,通常是通过纠正文件名或重新命名类或模块。

例如,假设你有一个名为Settings的类。不幸的是,当文件被创建时,它被命名为setting.rb。注意文件名中的单数命名。因为这个文件没有按照Rails的期望命名,所以即使在较早的Rails版本中,它也不会被自动加载。随之而来的是,有一个相应的require 语句,是为了处理这个文件的加载,通常是在application.rb 或初始化器中。

在这种情况下,你可以将文件重命名为settings.rb (复数),或者将类名改为Setting (单数),这样类名和文件名就会匹配。你还应该删除相应的require 语句。你不再需要它了,因为Zeitwerk会帮你处理它。

让我们来看看另一个涉及到命名间距的例子。例如,在一些项目中,你可能把文件放到一个子目录下,以便更好地组织你的代码。

比方说,你有以下的层次结构。

app/
├── models/
│   ├── user.rb
│   └── user/
│       └── password.rb

user.rb 文件中,类的名称是User 。然而,在password.rb 文件中,你可能把它简单地定义为Password 。在 Zeitwerk 下,你需要把它重命名为User::Password 或把文件从子目录移到 "根 "模型目录中。

你可能注意到,user.rb 可以定义User 而不是Model::User 。原因是app文件夹内任何直接的子目录都被识别为 "根 "目录,因为它是自动加载路径的一部分

3.为自动加载器使用自定义转折点

在某些情况下,你可能需要使用自定义转折点。让我们用另一个例子来证明这一点。

app/
├── models/
│   └── url.rb

在这种情况下,Zeitwerk会认为url.rb 文件应该被定义为Url ,而不是你想要的URL

为了解决这个问题,你需要让Zeitwerk知道,创建一个初始化器并添加一些自定义转折规则。

# ./config/initializers/zeitwerk.rb
Rails.autoloaders.each do |autoloader|
  autoloader.inflector.inflect(
    'url' => 'URL',
  )
end

4.配置 Zeitwerk 忽略某些文件

在某些情况下,你可能定义了自定义补丁或有不符合Zeitwerk假设的代码。

把它们整理到一个单独的目录中是个好主意。一旦你这样做了,你就可以把这个目录加入 Zeitwerk 的忽略路径。注意,如果你这样做,你需要像激活 Zeitwerk 之前那样,用require 语句单独加载这些文件。

要在Rails中把一个目录添加到Zeitwerk的忽略路径中,你可以使用以下代码。

# ./config/application.rb
Rails.autoloaders.main.ignore(Rails.root.join('app/models/your/directory'))

总结

一旦你修复了这些错误,再重新运行bin/rails zeitwerk:check ,以确保没有其他NameError 出现。重复这个过程,直到所有的检查都通过。保留一份任何常量或模块名称变化的清单。我建议你稍后进行一次代码库范围内的搜索,以确保任何引用的代码都已更新。

记住:bin/rails zeitwerk:check 只是确保Zeitwerk能够在应用程序启动时急于加载整个代码库。

如果你有一个测试套件,你可以现在就运行它,以确保测试通过和类/模块被正确引用。

解决Zeitwerk升级的问题

处理弃用问题

如果你以前使用过Rails 6,你可能偶尔会看到以下弃用信息。

DEPRECATION WARNING: Initialization autoloaded the constant SomeClassOrModule.

Being able to do this is deprecated. Autoloading during initialization is going
to be an error condition in future versions of Rails.

原因是在你的代码中,你在Rails的初始化过程中自动加载了一个常量或一个模块。因为Rails使用自动加载器来处理所有的代码加载,所以重要的是,常量定义不应该发生在初始化期间,因为这只应该由代码加载器处理。

为了解决这个问题,你可以用下面的方法来包装你的代码。

# Typically in an initializer or application.rb
Rails.application.config.to_prepare do
  # Your code here
end

你的代码必须能够以空闲的方式运行。换句话说,Rails很可能会多次运行你放在块中的代码,所以要确保你的代码逻辑支持这种可能性。

使用Zeitwerk的根命名方式

有时,你的传统应用程序可能会有一些命名间距的要求,这仅仅是由于它在旧的Rails版本中的配置方式。

让我们从一个例子开始。

app/
├── services/
│   └── authentication.rb
│   └── payment.rb
│   └── verification.rb

在上面的目录结构中,你可能将类定义为Services::Authentication,Services::Payment, 和Services::Verification 。相应地,在application.rb 或初始化器中,目录./app/services 被添加到应用程序的加载路径中。

虽然这在以前的经典模式下是可行的,但Zeitwerk会把服务的子目录解释为 "根 "目录。这意味着,为了让你保持目前的命名方式,你需要添加一些额外的配置,或者像这样把文件移到另一级子目录下。

app/
├── services/
│   └── services/
│       └── authentication.rb
│       └── payment.rb
│       └── verification.rb

当然,像这样嵌套在另一级子目录下可能会显得很混乱,特别是当代码库由较大的团队来完成时。

这里有一个解决这个问题的替代方案。

首先,从Zeitwerk的自动加载路径中删除子目录,然后配置Zeitwerk将应用目录解释为根目录。

# ./config/application.rb
config.eager_load_paths.delete("#{Rails.root}/app/services")
config.eager_load_paths.unshift("#{Rails.root}/app")

这将确保Zeitwerk以非根目录的方式解释./app/services ,因此放在./app/services 下的文件将被期望以正确的方式命名(例如:Services::YourClass )。

注意

应用程序无法启动

现在你已经升级了你的应用程序,是时候部署了。

偶尔,我遇到过这样的情况:即使测试套件通过了,但应用程序在部署时却无法启动。如果你已经为所有的实现代码添加了测试覆盖,这种情况应该不会发生。

通常情况下,出现这种问题的原因是你创建或重命名了一个文件,但其对应的类或模块名称定义不匹配。例如,假设你有几个相似的类,你决定复制这些文件,以便创建新的类。在复制的过程中,你忘记了重命名相应的类名。同样,由于你很匆忙,测试覆盖率很小,没有抓住这一点。

在开发和测试环境中,Zeitwerk只加载它需要加载的东西。换句话说,它并不急于加载整个应用程序。然而,在生产环境中,Zeitwerk的急切加载将由Rails调用。这是由环境特定文件中的config.eager_load配置来配置的。

由于测试环境没有完全加载你的文件,这个错误命名的类在测试或开发环境中都不会崩溃你的应用程序。

检查这个问题的一个好方法是重新运行bin/rails zeitwerk:check 。你也可以在你的测试套件中添加一个快速测试来自动检查这个问题。

最后一句话

Zeitwerk的自动加载器不仅简化了我们应用程序的代码加载,而且还简化了我们的源文件和类定义本身的组织。在升级旧的应用程序时,最好的方法是将你的代码命名与Rails的 "惯例高于配置 "的理念相一致。

在过去,我们的开发者利用了各种加载技术,这些技术往往是很脆的。当选择实现一些自定义命名(比如通过删除自动加载路径)或者简单地服从Rails框架的选择时,最好是服从框架的选择。

这可能意味着重新命名你现有的文件,并作为升级清理过程的一部分进行代码搜索和替换。这样做的长期好处是,它可以帮助代码维护者在一套类似的命名规则中工作,并使新的开发人员更容易上手。

祝你在升级过程中好运