v7.0.0
更多資訊請前往 rubyonrails.org: 更多在 Ruby on Rails

自動載入和重新載入常量

本指南記錄了在 zeitwerk 模式下自動載入和重新載入的工作原理。

閱讀本指南後,您將瞭解:

1 介紹

資訊。本指南記錄了 Rails 應用程式中的自動載入、重新載入和預先載入。

在普通的 Ruby 程式中,依賴需要手動載入。例如,以下 controller 使用類 ApplicationControllerPost,通常您需要為它們放置 require 呼叫:

 不要這樣做。
require "application_controller"
require "post"
 不要這樣做。

class PostsController < ApplicationController
  def index
    @posts = Post.all
  end
end

這不是 Rails 應用程式的情況,應用程式類和 modules 隨處可用:

class PostsController < ApplicationController
  def index
    @posts = Post.all
  end
end

慣用的 Rails 應用程式僅發出 require 呼叫以從其 lib 目錄、Ruby 標準庫、Ruby gems 等載入內容。也就是說,任何不屬於其自動載入路徑的內容,如下所述。

為了提供此功能,Rails 代表您管理了幾個 Zeitwerk 載入器。

2 專案結構

在 Rails 應用程式中,檔名必須與它們定義的常量匹配,目錄充當名稱空間。

例如,檔案 app/helpers/users_helper.rb 應定義 UsersHelper,檔案 app/controllers/admin/payments_controller.rb 應定義 Admin::PaymentsController

預設情況下,Rails 將 Zeitwerk 設定為使用 String#camelize 來改變檔名。例如,它期望 app/controllers/users_controller.rb 定義常量 UsersController 因為

"users_controller".camelize # => UsersController

下面的 Customizing Inflections 部分記錄了覆蓋此預設值的方法。

請檢視 Zeitwerk 文件 瞭解更多詳情。

3 config.autoload_paths

我們將其內容要自動載入和(可選)重新載入的應用程式目錄列表​​稱為自動載入路徑。例如,app/models。這樣的目錄代表根名稱空間:Object

資訊。自動載入路徑在 Zeitwerk 文件中稱為 root 目錄,但我們將在本指南中使用“自動載入路徑”。

在自動載入路徑中,檔名必須與它們定義的常量相匹配 此處

預設情況下,應用程式的自動載入路徑由應用程式啟動時存在的 app 的所有子目錄組成---除了 assetsjavascriptviews---加上它可能依賴的引擎的自動載入路徑。

例如,如果 UsersHelperapp/helpers/users_helper.rb 中實現,則 module 是可自動載入的,您不需要(也不應該編寫)對它的 require 呼叫:

$ bin/rails runner 'p UsersHelper'
UsersHelper

Rails 會自動將 app 下的自定義目錄新增到自動載入路徑中。例如,如果您的應用程式具有 app/presenters,則您無需設定任何內容即可自動載入演示者,它開箱即用。

