Это руководство документирует, как мигрировать приложение Rails с режима classic
на zeitwerk
.
После прочтения этого руководства вы узнаете:
classic
и zeitwerk
classic
в zeitwerk
zeitwerk
zeitwerk
classic
и zeitwerk
?С самого начала и до Rails 5, Rails использовал автоматический загрузчик, реализованный в Active Support. Этот автозагрузчик, известный как classic
, все еще доступен в Rails 6.x. Rails 7 больше не включает этот автозагрузчик.
Начиная с Rails 6, Rails поставляется с новым и лучшим способом автозагрузки, делегирующим гему Zeitwerk. Это режим zeitwerk
. По умолчанию, приложения, загружающие умолчания для фреймворка 6.0 and 6.1, запускаются в режиме zeitwerk
, и в Rails 7 это единственный доступный режим.
classic
в zeitwerk
?Автозагрузчик classic
был чрезвычайно полезным, но имел ряд проблем, которые иногда делали автоматическую загрузку немного запутанной и непонятной. Zeitwerk был разработан, чтобы их решить, среди прочих мотивов.
При обновлении на Rails 6.x крайне рекомендуется переключиться на режим zeitwerk
, так как этот автозагрузчик лучше, а режим classic
устарел.
Rails 7 заканчивает переходный период и больше не включает режим classic
.
Не бойтесь :).
Zeitwerk был разработан, чтобы быть как можно более совместимым с классическим автозагрузчиком. Если у вас сейчас есть корректно работающая автозагрузка приложения, переключение, скорее всего, будет простым. Многие проекты, большие и малые, отчитались о реально гладком переходе.
Это руководство поможет вам уверенно изменить автоматический загрузчик.
Если, по какой-то причине, вы попали в ситуацию, которую не знаете как разрешить, не стесняйтесь открыть проблему в rails/rails
и поставить тег @fxn
.
zeitwerk
В приложениях на версиях Rails до 6.0, режим zeitwerk
недоступен. Нужен как минимум Rails 6.0.
В приложениях на Rails 6.x есть два сценария.
Если приложение загружает умолчания фреймворка Rails 6.0 или 6.1, и оно запускается в режиме classic
, это должно быть установлено вручную. Вам нужно что-то наподобие этого:
# config/application.rb
config.load_defaults 6.0
config.autoloader = :classic # УДАЛИТЕ ЭТУ СТРОЧКУ
Как отмечено, просто удалите переопределение, режим zeitwerk
установлен по умолчанию.
С другой стороны, если приложение загружает умолчания старого фреймворка, вам нужно включить режим zeitwerk
явно:
# config/application.rb
config.load_defaults 5.2
config.autoloader = :zeitwerk
В Rails 7 имеется только режим zeitwerk
, вам не нужно ничего делать, чтобы его включить.
На самом деле, в Rails 7 метод config.autoloader=
даже не существует. Если config/application.rb
его использует, пожалуйста удалите эту строчку.
zeitwerk
?Чтобы проверить, что приложение запускается в режиме zeitwerk
, выполните
bin/rails runner 'p Rails.autoloaders.zeitwerk_enabled?'
Если это выведет true
, режим zeitwerk
включен.
Тест на соответствие запускается для нетерпеливо загружаемых файлов. Следовательно, чтобы проверить на соответствие Zeitwerk, рекомендовано иметь все пути автозагрузки в пути нетерпеливой загрузки.
Это уже так по умолчанию, но если в проекте есть пользовательские пути автозагрузки, сконфигурированные наподобие:
config.autoload_paths << "#{Rails.root}/extras"
то они не будут нетерпеливо загружены и не будут проверены. Добавить их в пути нетерпеливой загрузки просто:
config.autoload_paths << "#{Rails.root}/extras"
config.eager_load_paths << "#{Rails.root}/extras"
Как только режим zeitwerk
включен и конфигурация путей нетерпеливой загрузки дважды проверена, запустите:
bin/rails zeitwerk:check
Успешная проверка выглядит так:
% bin/rails zeitwerk:check
Hold on, I am eager loading the application.
All is good!
Может быть дополнительный вывод в зависимости от конфигурации приложения, но итоговый "All is good!" это то, что вы должны увидеть.
Если двойная проверка, описанная в предыдущем разделе, определила, что фактически есть некоторые пользовательские пути автозагрузки вне путей нетерпеливой загрузки, задача их обнаружит и предупредит. Однако, если тестовый набор загружает эти файлы успешно, у вас все хорошо.
Теперь, если есть какой-то файл, который не определяет ожидаемую константу, задача вам подскажет. Она выводит один файл за раз, так как, если бы она продолжила, ошибка загрузки одного файла могла бы вызвать другие ошибки, не относящиеся к проверке, которую мы запустили, и отчет об ошибки мог бы быть запутанным.
Если выведена одна константа, почините ее и запустите задачу заново. Повторяйте, пока не получите "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
, почему?
Это наиболее распространенный тип несоответствия, нужно разобраться с аббревиатурами. Давайте поймем, почему мы получаем это сообщение об ошибке.
Классический автозагрузчик способен автоматически загрузить VAT
, так как у него на входе имя отсутствующей константы, VAT
, он вызывает underscore
на нем, что приводит к vat
, и ищет файл с именем vat.rb
. Это работает.
На входе у нового автозагрузчика файловая система. Взяв файл vat.rb
, Zeitwerk вызывает camelize
на vat
, что приводит к 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!
Как только All is good, рекомендуется оставить валидацию проекта в тестовом наборе. Раздел Проверка правильности Zeitwerk в тестах объясняет, как это сделать.
Можно автоматически и нетерпеливо загружать из стандартной структуры с поддиректориями concerns
наподобие
app/models
app/models/concerns
По умолчанию, app/models/concerns
принадлежит к путям автозагрузки, следовательно, подразумевается корневой директорией. Таким образом, по умолчанию app/models/concerns/foo.rb
должен определять Foo
, а не Concerns::Foo
.
Если ваше приложение использует Concerns
в качестве пространства имен, есть два варианта:
Concerns
из этих классов и модулей, и обновить клиентский код.
app/models/concerns
из путей автозагрузки:
# config/initializers/zeitwerk.rb
ActiveSupport::Dependencies.
autoload_paths.
delete("#{Rails.root}/app/models/concerns")
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
в путях автозагрузки технически возможно, но но немного запутано.
Если в файле определено пространство имен, как Hotel
тут:
app/models/hotel.rb # Определяет Hotel.
app/models/hotel/pricing.rb # Определяет Hotel::Pricing.
константа Hotel
должна быть установлена с помощью ключевых слов class
или module
. Например:
class Hotel
end
это правильно.
Альтернативы, такие как
Hotel = Class.new
или
Hotel = Struct.new
не будут работать, дочерние объекты, такие как Hotel::Pricing
не будут найдены.
Это ограничение применяется только для явных пространств имен. Классы и модули, не определяющие пространство имен, могут быть определены с помощью этих идиом.
В режиме 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
.
config.autoload_paths
Остерегайтесь конфигураций, в которых используются подстановочные знаки, например
config.autoload_paths += Dir["#{config.root}/extras/**/"]
Каждый элемент в config.autoload_paths
должен представлять пространство имен верхнего уровня (Object
). Это не будет работать.
Чтобы починить, просто уберите подстановочные знаки:
config.autoload_paths << "#{config.root}/extras"
Если ваше приложение декорирует классы или модули из engine, вероятно вы делаете где-то что-то вроде этого:
config.to_prepare do
Dir.glob("#{Rails.root}/app/overrides/**/*_override.rb").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").each do |override|
load override
end
end
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
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
В этом нет никакого ухудшения производительности.
Убедитесь, что зависите от как минимум Bootsnap 1.4.4.
Задача zeitwerk:check
удобна при миграции. Как только проект соответствует, рекомендуется автоматизировать эту проверку. Для этого достаточно нетерпеливо загрузить приложение, и, на самом деле, это единственное, что делает zeitwerk:check
.
Если ваш проект имеет непрерывную интеграцию, неплохо было бы нетерпеливо загрузить приложение при запуске тестов там. Если приложение не сможет быть нетерпеливо загружено по какой-то причине, лучше узнать это в CI, чем в production, не правда ли?
В CI обычно имеется некая установленная переменная среды для обозначения, что тесты выполняются там. К примеру, это может быть CI
:
# config/environments/test.rb
config.eager_load = ENV["CI"].present?
Начиная с Rails 7, новые приложения конфигурируются таким способом по умолчанию.
Если в вашем проекте нет непрерывной интеграции, вы все еще можете нетерпеливо загружать в тестах, вызывая Rails.application.eager_load!
:
require "test_helper"
class ZeitwerkComplianceTest < ActiveSupport::TestCase
test "eager loads all files without errors" do
assert_nothing_raised { Rails.application.eager_load! }
end
end
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
require
Проекты обычно так не делают. Но иногда так бывает.
В приложениях Rails require
используется эксклюзивно для загрузки кода из lib
или кода третьих сторон, например гемов или стандартной библиотеки. Никогда не загружайте автоматически загружаемый код приложения с помощью require
. Посмотрите, почему это уже было плохой идеей в classic
, тут.
require "nokogiri" # ХОРОШО
require "net/http" # ХОРОШО
require "user" # ПЛОХО, УДАЛИТЕ ЭТО (подразумеваем app/models/user.rb)
Пожалуйста, удалите любые вызовы require
этого типа.
require_dependency
Все известные случаи использования require_dependency
были устранены в Zeitwerk. Можно найти и удалить их в проекте.
Если ваше приложение использует наследование с единой таблицей, обратитесь к разделу по Single Table Inheritance руководства по автозагрузке и перезагрузке констант (режим Zeitwerk).
Теперь можно с уверенностью использовать пути констант в определениях модуля и класса:
# Автозагрузка в теле этого класса теперь соответствует семантике Ruby.
class Admin::UsersController < ApplicationController
# ...
end
Хитрость, о которой нужно было знать, в том, что, в зависимости от порядка выполнения, классический автозагрузчик иногда мог автоматически загрузить Foo::Wadus
в
class Foo::Bar
Wadus
end
Это не соответствует семантике Ruby, так как Foo
не во вложенности, и не будет работать в режиме zeitwerk
. Если вы обнаружите такой случай, можно использовать полное имя Foo::Wadus
:
class Foo::Bar
Foo::Wadus
end
или добавить Foo
во вложенность:
module Foo
class Bar
Wadus
end
end
В режиме classic
автозагрузка констант не является тредобезопасной, хотя в самом Rails есть блокировки, например, чтобы сделать веб-запросы тредобезопасными.
Автозагрузка констант в режиме zeitwerk
является тредобезопасной. Например, теперь можно автоматически загрузить в многотредовых скриптах, выполняемых с помощью команды runner
.
В режиме classic
если app/models/foo.rb
определяет Bar
, вы не сможете автоматически загрузить этот файл, но нетерпеливая загрузка будет работать, так как она загружает файлы рекурсивно вслепую. Это может быть источником ошибок, если вы сначала тестируете что-то нетерпеливо загрузив, а потом выполнение выдаст ошибку при автозагрузке.
В режиме zeitwerk
оба режима загрузки согласованы, они выдают ошибку в тех же самых файлах.