Zeitwerk - это новый движок загрузчика кода, используемый в Rails 6. Он должен быть новым по умолчанию для всех проектов Rails 6+, заменяющих старый классический движок.

Если судить по его характеристикам, режим Zeitwerk кажется ведет себя почти так же, как предыдущий классический режим. Основы очень похожи: модели, контроллеры, помощники и т. Д. Будут по-прежнему автоматически загружаться, перезагружаться и загружаться с нетерпением, как и всегда.

Однако основное отличие заключается в реализации. Zeitwerk использует лучшую стратегию загрузки, которая решает все известные ошибки, которые несет в себе классический режим.

Также интересно то, что Zeitwerk стал жемчужиной, а это значит, что вы можете использовать его в других проектах, не связанных с рельсом.

Почему лучше?

Как и классический, Zeitwerk предоставляет функции автозагрузки, активной загрузки и перезагрузки.

Однако, в то время как классический режим полагается на обратный вызов const_missing для загрузки файлов, Zeitwerk использует собственный Module#autoload метод Ruby.

Давайте исследуем различия, чтобы понять, почему последнее лучше.

Как загружается классический режим

Алгоритм классического режима в основном зависит от двух факторов: autoload_paths и использования обратного вызова Ruby const_missing.

Как это работает?

Во время выполнения кода каждый раз, когда Ruby находит неизвестную постоянную ссылку, запускается обратный вызов const_missing. Rails отменяет обратный вызов const_missing по умолчанию из Ruby, который обычно просто вызывает NameError. Вместо этого он пытается загрузить файл, связанный с искомой константой.

Вот когда в игру вступает autoload_paths. Rails просматривает список autoload_paths в поисках «змеиной» версии указанной константы и, если она существует, загружает / требует файл.

Если все работает хорошо, требуется файл, определяется константа и выполнение кода продолжается.

Это нормально работает, но имеет несколько ограничений.

Например, есть хорошо известная проблема, связанная с классическим режимом, известным как Когда константы не пропущены. По сути, он описывает, как вы можете легко ввести код, который зависит от порядка, в котором файлы загружаются автоматически для правильной работы.

Как так? Давайте посмотрим на быстрый пример. Учтите следующее:

# app/models/user.rb
class User < ApplicationRecord
end
# app/models/admin/user.rb
module Admin
  class User < ApplicationRecord
  end
end
# app/models/admin/user_manager.rb
module Admin
  class UserManager
    def self.all
      User.all # Want to load all admin users
    end
  end
end

В этом случае, если константа Admin::User уже была загружена во время вызова Admin::UserManager.all, она вернет Admin::User объектов.

Однако, если Admin::User еще не был загружен автоматически, а User был, Admin::UserManager.all вместо этого вернет User объектов!

Это, очевидно, крайне нежелательно, и источник проблемы исходит из дизайна: классический полагается на обратный вызов theconst_missing, и этот обратный вызов запускается только на последнем этапе алгоритма поиска. Таким образом, при построении вы не можете избежать решения этих проблем на уровне приложения.

Итак, как Zeitwerk это исправляет? Продолжить чтение…

Zeitwerk автозагрузка

Как и в классическом режиме, Zeitwerk также полагается на два фактора. Первый - это список autoload_paths (на самом деле они называются корневыми каталогами, но в контексте Rails они по-прежнему обозначаются как autoload_paths).

Второй фактор и основное отличие от классического заключается в том, что Zeitwerk полагается на собственный Module#autoload метод Ruby для запуска автозагрузки констант.

Чтобы понять это, давайте рассмотрим следующий пример. Допустим, у вас есть приложение Rails 6 со следующими моделями:

app/
  models/
    comment.rb
    post.rb

Когда проект загрузится, Rails вызовет Zeitwek#setup. Этот метод заботится о настройке автозагрузчиков для всех известных autoload_paths (хотя они еще не загружаются).

В этом примере каталог app/models/ включен в autoload_paths, поэтому Zeitwerk установит Module#autoload для каждой из констант, которые выводятся по именам файлов внутри этого каталога.

В этом случае Zeitwerk сделает вывод, что comment.rb и post.rb должны определять константы Comment и Post соответственно.

Итак, вот волшебство; Zeitwerk выполнит от вашего имени следующий код:

autoload :Comment, Rails.root.join('app/models/comment.rb')
autoload :Post, Rails.root.join('app/models/post.rb')

Это так просто! Но что делает вышеперечисленное? Давайте разберемся, как autoload вписывается в этот процесс.

Каждый раз, когда программа Ruby вычисляет константу, выполняются определенные шаги по порядку. Например, когда Ruby вычисляет константу Post:

  • Сначала Ruby проверяет, есть ли уже сохраненная ссылка на :Post в таблице символов. Если он есть, он возвращает указатель на определение класса.
  • Если не находит, Ruby проверяет, настроен ли autoload для :Post
  • Если автозагрузки нет, Ruby запускает const_missing обратный вызов.
  • Однако, если autoload действительно определен (как в нашем примере), Ruby requires путь, который был передан в качестве аргумента для вызова autoload (Rails.root.join('app/models/post.rb')), а затем ожидает, что требуемый файл определит Post

И вот так же автоматически загружается константа Post!

Это отлично работает и лучше, чем классический. Почему? Поскольку autoload - это встроенная функция в Ruby, поэтому вместо прослушивания const_missing и ручной загрузки материала (что может быть взломано) мы можем использовать метод, который обслуживает виртуальная машина, для решения той же самой цели!

Примечание: Zeitwerk полагается на соглашение, согласно которому каждый файл будет определять константу, названную по имени файла (это означает, что /comment.rb должен определять константуComment). К счастью, для обычного приложения на Rails это неудивительно.

Zeitwerk и Rails 6

Zeitwerk по умолчанию включен в Rails 6.

Если вы выполняете обновление, load_defaults "6.0" установит Zeitwerk в качестве автозагрузчика по умолчанию для вашего проекта, и вы получите его бесплатно.

Но если по какой-либо причине вы не хотите его использовать, вы всегда можете отказаться, установив config.autoloader = :classic в своем application.rb.

Использование драгоценных камней

Одним из самых больших преимуществ Zeitwerk является то, что он построен отдельно от Rails. Итак, если вы пишете или обслуживаете гем, вы можете легко добавить Zeitwerk в качестве зависимости, и вы сможете забыть о require операторах!

Вы можете прочитать актуальную документацию здесь, но в основном вам просто нужно сделать:

# lib/my_gem.rb (main file)

require "zeitwerk"
loader = Zeitwerk::Loader.for_gem
loader.setup # ready!

module MyGem
  # ...
end

loader.eager_load # optionally

Вот и все!

Ознакомьтесь с этим образцом различий от nanoc gem, когда они представили Zeitwerk в своем проекте: https://github.com/nanoc/nanoc/pull/1403/files и посмотрите на весь код, который они смогли удалить.

Для меня такой подход очень желателен, потому что ручное написание и поддержка requires подвержены ошибкам и ненадежны, если вы не будете делать это осторожно. С этим драгоценным камнем вы можете просто забыть о нем!

Спасибо Ксавье Нориа и всем участникам проекта Zeitwerk!