問題說明

自我們專案中 rails 採用 zeitwerk 後,每次啟動時總會出現以下錯誤訊息

DEPRECATION WARNING: Initialization autoloaded the constant MyClass

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

Reloading does not reboot the application, and therefore code executed duringinitialization does not run again. So, if you reload TyrSetting, for example,the expected changes won't be reflected in that stale Class object.

This autoloaded constant has been unloaded.

起因於如果我們在 config/initializers 下,呼叫了某個類別(class)例如 User,而 User 位於 app/models/user.rb 是沒有在 rails 啟動(initialize)之前先進行 require 的話,就會顯示這個棄用警告(deprecation warning)。

這樣的措施用意在於:若要在 rails initialize 階段使用的 class,就必須事先 require。在開發環境(development)下,當我們修改程式碼後進行 reload! 時,在 rails initialize 階段跑的程式碼,即 config/initializers 下的所有程式,不會因為 reload! 而重跑一次,因此會導致類別和實例會有 reload! 前後版本不一致的問題,

舉例如下

# config/initializers/foo.rb
User.extend(FooModule)

當修改過 FooModule 內容時,User 並不會因為 reload! 而跟著修改,只能 restart rails app。


解決辦法

rails 提供了 ActiveSupport::Reloader 的載入框架來解決這類需求。

ActiveSupport::Reloader#to_prepare 內的程式碼,會在 reload! 時,也再重新執行一遍。

# config/initializers/foo.rb
ActiveSupport::Reloader#to_prepare do
  User.extend(FooModule)
end

如果是一些 Setting 之類需要在非常早期優先載入的 class

則可以在 config/application.rb 內先行 require

# config/application.rb

module MyApp
  class Application < Rails::Application
    require 'setting

注意事項

to_prepare 裡面的程式碼會是最後才執行,即先把 initializers 下的都執行完畢後才逐一執行 to_preare 內的。

# config/initializers/foo.rb
puts "1..."
ActiveSupport::Reloader#to_prepare do
  puts "2..."
end

# config/initializers/bar.rb
puts "3..."
ActiveSupport::Reloader#to_prepare do
  puts "4..."
end

輸出結果為

1...
3...
2...
4...

因此執行順序若會造成影響,則這裡需要特別注意。


非正規解法

在 initializers 內,如果先 require 的話,就可以讓 deprecation warning 消失。不過這樣就必須把該 class 內所用到的其他 class, module 需要一併 require。

# config/initializers/foo.rb
require 'application_record'
require 'user'
require 'foo_module'

User.extend(FooModule)

這樣做在未來新版本時,仍會導致 reload! 無效或噴錯,因此不太建議用這種方式解,除非你也不打算升級更新了 😏。