預設自動載入路徑陣列可以透過在 config/application.rbconfig/environments/*.rb 中推送到 config.autoload_paths 來擴充套件。例如:

module MyApplication
  class Application < Rails::Application
    config.autoload_paths << "#{root}/extras"
  end
end

此外,引擎可以推入引擎類的主體和它們自己的 config/environments/*.rb

警告。請不要變異ActiveSupport::Dependencies.autoload_paths;更改自動載入路徑的公共介面是 config.autoload_paths

在應用程式啟動時,您不能在自動載入路徑中自動載入程式碼。特別是,直接在config/initializers/*.rb。請檢查 Autoloading when the application boots 下面的有效方法。

自動載入路徑由 Rails.autoloaders.main 自動載入器管理。

4 config.autoload_once_paths

您可能希望無需重新載入即可自動載入類和 modules。自動載入一次路徑儲存可以自動載入但不會重新載入的程式碼。

預設情況下,此集合為空,但您可以將其擴充套件到 config.autoload_once_paths。您可以在 config/application.rbconfig/environments/*.rb 中執行此操作。例如:

module MyApplication
  class Application < Rails::Application
    config.autoload_once_paths << "#{root}/app/serializers"
  end
end

此外,引擎可以推入引擎類的主體和它們自己的 config/environments/*.rb

資訊。如果 app/serializers 被推送到 config.autoload_once_paths,Rails 不再認為這是一個自動載入路徑,儘管它是 app 下的自定義目錄。此設定會覆蓋該規則。

這是用於類的 key 和快取在重新載入後仍然存在的位置的 modules,例如 Rails 框架本身。

例如,Active Job 序列化器儲存在 Active Job 中:

# config/initializers/custom_serializers.rb
Rails.application.config.active_job.custom_serializers << MoneySerializer

並且 Active Job 本身不會在重新載入時重新載入,只有自動載入路徑中的應用程式和引擎程式碼才會重新載入。

使 MoneySerializer 可重新載入會令人困惑,因為重新載入編輯過的版本不會影響儲存在 Active Job 中的類物件。事實上,如果 MoneySerializer 是可重新載入的,那麼從 Rails 7 開始,這樣的初始化器會引發一個 NameError

另一個用例是引擎裝飾框架類:

initializer "decorate ActionController::Base" do
  ActiveSupport.on_load(:action_controller_base) do
    include MyDecoration
  end
end

在那裡,在初始化程式執行時儲存在 MyDecoration 中的 module 物件成為 ActionController::Base 的祖先,並且重新載入 MyDecoration 沒有意義,它不會影響該祖先鏈。

來自自動載入一次路徑的類和 modules 可以在 config/initializers 中自動載入。因此,使用該設定可以正常工作:

# config/initializers/custom_serializers.rb
Rails.application.config.active_job.custom_serializers << MoneySerializer

資訊:從技術上講,您可以在 :bootstrap_hook 之後執行的任何初始化程式中自動載入由 once 自動載入器管理的類和 modules。

一旦路徑由 Rails.autoloaders.once 管理,則自動載入。

5 $LOAD_PATH

自動載入路徑預設新增到 $LOAD_PATH。但是,Zeitwerk 在內部使用絕對檔名,並且您的應用程式不應為可自動載入的檔案發出 require 呼叫,因此實際上不需要這些目錄。您可以選擇退出此標誌:

config.add_autoload_paths_to_load_path = false

由於查詢次數較少,這可能會加快合法的 require 呼叫速度。此外,如果您的應用程式使用 Bootsnap,則可以避免庫構建不必要的索引,並節省它們所需的 RAM。

6 重灌

如果自動載入路徑中的應用程式檔案發生更改,Rails 會自動重新載入類和 modules。

更準確地說,如果 Web 伺服器正在執行並且應用程式檔案已被修改,Rails 會在處理下一個請求之前解除安裝由 main 自動載入器管理的所有自動載入常量。這樣,在該請求期間使用的應用程式類或 modules 將再次自動載入,從而在檔案系統中獲取它們當前的實現。

可以啟用或禁用重新載入。控制此行為的設定是 config.cache_classes,在 development 模式下預設為 false(啟用重新載入),在 production 模式下預設為 true(禁用重新載入)。

預設情況下,Rails 使用事件檔案監視器來檢測檔案更改。它可以設定為透過遍歷自動載入路徑來檢測檔案更改。這由 config.file_watcher 設定控制。

在 Rails 控制檯中,無論 config.cache_classes 的 value 是什麼,都沒有檔案觀察器處於活動狀態。這是因為,通常情況下,在控制檯 session 中間重新載入程式碼會令人困惑。與單個請求類似,您通常希望控制檯 session 由一組一致的、不變的應用程式類和 modules 提供服務。

但是,您可以透過執行 reload! 在控制檯中強制重新載入:

irb(main):001:0> User.object_id
=> 70136277390120
irb(main):002:0> reload!
Reloading...
=> true
irb(main):003:0> User.object_id
=> 70136284426020

可以看到,過載後存放在User常量中的類物件是不同的。

6.1 重新載入和陳舊的物件

理解 Ruby 沒有辦法在記憶體中真正重新載入類和 modules 並且在它們已經使用的任何地方都反映這一點非常重要。從技術上講,“解除安裝” User 類意味著透過 Object.send(:remove_const, "User") 刪除 User 常量。

例如,檢視這個 Rails 控制檯 session:

irb> joe = User.new
irb> reload!
irb> alice = User.new
irb> joe.class == alice.class
=> false

joe 是原始 User 類的實例。當有重新載入時, User 常量然後評估為不同的、重新載入的類。 alice 是新載入的 User 的實例,但 joe 不是——他的類是陳舊的。你可以再次定義 joe,啟動一個 IRB subsession,或者只是啟動一個新的控制檯而不是呼叫 reload!

您可能會發現此問題的另一種情況是在未重新載入的位置對可重新載入的類進行子類化:

# lib/vip_user.rb
class VipUser < User
end

如果重新載入 User,由於 VipUser 不是,因此 VipUser 的超類是原始的陳舊類物件。

底線:不要快取可重新載入的類或 modules

7 應用程式啟動時自動載入

啟動時,應用程式可以從自動載入一次路徑自動載入,這些路徑由 once 自動載入器管理。請檢查上面的 config.autoload_once_paths 部分。

但是,您不能從由 main 自動載入器管理的自動載入路徑自動載入。這適用於 config/initializers 中的程式碼以及應用程式或引擎初始值設定項。

為什麼?初始化程式僅在應用程式啟動時執行一次。如果您重新啟動伺服器,它們會在新程序中再次執行,但重新載入不會重新啟動伺服器,並且初始化程式不會再次執行。讓我們看看兩個主要用例。

7.1 用例 1:在引導期間,載入可重新載入的程式碼

讓我們想象一下 ApiGateway 是一個來自 app/services 的可過載類,由 main 自動載入器管理,你需要在應用程式啟動時設定它的端點:

# config/initializers/api_gateway_setup.rb
ApiGateway.endpoint = "https://example.com" # DO NOT DO THIS

重新載入的 ApiGateway 將有一個 nil 端點,因為上面的程式碼不會再次執行。

您仍然可以在啟動期間進行設定,但您需要將它們包裝在一個 to_prepare 塊中,該塊在啟動時和每次重新載入後執行:

# config/initializers/api_gateway_setup.rb
Rails.application.config.to_prepare do
  ApiGateway.endpoint = "https://example.com" # CORRECT
end

由於歷史原因,這個 callback 可能會執行兩次。它執行的程式碼必須是冪等的。

7.2 用例 2:在引導期間,載入保持快取的程式碼

某些設定採用類或 module 物件,並將其儲存在不會重新載入的位置。

一個例子是中介軟體:

config.middleware.use MyApp::Middleware::Foo

重新載入時,中介軟體堆疊不受影響,因此,啟動時儲存在 MyApp::Middleware::Foo 中的任何物件都將保持陳舊狀態。

另一個例子是 Active Job 序列化器:

# config/initializers/custom_serializers.rb
Rails.application.config.active_job.custom_serializers << MoneySerializer

任何 MoneySerializer 在初始化期間評估的結果都會被推送到自定義序列化程式。如果這是可重新載入的,則初始物件仍將在 Active Job 內,不會反映您的更改。

另一個例子是透過包含 modules 來裝飾框架類的 railties 或引擎。例如, turbo-rails 以這種方式裝飾 ActiveRecord::Base

initializer "turbo.broadcastable" do
  ActiveSupport.on_load(:active_record) do
    include Turbo::Broadcastable
  end
end

這將一個 module 物件新增到 ActiveRecord::Base 的祖先鏈中。如果重新載入,Turbo::Broadcastable 中的更改將不起作用,祖先鏈仍將具有原始鏈。

推論:那些類或 modules 不能重新載入

在引導期間引用這些類或 modules 的最簡單方法是將它們定義在不屬於自動載入路徑的目錄中。例如,lib 是一個慣用的選擇。預設情況下它不屬於自動載入路徑,但它屬於 $LOAD_PATH。只需執行正常的 require 即可載入並完成。

如上所述,另一種選擇是在自動載入一次路徑和自動載入中使用定義它們的目錄。有關詳細資訊,請檢視 關於 config.autoload_once_paths 的部分

8 急切載入

在類似生產的環境中,通常最好在應用程式啟動時載入所有應用程式程式碼。 Eager loading 把所有東西都放在記憶體中,準備好立即處理請求,而且它也是 CoW 友好的。

預載入由標誌 config.eager_load 控制,該標誌在 production 模式下預設啟用。

檔案急切載入的順序是未定義的。

如果定義了 Zeitwerk 常量,則無論應用程式自動載入模式如何,Rails 都會呼叫 Zeitwerk::Loader.eager_load_all。這確保了 Zeitwerk 管理的依賴項是預先載入的。

9 單表繼承

Single Table Inheritance 是一個不能很好地與延遲載入配合使用的特性。原因是:它的 API 通常需要能夠列舉 STI 層次結構才能正常工作,而延遲載入會延遲載入類,直到它們被引用。您無法列舉尚未引用的內容。

從某種意義上說,無論載入模式如何,應用程式都需要預先載入 STI 層次結構。

當然,如果應用程式在啟動時立即載入,那已經完成了。如果沒有,在實踐中實例化資料庫中的現有型別就足夠了,這在開發或測試模式下通常很好。一種方法是在您的 lib 目錄中包含一個 STI 預載入 module:

module StiPreload
  unless Rails.application.config.eager_load
    extend ActiveSupport::Concern

    included do
      cattr_accessor :preloaded, instance_accessor: false
    end

    class_methods do
      def descendants
        preload_sti unless preloaded
        super
      end

      # Constantizes all types present in the database. There might be more on
      # disk, but that does not matter in practice as far as the STI API is
      # concerned.
      #
      # Assumes store_full_sti_class is true, the default.
      def preload_sti
        types_in_db = \
          base_class.
            unscoped.
            select(inheritance_column).
            distinct.
            pluck(inheritance_column).
            compact

        types_in_db.each do |type|
          logger.debug("Preloading STI type #{type}")
          type.constantize
        end

        self.preloaded = true
      end
    end
  end
end

然後將其包含在專案的 STI 根類中:

# app/models/shape.rb
require "sti_preload"

class Shape < ApplicationRecord
  include StiPreload # Only in the root class.
end
# app/models/polygon.rb
class Polygon < Shape
end
# app/models/triangle.rb
class Triangle < Polygon
end

10 自定義變化

預設情況下,Rails 使用 String#camelize 來知道給定的檔案或目錄名稱應該定義哪個常量。例如, posts_controller.rb 應該定義 PostsController 因為這是 "posts_controller".camelize 返回的內容。

可能是某些特定檔案或目錄名稱沒有按照您的意願變形。例如, html_parser.rb 預計預設定義 HtmlParser。如果你更喜歡 HTMLParser 這個類怎麼辦?有幾種方法可以自定義它。

最簡單的方法是在 config/initializers/inflections.rb 中定義首字母縮略詞:

ActiveSupport::Inflector.inflections(:en) do |inflect|
  inflect.acronym "HTML"
  inflect.acronym "SSL"
end

這樣做會影響 Active Support 如何全域性變化。這在某些應用程式中可能沒問題,但您也可以透過將覆蓋集合傳遞給預設變形器來自定義如何獨立於 Active Support 自定義單個基本名稱:

# config/initializers/zeitwerk.rb
Rails.autoloaders.each do |autoloader|
  autoloader.inflector.inflect(
    "html_parser" => "HTMLParser",
    "ssl_error"   => "SSLError"
  )
end

但是,該技術仍然取決於 String#camelize,因為這是預設變形器用作後備的。如果您不想完全不依賴 Active Support 變形並擁有對變形的絕對控制,請將變形器設定為 Zeitwerk::Inflector 的實例:

# config/initializers/zeitwerk.rb
Rails.autoloaders.each do |autoloader|
  autoloader.inflector = Zeitwerk::Inflector.new
  autoloader.inflector.inflect(
    "html_parser" => "HTMLParser",
    "ssl_error"   => "SSLError"
  )
end

沒有可以影響所述實例的全域性設定;它們是確定性的。

您甚至可以定義自定義變形器以獲得完全的靈活性。請檢視 Zeitwerk 文件 瞭解更多詳情。

11 自動載入和引擎

引擎在父應用程式的上下文中執行,它們的程式碼由父應用程式自動載入、重新載入和預先載入。如果應用程式執行在 zeitwerk 模式下,引擎程式碼以 zeitwerk 模式載入。如果應用程式執行在 classic 模式下,引擎程式碼以 classic 模式載入。

Rails 啟動時,引擎目錄被新增到自動載入路徑中,從自動載入器的 view 點來看,沒有差別。自動載入器的主要輸入是自動載入路徑,它們是屬於應用程式原始碼樹還是某個引擎原始碼樹是無關緊要的。

例如,這個應用程式使用 Devise

% bin/rails runner 'pp ActiveSupport::Dependencies.autoload_paths'
[".../app/controllers",
 ".../app/controllers/concerns",
 ".../app/helpers",
 ".../app/models",
 ".../app/models/concerns",
 ".../gems/devise-4.8.0/app/controllers",
 ".../gems/devise-4.8.0/app/helpers",
 ".../gems/devise-4.8.0/app/mailers"]

如果引擎控制其父應用程式的自動載入模式,則可以照常編寫引擎。

但是,如果引擎支援 Rails 6 或 Rails 6.1 並且不控制其父應用程式,則它必須準備好在 classiczeitwerk 模式下執行。需要考慮的事項:

  1. 如果 classic 模式需要一個 require_dependency 呼叫來確保在某個時刻載入一些常量,請寫下它。雖然 zeitwerk 不需要它,但它不會受到傷害,它也可以在 zeitwerk 模式下工作。

  2. classic模式下劃線常量名("User" -> "user.rb"),zeitwerk模式駱駝化檔名("user.rb" -> "User")。它們在大多數情況下是一致的,但如果像“HTMLParser”那樣有一系列連續的大寫字母,它們就不會。相容的最簡單方法是避免使用此類名稱。在這種情況下,最好選擇“HtmlParser”。

  3. classic模式下,檔案app/model/concerns/foo.rb允許定義FooConcerns::Foo。在 zeitwerk 模式下,只有一種選擇:必須定義 Foo。為了相容,定義Foo

12 故障排除

跟蹤裝載機正在做什麼的最好方法是檢查他們的活動。

最簡單的方法是包括

Rails.autoloaders.log!

在載入框架預設值後在 config/application.rb 中。這會將跟蹤列印到標準輸出。

如果您更喜歡記錄到檔案,請設定它:

Rails.autoloaders.logger = Logger.new("#{Rails.root}/log/autoloading.log")

config/application.rb 執行時,Rails 記錄器還不可用。如果您更喜歡使用 Rails 記錄器,請在初始化程式中設定此設定:

# config/initializers/log_autoloaders.rb
Rails.autoloaders.logger = Rails.logger

13 Rails.autoloaders

管理您的應用程式的 Zeitwerk 實例可在

Rails.autoloaders.main
Rails.autoloaders.once

謂詞

Rails.autoloaders.zeitwerk_enabled?

在 Rails 7 應用程式中仍然可用,並返回 true

回饋

我們鼓勵您幫助提高本指南的品質。

如果您發現任何拼寫錯誤或資訊錯誤,請提供回饋。 要開始回饋,您可以閱讀我們的 回饋 部分。

您還可能會發現不完整的內容或不是最新的內容。 請務必為 main 新增任何遺漏的文件。假設是 非穩定版指南(edge guides) 請先驗證問題是否已經在主分支上解決。 請前往 Ruby on Rails 指南寫作準則 查看寫作風格和慣例。

如果由於某種原因您發現要修復的內容但無法自行修補,請您 提出 issue

關於 Ruby on Rails 的任何類型的討論歡迎提供任何文件至 rubyonrails-docs 討論區