1 介紹
資訊。本指南記錄了 Rails 應用程式中的自動載入、重新載入和預先載入。
在普通的 Ruby 程式中,依賴需要手動載入。例如,以下 controller 使用類 ApplicationController
和 Post
,通常您需要為它們放置 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
的所有子目錄組成---除了 assets
、javascript
和 views
---加上它可能依賴的引擎的自動載入路徑。
例如,如果 UsersHelper
在 app/helpers/users_helper.rb
中實現,則 module 是可自動載入的,您不需要(也不應該編寫)對它的 require
呼叫:
$ bin/rails runner 'p UsersHelper'
UsersHelper
Rails 會自動將 app
下的自定義目錄新增到自動載入路徑中。例如,如果您的應用程式具有 app/presenters
,則您無需設定任何內容即可自動載入演示者,它開箱即用。
預設自動載入路徑陣列可以透過在 config/application.rb
或 config/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.rb
或 config/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 並且不控制其父應用程式,則它必須準備好在 classic
或 zeitwerk
模式下執行。需要考慮的事項:
如果
classic
模式需要一個require_dependency
呼叫來確保在某個時刻載入一些常量,請寫下它。雖然zeitwerk
不需要它,但它不會受到傷害,它也可以在zeitwerk
模式下工作。classic
模式下劃線常量名("User" -> "user.rb"),zeitwerk
模式駱駝化檔名("user.rb" -> "User")。它們在大多數情況下是一致的,但如果像“HTMLParser”那樣有一系列連續的大寫字母,它們就不會。相容的最簡單方法是避免使用此類名稱。在這種情況下,最好選擇“HtmlParser”。在
classic
模式下,檔案app/model/concerns/foo.rb
允許定義Foo
和Concerns::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 討論區。