鼬~~~哩賀,我是寫程式的山姆老弟,今天是我們的開賽第三天!
今天要來看的是,RailsGuides 的 **Autoloading and Reloading Constants 篇,究竟 Rails 在開發的過程當中,為什麼不太需要使用到 require,去引用其他 ruby 檔呢?
Rails 以 MVC 為主軸,分為 Model-View-Controller,三者互相緊密合作,Model 負責從資料庫拿出資料、View 負責呈現資料、Controller 負責接收請求、將最終渲染後的網頁傳給客戶端,如果沒有 autoloading,Controller 在跟 Model 拿資料時,就會像下面這樣
# app/controllers/users_controller.rb
require '../models/user.rb' # 需要多這道程序,才能使用 User 這個 Class
class UsersController < ApplicationController
def index
@users = User.all
end
end
# app/models/user.rb
class User < ApplicationRecord
end
你可能會想說,這不是很正常嗎?其他語言也都是這樣做的啊?是很正常沒錯,但真的很麻煩!
一個正常的 Controller,用到兩到三個 Model 都是很正常的,可能也會用到其他自定義的 Class,那這樣引用名單就會很長,人工維護也可能有時候會漏掉一些,如果可以省掉這些麻煩,豈不美哉?
讓我想到之前還在寫 Android 的時候,每當我使用了其他 class,Android Studio 都會跳出紅驚嘆號,自動問我要不要引用某檔案進來,當時我還覺得挺方便的,但接觸到 Rails 之後,覺得還是 Rails 這樣不需要引用更好一點 XD,不過對於 Rails 新手來說,這些不需要引用的特性,會造成一種「不確定感」,讓之前剛學 Rails 的我覺得非常痛苦,我會不知道我能不能「這樣」使用,我「這樣」的使用方式,有沒有符合 Rails 的慣例 等等的疑問。
在 Rails 的設計哲學當中,「慣例優於設定」的目的,就是要讓開發者免於各種繁雜的設定手續,所以 Rails 跟開發者們共同維護著一種慣例:「你只要照著 Rails 訂好的規則走,你就不需要做這些繁雜的設定囉!」
如果是 Rails 7 的話,是透過 zeitwerk 這個 gem,提供 autoloader,在 rails 的 source code 的 rails/railties/lib/rails/autoloaders.rb
,可以看到 rails 一次 intialize 兩個 autoloader,一個是 main
、一個是 once
# rails/railties/lib/rails/autoloaders.rb
module Rails
class Autoloaders
...
attr_reader :main, :once
def initialize
require "zeitwerk"
@main = Zeitwerk::Loader.new
@main.tag = "rails.main"
@main.inflector = Inflector
@once = Zeitwerk::Loader.new
@once.tag = "rails.once"
@once.inflector = Inflector
end
...
end
end
並且在 rails/railties/lib/rails/application/finisher.rb
,有將相依路徑都加到 autoload 的名單中的痕跡
# rails/railties/lib/rails/application/finisher.rb
module Rails
class Application
module Finisher
include Initializable
initializer :setup_main_autoloader do
autoloader = Rails.autoloaders.main
...
ActiveSupport::Dependencies.autoload_paths.uniq.each do |path|
next unless File.directory?(path)
autoloader.push_dir(path)
autoloader.do_not_eager_load(path) unless ActiveSupport::Dependencies.eager_load?(path)
end
...
autoloader.setup
end
只要你符合以下 Rails 所定義的「慣例」,Rails 就會幫你把 ruby 檔案做 autoloading:
app/
底下所有資料夾底下的 rb
檔案(包括開發者另外新增的資料夾)
javascript
、assets
、views
三個資料夾不會被加進 autoloading檔案名 和 Class 名必須是 Camel 關係:
# 第一種替換規則的方法
# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym "HTML"
inflect.acronym "SSL"
end
# 優點是簡單好設定,缺點是這個設定是全域的
# 第二種替換規則的方法
# config/initializers/zeitwerk.rb
Rails.autoloaders.each do |autoloader|
autoloader.inflector.inflect(
"html_parser" => "HTMLParser",
"ssl_error" => "SSLError"
)
end
如果不在規則內的,可以在 config/application.rb
或 config/environments/你想加入規則的環境.rb
新增想要被 autoload 的路徑
module YourApplication
class Application < Rails::Application
...
config.autoload_paths << "#{Rails.root}/your_path"
end
end
通常發生在 Rails 已經啟動(boot)的狀態,會有一些狀況:
# config/initializers/api_gateway_setup.rb
ApiGateway.endpoint = "https://example.com" # DO NOT DO THIS
因為 initializer 只會在 rails 啟動的時候執行,在整個啟動的期間只會執行一次,所以如果你在 initializer 中寫了上面的初始化動作,那當 ApiGateway
被 reload 之後,ApiGateway.endpoint
因為沒有再次執行 initializer,導致 ApiGateway.endpoint
會是 nil
如果真的要在 initializer 中,做初始化某個值的動作,那可以用以下方式
# config/initializers/api_gateway_setup.rb
Rails.application.config.to_prepare do
ApiGateway.endpoint = "https://example.com" # CORRECT
end
被 Rails.application.config.to_prepare
包起來的部分,將會在每個 reload 之後也被執行
ps. 官方有警告說,to_prepare 中的程式 可能 會被執行兩次,所以裡面的程式必須是執行一次、兩次都無所謂的
例如 middleware 已指定 MyApp::Middleware::Foo
這個 module,但當 MyApp::Middleware::Foo
reload 之後,在 middleware 內的,卻還是舊版的 MyApp::Middleware::Foo
config.middleware.use MyApp::Middleware::Foo
所以如果開了 rails console 之後,檔案有改動的話,需要手動輸入 reload!
,來取得最新的 code
官方有解釋説,rails console 比較像是一個個獨立運作的 request,所以如果要在運作中的 console reload 的話,會造成前後不一致的狀況
Unloading 等於 Object.send(:remove_const, object_name)
,就是把這個 class 給移除
$ rails console
$ Object.send(:remove_const, 'User') # 把 User model 拿掉
=> User(id: integer, name: string, created_at: datetime, updated_at: datetime, ...)
$ User
NameError: uninitialized constant User
Did you mean? Users
$ bin/rails zeitwerk:check
Hold on, I am eager loading the application.
All is good!
故意弄出一個不符合規則的,我在 app/controllers 新增一個 abc_controller.rb
,按照慣例應該要叫做 AbcController,但我故意把它叫做 ABCController
$ bin/rails zeitwerk:check
Hold on, I am eager loading the application.
expected file app/controllers/abc_controller.rb to define constant AbcController
# config/application.rb
...
module YourApplicationName
class Application < Rails::Application
...
Rails.autoloaders.logger = Logger.new("#{Rails.root}/log/autoloading.log")
end
end
可以讓 autoloader 把 log 吐出來到 log/autoloading.log
這個檔案
D, [2022-08-29T12:22:00.540687 #4915] DEBUG -- : Zeitwerk@rails.main: autoload set for ActiveStorage::Filename, to be loaded from /Users/unclesam/.rvm/gems/ruby-3.0.0/gems/activestorage-6.1.6.1/app/models/active_storage/filename.rb
D, [2022-08-29T12:22:00.540701 #4915] DEBUG -- : Zeitwerk@rails.main: autoload set for ActiveStorage::Preview, to be loaded from /Users/unclesam/.rvm/gems/ruby-3.0.0/gems/activestorage-6.1.6.1/app/models/active_storage/preview.rb
D, [2022-08-29T12:22:00.540717 #4915] DEBUG -- : Zeitwerk@rails.main: autoload set for ActiveStorage::Record, to be loaded from /Users/unclesam/.rvm/gems/ruby-3.0.0/gems/activestorage-6.1.6.1/app/models/active_storage/record.rb
D, [2022-08-29T12:22:00.540732 #4915] DEBUG -- : Zeitwerk@rails.main: autoload set for ActiveStorage::Variant, to be loaded from /Users/unclesam/.rvm/gems/ruby-3.0.0/gems/activestorage-6.1.6.1/app/models/active_storage/variant.rb
D, [2022-08-29T12:22:00.540750 #4915] DEBUG -- : Zeitwerk@rails.main: autoload set for ActiveStorage::VariantRecord, to be loaded from /Users/unclesam/.rvm/gems/ruby-3.0.0/gems/activestorage-6.1.6.1/app/models/active_storage/variant_record.rb
D, [2022-08-29T12:22:00.540770 #4915] DEBUG -- : Zeitwerk@rails.main: autoload set for ActiveStorage::VariantWithRecord, to be loaded from /Users/unclesam/.rvm/gems/ruby-3.0.0/gems/activestorage-6.1.6.1/app/models/active_storage/variant_with_record.rb
D, [2022-08-29T12:22:00.540787 #4915] DEBUG -- : Zeitwerk@rails.main: autoload set for ActiveStorage::Variation, to be loaded from /Users/unclesam/.rvm/gems/ruby-3.0.0/gems/activestorage-6.1.6.1/app/models/active_storage/variation.rb
D, [2022-08-29T12:22:00.591097 #4915] DEBUG -- : Zeitwerk@rails.main: constant ApplicationRecord loaded from file /Users/unclesam/Projects/fullstack/online_course/app/models/application_record.rb
D, [2022-08-29T12:22:00.595789 #4915] DEBUG -- : Zeitwerk@rails.main: constant Oauthable loaded from file /Users/unclesam/Projects/fullstack/online_course/app/models/concerns/oauthable.rb
D, [2022-08-29T12:22:00.595938 #4915] DEBUG -- : Zeitwerk@rails.main: constant User loaded from file /Users/unclesam/Projects/fullstack/online_course/app/models/user.rb
Rails.autoloaders.main
Rails.autoloaders.once
ActiveSupport::Dependencies.autoload_paths
puts($LOAD_PATH)
Rails.application.config.after_initialize
Rails.application.config.to_prepare
恩… 老實說這篇我覺得不知道自己在看什麼XD,心中默默 OS:「反正我就是開發的時候,覺得怪怪的時候就直接重開 rails server 就對了麻!」,