1 O que são os modos "classic" e "zeitwerk"?
Desde o início e até o Rails 5, o Rails usava um carregador automático implementado no Active Support. Esse carregador automático é conhecido como "classic" e ainda está disponível no Rails 6.x. O Rails 7 não inclui mais esse carregador automático.
A partir do Rails 6, o Rails vem com uma nova e melhor forma de carregamento automático, que delega para a gem Zeitwerk. Esse é o modo "zeitwerk". Por padrão, os aplicativos que carregam as configurações padrão do framework 6.0 e 6.1 rodam no modo "zeitwerk", e esse é o único modo disponível no Rails 7.
2 Por que mudar do modo "classic" para o modo "zeitwerk"?
O carregador automático "classic" tem sido extremamente útil, mas tinha uma série de problemas que tornavam o carregamento automático um pouco complicado e confuso às vezes. O Zeitwerk foi desenvolvido para resolver isso, entre outras motivações.
Ao atualizar para o Rails 6.x, é altamente recomendado mudar para o modo "zeitwerk", pois é um carregador automático melhor e o modo "classic" está obsoleto.
O Rails 7 encerra o período de transição e não inclui mais o modo "classic".
3 Estou com medo
Não fique :).
O Zeitwerk foi projetado para ser o mais compatível possível com o carregador automático clássico. Se você tem um aplicativo funcionando corretamente com o carregamento automático hoje, as chances são de que a mudança será fácil. Muitos projetos, grandes e pequenos, relataram transições muito tranquilas.
Este guia irá ajudá-lo a mudar o carregador automático com confiança.
Se, por qualquer motivo, você encontrar uma situação que não sabe como resolver, não hesite em abrir uma issue em rails/rails
e marcar @fxn
.
4 Como ativar o modo "zeitwerk"
4.1 Aplicativos rodando o Rails 5.x ou anterior
Em aplicativos que estão rodando uma versão anterior ao Rails 6.0, o modo "zeitwerk" não está disponível. Você precisa estar pelo menos no Rails 6.0.
4.2 Aplicativos rodando o Rails 6.x
Em aplicativos que estão rodando o Rails 6.x, existem dois cenários.
Se o aplicativo está carregando as configurações padrão do framework do Rails 6.0 ou 6.1 e está rodando no modo "classic", ele deve estar optando por isso manualmente. Você deve ter algo semelhante a isso:
# config/application.rb
config.load_defaults 6.0
config.autoloader = :classic # DELETE THIS LINE
Como observado, basta excluir a substituição, o modo "zeitwerk" é o padrão.
Por outro lado, se o aplicativo está carregando configurações antigas do framework, você precisa habilitar explicitamente o modo "zeitwerk":
# config/application.rb
config.load_defaults 5.2
config.autoloader = :zeitwerk
4.3 Aplicativos rodando o Rails 7
No Rails 7, só existe o modo "zeitwerk", você não precisa fazer nada para ativá-lo.
De fato, no Rails 7, o setter config.autoloader=
nem mesmo existe. Se config/application.rb
o utiliza, por favor, exclua a linha.
5 Como verificar se o aplicativo está rodando no modo "zeitwerk"?
Para verificar se o aplicativo está rodando no modo "zeitwerk", execute
bin/rails runner 'p Rails.autoloaders.zeitwerk_enabled?'
Se isso imprimir true
, o modo "zeitwerk" está ativado.
6 Meu aplicativo está em conformidade com as convenções do Zeitwerk?
6.1 config.eager_load_paths
O teste de conformidade é executado apenas para arquivos carregados antecipadamente. Portanto, para verificar a conformidade do Zeitwerk, é recomendável ter todos os caminhos de carregamento automático nos caminhos de carregamento antecipado.
Isso já é o caso por padrão, mas se o projeto tiver caminhos de carregamento automático personalizados configurados assim:
config.autoload_paths << "#{Rails.root}/extras"
eles não serão carregados antecipadamente e não serão verificados. Adicioná-los aos caminhos de carregamento antecipado é fácil:
config.autoload_paths << "#{Rails.root}/extras"
config.eager_load_paths << "#{Rails.root}/extras"
6.2 zeitwerk:check
Depois que o modo "zeitwerk" estiver ativado e a configuração dos caminhos de carregamento antecipado for verificada, execute:
bin/rails zeitwerk:check
Uma verificação bem-sucedida será parecida com esta:
% bin/rails zeitwerk:check
Aguarde, estou carregando o aplicativo antecipadamente.
Tudo está bom!
Pode haver saída adicional dependendo da configuração do aplicativo, mas o último "Tudo está bom!" é o que você está procurando. Se a verificação dupla explicada na seção anterior determinar que na verdade devem haver alguns caminhos de autoload personalizados fora dos caminhos de carregamento imediato, a tarefa irá detectar e avisar sobre eles. No entanto, se o conjunto de testes carregar esses arquivos com sucesso, está tudo bem.
Agora, se houver algum arquivo que não defina a constante esperada, a tarefa irá informá-lo. Ela faz isso um arquivo de cada vez, porque se ela continuar, a falha ao carregar um arquivo pode se propagar para outras falhas não relacionadas à verificação que queremos executar e o relatório de erro seria confuso.
Se houver uma constante relatada, corrija aquela em particular e execute a tarefa novamente. Repita até obter "Tudo está bem!".
Vamos tomar como exemplo:
% bin/rails zeitwerk:check
Aguarde, estou carregando a aplicação.
esperava que o arquivo app/models/vat.rb definisse a constante Vat
VAT é um imposto europeu. O arquivo app/models/vat.rb
define VAT
, mas o carregador automático espera Vat
, por quê?
6.3 Siglas
Este é o tipo mais comum de discrepância que você pode encontrar, tem a ver com siglas. Vamos entender por que recebemos essa mensagem de erro.
O carregador automático clássico é capaz de carregar automaticamente VAT
porque sua entrada é o nome da constante ausente, VAT
, invoca underscore
nele, o que resulta em vat
, e procura um arquivo chamado vat.rb
. Isso funciona.
A entrada do novo carregador automático é o sistema de arquivos. Dado o arquivo vat.rb
, o Zeitwerk invoca camelize
em vat
, o que resulta em Vat
, e espera que o arquivo defina a constante Vat
. Isso é o que a mensagem de erro diz.
Corrigir isso é fácil, você só precisa informar o inflector sobre essa sigla:
# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym "VAT"
end
Fazendo isso afeta como o Active Support inflete globalmente. Isso pode ser bom, mas se preferir, você também pode passar substituições para os inflectors usados pelos carregadores automáticos:
# config/initializers/zeitwerk.rb
Rails.autoloaders.main.inflector.inflect("vat" => "VAT")
Com essa opção, você tem mais controle, porque apenas arquivos chamados exatamente vat.rb
ou diretórios chamados exatamente vat
serão infletidos como VAT
. Um arquivo chamado vat_rules.rb
não é afetado por isso e pode definir VatRules
normalmente. Isso pode ser útil se o projeto tiver esse tipo de inconsistências de nomenclatura.
Com isso no lugar, a verificação é aprovada!
% bin/rails zeitwerk:check
Aguarde, estou carregando a aplicação.
Tudo está bem!
Uma vez que tudo está bem, é recomendado continuar validando o projeto no conjunto de testes. A seção Verificar a Conformidade do Zeitwerk no Conjunto de Testes explica como fazer isso.
6.4 Concerns
Você pode carregar automaticamente e carregar imediatamente a partir de uma estrutura padrão com subdiretórios concerns
como
app/models
app/models/concerns
Por padrão, app/models/concerns
pertence aos caminhos de autoload e, portanto, é assumido como um diretório raiz. Portanto, por padrão, app/models/concerns/foo.rb
deve definir Foo
, não Concerns::Foo
.
Se sua aplicação usa Concerns
como namespace, você tem duas opções:
- Remova o namespace
Concerns
dessas classes e módulos e atualize o código do cliente. - Deixe as coisas como estão, removendo
app/models/concerns
dos caminhos de autoload:
# config/initializers/zeitwerk.rb
ActiveSupport::Dependencies.
autoload_paths.
delete("#{Rails.root}/app/models/concerns")
6.5 Ter app
nos Caminhos de Autoload
Alguns projetos desejam que algo como app/api/base.rb
defina API::Base
e adicionam app
aos caminhos de autoload para conseguir isso.
Como o Rails adiciona automaticamente todos os subdiretórios de app
aos caminhos de autoload (com algumas exceções), temos outra situação em que existem diretórios raiz aninhados, semelhante ao que acontece com app/models/concerns
. Essa configuração não funciona mais como está.
No entanto, você pode manter essa estrutura, basta excluir app/api
dos caminhos de autoload em um inicializador:
# config/initializers/zeitwerk.rb
ActiveSupport::Dependencies.
autoload_paths.
delete("#{Rails.root}/app/api")
Cuidado com subdiretórios que não possuem arquivos para serem carregados automaticamente/carregados imediatamente. Por exemplo, se a aplicação tiver app/admin
com recursos para o ActiveAdmin, você precisa ignorá-los. O mesmo vale para assets
e similares:
# config/initializers/zeitwerk.rb
Rails.autoloaders.main.ignore(
"app/admin",
"app/assets",
"app/javascripts",
"app/views"
)
Sem essa configuração, a aplicação carregaria essas árvores imediatamente. Daria erro em app/admin
porque seus arquivos não definem constantes e definiria um módulo Views
, por exemplo, como um efeito colateral indesejado.
Como você pode ver, ter app
nos caminhos de autoload é tecnicamente possível, mas um pouco complicado.
6.6 Constantes Carregadas Automaticamente e Namespaces Explícitos
Se um namespace for definido em um arquivo, como Hotel
aqui:
app/models/hotel.rb # Define Hotel.
app/models/hotel/pricing.rb # Define Hotel::Pricing.
a constante Hotel
deve ser definida usando as palavras-chave class
ou module
. Por exemplo:
class Hotel
end
é bom.
Alternativas como
Hotel = Class.new
ou
Hotel = Struct.new
não funcionarão, objetos filhos como Hotel::Pricing
não serão encontrados.
Essa restrição se aplica apenas a namespaces explícitos. Classes e módulos que não definem um namespace podem ser definidos usando esses idiomas.
6.7 Um Arquivo, Uma Constante (no Mesmo Nível Superior)
No modo classic
, tecnicamente você poderia definir várias constantes no mesmo nível superior e todas serem recarregadas. Por exemplo, dado
# app/models/foo.rb
class Foo
end
class Bar
end
enquanto Bar
não poderia ser carregado automaticamente, carregar Foo
marcaria Bar
como carregado automaticamente também.
Isso não acontece no modo zeitwerk
, você precisa mover Bar
para seu próprio arquivo bar.rb
. Um arquivo, uma constante no nível superior.
Isso afeta apenas as constantes no mesmo nível superior como no exemplo acima. Classes e módulos internos estão bem. Por exemplo, considere
# app/models/foo.rb
class Foo
class InnerClass
end
end
Se a aplicação recarregar Foo
, também recarregará Foo::InnerClass
.
6.8 Globs em config.autoload_paths
Cuidado com configurações que usam curingas como
config.autoload_paths += Dir["#{config.root}/extras/**/"]
Cada elemento de config.autoload_paths
deve representar o namespace de nível superior (Object
). Isso não funcionará.
Para corrigir isso, basta remover os curingas:
config.autoload_paths << "#{config.root}/extras"
6.9 Decorando Classes e Módulos de Engines
Se sua aplicação decora classes ou módulos de uma engine, é provável que esteja fazendo algo assim em algum lugar:
config.to_prepare do
Dir.glob("#{Rails.root}/app/overrides/**/*_override.rb").sort.each do |override|
require_dependency override
end
end
Isso precisa ser atualizado: você precisa informar ao autoloader main
para ignorar o diretório com os overrides e precisa carregá-los com load
. Algo assim:
overrides = "#{Rails.root}/app/overrides"
Rails.autoloaders.main.ignore(overrides)
config.to_prepare do
Dir.glob("#{overrides}/**/*_override.rb").sort.each do |override|
load override
end
end
6.10 before_remove_const
O Rails 3.1 adicionou suporte a um callback chamado before_remove_const
que era invocado se uma classe ou módulo respondesse a esse método e estivesse prestes a ser recarregado. Esse callback permaneceu sem documentação e é improvável que seu código o utilize.
No entanto, caso o utilize, você pode reescrever algo como
class Country < ActiveRecord::Base
def self.before_remove_const
expire_redis_cache
end
end
como
# 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
6.11 Spring e o Ambiente test
O Spring recarrega o código da aplicação se algo mudar. No ambiente test
, você precisa habilitar a recarga para que isso funcione:
# config/environments/test.rb
config.cache_classes = false
ou, a partir do Rails 7.1:
# config/environments/test.rb
config.enable_reloading = true
Caso contrário, você verá:
reloading is disabled because config.cache_classes is true
ou
reloading is disabled because config.enable_reloading is false
Isso não tem penalidade de desempenho.
6.12 Bootsnap
Certifique-se de depender pelo menos do Bootsnap 1.4.4.
7 Verifique a Conformidade do Zeitwerk no Conjunto de Testes
A tarefa zeitwerk:check
é útil durante a migração. Uma vez que o projeto esteja em conformidade, é recomendado automatizar essa verificação. Para fazer isso, é suficiente carregar a aplicação de forma ansiosa, que é exatamente o que zeitwerk:check
faz.
7.1 Integração Contínua
Se o projeto tiver integração contínua, é uma boa ideia carregar a aplicação de forma ansiosa quando o conjunto de testes for executado lá. Se a aplicação não puder ser carregada de forma ansiosa por qualquer motivo, é melhor saber na integração contínua do que na produção, certo?
As integrações contínuas geralmente definem alguma variável de ambiente para indicar que o conjunto de testes está sendo executado lá. Por exemplo, poderia ser CI
:
# config/environments/test.rb
config.eager_load = ENV["CI"].present?
A partir do Rails 7, as aplicações recém-geradas são configuradas dessa maneira por padrão.
7.2 Conjuntos de Testes Simples
Se o projeto não tiver integração contínua, você ainda pode carregar de forma ansiosa no conjunto de testes chamando Rails.application.eager_load!
:
7.2.1 Minitest
require "test_helper"
class ZeitwerkComplianceTest < ActiveSupport::TestCase
test "carrega todos os arquivos sem erros" do
assert_nothing_raised { Rails.application.eager_load! }
end
end
7.2.2 RSpec
require "rails_helper"
RSpec.describe "Conformidade do Zeitwerk" do
it "carrega todos os arquivos sem erros" do
expect { Rails.application.eager_load! }.not_to raise_error
end
end
8 Exclua quaisquer Chamadas require
Em minha experiência, projetos geralmente não fazem isso. Mas já vi alguns e ouvi falar de alguns outros.
Em um aplicativo Rails, você usa require
exclusivamente para carregar código de lib
ou de terceiros, como dependências de gemas ou da biblioteca padrão. Nunca carregue código de aplicativo autoloadable com require
. Veja por que isso é uma má ideia no modo classic
aqui.
require "nokogiri" # BOM
require "net/http" # BOM
require "user" # RUIM, DELETE ISSO (supondo que seja app/models/user.rb)
Por favor, exclua todas as chamadas de require
desse tipo.
9 Novos recursos que você pode aproveitar
9.1 Excluir chamadas de require_dependency
Todos os casos conhecidos de require_dependency
foram eliminados com o Zeitwerk. Você deve procurar no projeto e excluí-los.
Se o seu aplicativo usa Herança de Tabela Única, consulte a seção Herança de Tabela Única do guia Autoloading and Reloading Constants (Modo Zeitwerk).
9.2 Nomes qualificados em definições de classe e módulo agora são possíveis
Agora você pode usar caminhos de constantes de forma robusta em definições de classe e módulo:
# O carregamento automático neste corpo da classe agora corresponde à semântica do Ruby.
class Admin::UsersController < ApplicationController
# ...
end
Um ponto a ser observado é que, dependendo da ordem de execução, o carregador automático clássico às vezes podia carregar automaticamente Foo::Wadus
em
class Foo::Bar
Wadus
end
Isso não corresponde à semântica do Ruby porque Foo
não está no aninhamento e não funcionará no modo zeitwerk
. Se você encontrar esse caso especial, pode usar o nome qualificado Foo::Wadus
:
class Foo::Bar
Foo::Wadus
end
ou adicionar Foo
ao aninhamento:
module Foo
class Bar
Wadus
end
end
9.3 Segurança em threads em todos os lugares
No modo classic
, o carregamento automático de constantes não é seguro para threads, embora o Rails tenha bloqueios em vigor, por exemplo, para tornar as solicitações da web seguras para threads.
O carregamento automático de constantes é seguro para threads no modo zeitwerk
. Por exemplo, agora você pode carregar automaticamente em scripts multithread executados pelo comando runner
.
9.4 Carregamento antecipado e carregamento automático são consistentes
No modo classic
, se app/models/foo.rb
define Bar
, você não poderá carregar automaticamente esse arquivo, mas o carregamento antecipado funcionará porque carrega arquivos recursivamente às cegas. Isso pode ser uma fonte de erros se você testar as coisas primeiro com carregamento antecipado, a execução pode falhar mais tarde com o carregamento automático.
No modo zeitwerk
, ambos os modos de carregamento são consistentes, eles falham e geram erros nos mesmos arquivos.
Feedback
Você é incentivado a ajudar a melhorar a qualidade deste guia.
Por favor, contribua se encontrar algum erro de digitação ou factual. Para começar, você pode ler nossa contribuição à documentação seção.
Você também pode encontrar conteúdo incompleto ou desatualizado. Por favor, adicione qualquer documentação ausente para o principal. Certifique-se de verificar Guias Edge primeiro para verificar se os problemas já foram corrigidos ou não no branch principal. Verifique as Diretrizes dos Guias do Ruby on Rails para estilo e convenções.
Se por algum motivo você encontrar algo para corrigir, mas não puder corrigi-lo você mesmo, por favor abra uma issue.
E por último, mas não menos importante, qualquer tipo de discussão sobre a documentação do Ruby on Rails é muito bem-vinda no Fórum oficial do Ruby on Rails.