v7.1.3.2
更多資訊請至 rubyonrails.org: 更多 Ruby on Rails

Classic 至 Zeitwerk HOWTO

本指南說明如何將 Rails 應用程式從 classic 遷移至 zeitwerk 模式。

閱讀本指南後,您將了解

1 什麼是 classiczeitwerk 模式?

從一開始到 Rails 5,Rails 使用 Active Support 中實作的自動載入器。此自動載入器稱為 classic,在 Rails 6.x 中仍然可用。Rails 7 不再包含此自動載入器。

從 Rails 6 開始,Rails 附帶一種新的、更好的自動載入方式,委派給 Zeitwerk 寶石。這是 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 或更低版本的應用程式

在執行 Rails 6.0 之前版本的應用程式中,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 # DELETE THIS LINE

如前所述,只需刪除覆寫,zeitwerk 模式就是預設值。

另一方面,如果應用程式載入舊的架構預設值,則需要明確啟用 zeitwerk 模式

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

4.3 執行 Rails 7 的應用程式

在 Rails 7 中,只有 zeitwerk 模式,您不需要執行任何操作即可啟用它。

確實,在 Rails 7 中,設定器 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!

根據應用程式設定,可能會產生其他輸出,但最後的「一切都很好!」是您要尋找的。

如果前一節說明的仔細檢查確定熱載入路徑之外必須有一些自訂自動載入路徑,任務會偵測並警告它們。但是,如果測試套件成功載入那些檔案,那就沒問題了。

現在,如果有任何檔案未定義預期的常數,任務會告訴您。它一次只處理一個檔案,因為如果它繼續進行,載入一個檔案失敗可能會連帶導致其他與我們要執行的檢查無關的失敗,而錯誤報告會令人困惑。

如果報告了一個常數,請修正那個特定的常數,然後再次執行任務。重複執行,直到您看到「一切都很好!」。

例如

$ 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。這就是錯誤訊息所說的。

要修正這個問題很簡單,您只需要告訴變形器這個縮寫

# 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

你可以使用 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")

小心沒有檔案要自動載入/急切載入的子目錄。例如,如果應用程式有 app/admin,其中有 ActiveAdmin 的資源,您需要忽略它們。assets 和相關檔案也是如此

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

沒有該組態,應用程式會急切載入這些樹狀結構。會在 app/admin 發生錯誤,因為它的檔案沒有定義常數,而且會定義一個 Views 模組,例如,作為一個不必要的副作用。

如您所見,在自動載入路徑中包含 app 在技術上是可行的,但有點棘手。

6.6 自動載入的常數和明確的命名空間

如果命名空間定義在檔案中,如下面的 Hotel

app/models/hotel.rb         # Defines Hotel.
app/models/hotel/pricing.rb # Defines 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 中的 Glob

小心使用萬用字元組態,例如

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 中知道,而不是在生產環境中,對吧?

CI 通常會設定一些環境變數來表示測試套件在那裡執行。例如,它可能是 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 應用程式中,您專門使用 requirelib 或第三方載入程式碼,例如 gem 相依項或標準函式庫。切勿使用 require 載入可自動載入的應用程式程式碼。請參閱為何這在 classic 中已是不好的做法 在此處

require "nokogiri" # GOOD
require "net/http" # GOOD
require "user"     # BAD, DELETE THIS (assuming app/models/user.rb)

請刪除該類型的所有 require 呼叫。

9 您可利用的新功能

9.1 刪除 require_dependency 呼叫

Zeitwerk 已消除所有已知的 require_dependency 使用案例。您應 grep 專案並將它們刪除。

如果您的應用程式使用單一表格繼承,請參閱自動載入和重新載入常數 (Zeitwerk 模式) 指南的 單一表格繼承區段

9.2 現在可在類別和模組定義中使用限定名稱

您現在可以在類別和模組定義中穩健地使用常數路徑

# Autoloading in this class body matches Ruby semantics now.
class Admin::UsersController < ApplicationController
  # ...
end

需要注意的是,根據執行順序,傳統的自動載入器有時可以在

class Foo::Bar
  Wadus
end

自動載入 Foo::Wadus,這不符合 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模式中,兩種載入模式是一致的,它們會在相同的檔案中失敗和出錯。

回饋

我們鼓勵您協助提升本指南的品質。

如果您發現任何錯字或事實錯誤,請協助我們修正。首先,您可以閱讀我們的文件貢獻章節。

您也可能會發現不完整或過時的內容。請務必為 main 新增任何遺漏的文件。請務必先查看Edge Guides,以確認問題是否已在 main 分支中修正。查看Ruby on Rails 指南指南,了解風格和慣例。

如果您發現需要修正的內容,但無法自行修補,請開啟問題

最後,我們非常歡迎在官方 Ruby on Rails 論壇上討論任何有關 Ruby on Rails 文件的問題。