Más en rubyonrails.org: Más Ruby on Rails

NO LEA ESTE ARCHIVO EN GITHUB, LAS GUÍAS SE PUBLICAN EN https://railsguides.es/

Constantes de autocarga y recarga (Zeitwerk Mode)

1 Introduction

Esta guía documenta la carga automática en el modo zeitwerk, que es nuevo en Rails 6. Si en su lugar desea leer sobre el modo clásico, consulte Constantes de carga y recarga automática (modo clásico).

En un programa de Ruby normal, las dependencias deben cargarse a mano. Por ejemplo, el siguiente controlador usa las clases ApplicationController y Post, y normalmente necesitarías poner llamadas require para ellas:

# DO NOT DO THIS.
require "application_controller"
require "post"
# DO NOT DO THIS.

class PostsController < ApplicationController
  def index
    @posts = Post.all
  end
end

Este no es el caso de las aplicaciones Rails, donde las clases de aplicaciones y los módulos están disponibles en todas partes:

class PostsController < ApplicationController
  def index
    @posts = Post.all
  end
end

Las aplicaciones de Idiomatic Rails solo emiten llamadas require para cargar cosas desde su directorio lib, la biblioteca estándar de Ruby, gemas Ruby, etc. Es decir, cualquier cosa que no pertenezca a sus rutas de carga automática, explicadas a continuación.

2 Enabling Zeitwerk Mode

El modo de carga automática zeitwerk está habilitado de forma predeterminada en las aplicaciones de Rails 6 que se ejecutan en CRuby:

# config/application.rb
config.load_defaults 6.0 # enables zeitwerk mode in CRuby

En el modo zeitwerk, Rails usa Zeitwerk internamente para autocargar, recargar y cargar ansioso. Rails crea una instancia y configura una instancia dedicada de Zeitwerk que administra el proyecto.

No se configura Zeitwerk manualmente en una aplicación Rails. En su lugar, configura la aplicación utilizando los puntos de configuración portátiles que se explican en esta guía, y Rails lo traduce a Zeitwerk en su nombre.

3 Project Structure

En una aplicación Rails, los nombres de los archivos deben coincidir con las constantes que definen, y los directorios actúan como espacios de nombres.

Por ejemplo, el archivo app/helpers/users_helper.rb debe definir UsersHelper y el archivo app/controllers/admin/payments_controller.rb debe definir Admin::PaymentsController.

De forma predeterminada, Rails configura Zeitwerk para declinar los nombres de los archivos con String#camelize. Por ejemplo, espera que app/controllers/users_controller.rb defina el constante UsersController porque

"users_controller".camelize # => UsersController

La sección Personalización de inflexiones a continuación documenta las formas de anular este valor predeterminado.

Por favor, consulte la documentación de Zeitwerk para obtener más detalles.

4 Autoload paths

Nos referimos a la lista de directorios de aplicaciones cuyo contenido se va a cargar automáticamente como rutas de carga automática. Por ejemplo, app/models. Dichos directorios representan el espacio de nombres raíz: Object.

Las rutas de carga automática se denominan directorios raíz en la documentación de Zeitwerk, pero nos quedaremos con la "ruta de carga automática" en esta guía.

Dentro de una ruta de carga automática, los nombres de los archivos deben coincidir con las constantes que definen como se documenta aquí.

De forma predeterminada, las rutas de carga automática de una aplicación constan de todos los subdirectorios de app que existen cuando la aplicación arranca --- excepto para assets, javascript, views, --- más las rutas de carga automática de los motores podría depender.

Por ejemplo, si UsersHelper está implementado en app/helpers/users_helper.rb, el módulo se puede autocargar, no necesitas (y no debes escribir) una llamada require para ello:

$ bin/rails runner 'p UsersHelper'
UsersHelper

Las rutas de carga automática eligen automáticamente cualquier directorio personalizado en la app. Por ejemplo, si su aplicación tiene aplicación/presentadores, o aplicación/servicios, etc., se agregan a las rutas de carga automática.

La matriz de rutas de carga automática se puede ampliar mutando config.autoload_paths, en config/application.rb, pero hoy en día esto no se recomienda.

Por favor, no mute ActiveSupport::Dependencies.autoload_paths, la interfaz pública para cambiar las rutas de carga automática es config.autoload_paths.

5 $LOAD_PATH

Las rutas de carga automática se agregan a $LOAD_PATH de forma predeterminada. Sin embargo, Zeitwerk usa nombres de archivo absolutos internamente, y su aplicación no debería emitir llamadas require para archivos autocargables, por lo que esos directorios en realidad no son necesarios allí. Puede optar por no participar con esta bandera:

config.add_autoload_paths_to_load_path = false

Eso puede acelerar un poco las llamadas legítimas require, ya que hay menos búsquedas. Además, si su aplicación usa Bootsnap, eso evita que la biblioteca cree índices innecesarios y guarda la RAM que necesitarían.

6 Reloading

Rails recarga automáticamente clases y módulos si cambian los archivos de la aplicación.

Más precisamente, si el servidor web se está ejecutando y los archivos de la aplicación se han modificado, Rails descarga todas las constantes cargadas automáticamente justo antes de que se procese la siguiente solicitud. De esa manera, las clases de aplicación o los módulos utilizados durante esa solicitud se cargarán automáticamente, recogiendo así su implementación actual en el sistema de archivos.

La recarga se puede habilitar o deshabilitar. La configuración que controla este comportamiento es config.cache_classes, que es falsa de forma predeterminada en el modo dedevelop (recarga habilitada) y verdadera de forma predeterminada en el modo de production (recarga deshabilitada).

Rails detecta que los archivos han cambiado usando un monitor de archivos de eventos (predeterminado) o recorriendo las rutas de carga automática, dependiendo de config.file_watcher.

En una consola Rails no hay ningún observador de archivos activo independientemente del valor de config.cache_classes. Esto se debe a que, normalmente, sería confuso tener el código recargado en medio de una sesión de consola, de la misma manera que generalmente desea que una solicitud individual sea atendida por un conjunto consistente y sin cambios de clases y módulos de aplicación.

Sin embargo, puede forzar una recarga en la consola ejecutando reload!:

$ bin/rails c
Loading development environment (Rails 6.0.0)
irb(main):001:0> User.object_id
=> 70136277390120
irb(main):002:0> reload!
Reloading...
=> true
irb(main):003:0> User.object_id
=> 70136284426020

como puede ver, el objeto de clase almacenado en la constante User es diferente después de la recarga.

6.1 Reloading and Stale Objects

Es muy importante entender que Ruby no tiene una forma de recargar realmente clases y módulos en la memoria, y eso se refleja en todos los lugares donde ya se usan. Técnicamente, "descargar" la clase User significa eliminar la constante User a través de Object.send(:remove_const, "User").

Por lo tanto, el código que hace referencia a una clase o módulo recargable, pero que no se ejecuta de nuevo al recargar, se vuelve obsoleto. Veamos un ejemplo a continuación.

Consideremos este inicializador:

# config/initializers/configure_payment_gateway.rb
# DO NOT DO THIS.
$PAYMENT_GATEWAY = Rails.env.production? ? RealGateway : MockedGateway
# DO NOT DO THIS.

La idea sería usar $PAYMENT_GATEWAY en el código, y dejar que el inicializador lo establezca en la implementación real que depende del entorno.

En la recarga, MockedGateway se vuelve a cargar, pero $PAYMENT_GATEWAY no se actualiza porque los inicializadores solo se ejecutan al arrancar. Por lo tanto, no reflejará los cambios.

Hay varias formas de hacer esto de forma segura. Por ejemplo, la aplicación podría definir un método de clase PaymentGateway.impl cuya definición depende del entorno; o podría definir "PaymentGateway" para tener una clase principal o mixin que depende del entorno; o use el mismo truco de variable global, pero en una devolución de llamada del recargador, como se explica a continuación.

Veamos otras situaciones que involucran objetos obsoletos de clase o módulo.

Consulta esta sesión de la consola de Rails:

> joe = User.new
> reload!
> alice = User.new
> joe.class == alice.class
false

joe es una instancia de la clase User original. Cuando hay una recarga, la constante User se evalúa como una clase recargada diferente. alice es una instancia del actual, pero joe no lo es, su clase está obsoleta. Puede definir joe nuevamente, iniciar una subsesión de IRB o simplemente iniciar una nueva consola en lugar de llamar a reload!.

Otra situación en la que puede encontrar este problema es subclasificar clases recargables en un lugar que no se recarga:

# lib/vip_user.rb
class VipUser < User
end

si se vuelve a cargar User, ya que VipUser no lo es, la superclase de VipUser es el objeto de clase obsoleto original.

En pocas palabras: no guarde en caché clases o módulos recargables.

6.2 Autoloading when the application boots

Las aplicaciones pueden cargar automáticamente constantes durante el arranque mediante una devolución de llamada del cargador:

Rails.application.reloader.to_prepare do
  $PAYMENT_GATEWAY = Rails.env.production? ? RealGateway : MockedGateway
end

Ese bloque se ejecuta cuando se inicia la aplicación y cada vez que se vuelve a cargar el código.

Por razones históricas, esta devolución de llamada puede ejecutarse dos veces. El código que ejecuta debe ser idempotente.

7 Eager Loading

En entornos de producción, generalmente es mejor cargar todo el código de la aplicación cuando se inicia la aplicación. La carga ansiosa pone todo en la memoria listo para atender solicitudes de inmediato, y también es compatible con CoW.

La carga ansiosa está controlada por la bandera config.eager_load, que está habilitada por defecto en el modo producción.

El orden en el que se cargan los archivos no está definido.

si la constante Zeitwerk está definida, Rails invoca Zeitwerk::Loader.eager_load_all independientemente del modo de carga automática de la aplicación. Eso asegura que las dependencias administradas por Zeitwerk estén cargadas con entusiasmo.

8 Single Table Inheritance

La herencia de tabla única es una función que no funciona bien con la carga diferida. La razón es que su API generalmente necesita poder enumerar la jerarquía STI para que funcione correctamente, mientras que la carga diferida difiere la carga de clases hasta que se hace referencia a ellas. No puede enumerar lo que aún no ha hecho referencia.

En cierto sentido, las aplicaciones necesitan ansiosamente cargar las jerarquías de STI independientemente del modo de carga.

Por supuesto, si la aplicación se carga ansiosamente al arrancar, eso ya está logrado. Cuando no es así, en la práctica es suficiente instanciar los tipos existentes en la base de datos, lo que en los modos de desarrollo o prueba suele estar bien. Una forma de hacerlo es lanzar este módulo al directorio lib:

module StiPreload
  unless Rails.application.config.eager_load
    extend ActiveSupport::Concern

    included do
      cattr_accessor :preloaded, instance_accessor: false
    end

    class_methods do
      def descendants
        preload_sti unless preloaded
        super
      end

      # Constantizes all types present in the database. There might be more on
      # disk, but that does not matter in practice as far as the STI API is
      # concerned.
      #
      # Assumes store_full_sti_class is true, the default.
      def preload_sti
        types_in_db = \
          base_class.
            unscoped.
            select(inheritance_column).
            distinct.
            pluck(inheritance_column).
            compact

        types_in_db.each do |type|
          logger.debug("Preloading STI type #{type}")
          type.constantize
        end

        self.preloaded = true
      end
    end
  end
end

y luego inclúyalo en las clases raíz STI de su proyecto:

# app/models/shape.rb
require "sti_preload"

class Shape < ApplicationRecord
  include StiPreload # Only in the root class.
end

# app/models/polygon.rb
class Polygon < Shape
end

# app/models/triangle.rb
class Triangle < Polygon
end

9 Customizing Inflections

Por defecto, Rails usa String#camelize para saber qué constante debe definir un archivo o nombre de directorio dado. Por ejemplo, posts_controller.rb debería definir PostsController porque eso es lo que devuelve "posts_controller".camelize.

Podría darse el caso de que algún nombre de directorio o archivo en particular no se flexione como desea. Por ejemplo, se espera que html_parser.rb defina HtmlParser de forma predeterminada. ¿Qué pasa si prefiere que la clase sea HTMLParser? Hay algunas formas de personalizar esto.

La forma más sencilla es definir acrónimos en config/initializers/inflections.rb:

ActiveSupport::Inflector.inflections(:en) do |inflect|
  inflect.acronym "HTML"
  inflect.acronym "SSL"
end

Si lo hace, afectará a la forma en que Active Support se flexiona globalmente. Eso puede estar bien en algunas aplicaciones, pero también puede personalizar cómo camelizar nombres de base individuales independientemente de Active Support pasando una colección de anulaciones a los inflectores predeterminados:

# config/initializers/zeitwerk.rb
Rails.autoloaders.each do |autoloader|
  autoloader.inflector.inflect(
    "html_parser" => "HTMLParser",
    "ssl_error"   => "SSLError"
  )
end

Sin embargo, esa técnica todavía depende de String#camelize, porque eso es lo que los inflectores predeterminados usan como respaldo. Si en cambio prefiere no depender de las inflexiones de Active Support y tener un control absoluto sobre las inflexiones, configure los inflectores para que sean instancias de Zeitwerk::Inflector:

# config/initializers/zeitwerk.rb
Rails.autoloaders.each do |autoloader|
  autoloader.inflector = Zeitwerk::Inflector.new
  autoloader.inflector.inflect(
    "html_parser" => "HTMLParser",
    "ssl_error"   => "SSLError"
  )
end

No existe una configuración global que pueda afectar a dichas instancias, son deterministas.

Incluso puede definir un inflector personalizado para una flexibilidad total. Por favor, consulte la documentación de Zeitwerk para obtener más detalles.

10 Troubleshooting

La mejor forma de seguir lo que están haciendo los cargadores es inspeccionar su actividad.

La forma más sencilla de hacerlo es lanzar

Rails.autoloaders.log!

a config/application.rb después de cargar los valores predeterminados del marco. Eso imprimirá trazas en la salida estándar.

Si prefiere iniciar sesión en un archivo, configure esto en su lugar:

Rails.autoloaders.logger = Logger.new("#{Rails.root}/log/autoloading.log")

El registrador Rails todavía no está listo en config/application.rb, pero está en inicializadores:

# config/initializers/log_autoloaders.rb
Rails.autoloaders.logger = Rails.logger

11 Rails.autoloaders

Las instancias de Zeitwerk que administran su aplicación están disponibles en

Rails.autoloaders.main
Rails.autoloaders.once

El primero es el principal. Este último está allí principalmente por razones de compatibilidad con versiones anteriores, en caso de que la aplicación tenga algo en config.autoload_once_paths (esto no se recomienda hoy en día).

Puede comprobar si el modo zeitwerk está habilitado con

Rails.autoloaders.zeitwerk_enabled?

12 Opting Out

Las aplicaciones pueden cargar los valores predeterminados de Rails 6 y seguir usando el cargador automático clásico de esta manera:

# config/application.rb
config.load_defaults 6.0
config.autoloader = :classic

Eso puede ser útil si se actualiza a Rails 6 en diferentes fases, pero se desaconseja el modo clásico para nuevas aplicaciones.

El modo zeitwerk no está disponible en versiones de Rails anteriores a 6.0.

Comentarios Sobre el Contenido

Las guías de rieles se administran y publican en latinadeveloper/railsguides.es en GitHub.

Si lee esta guía y encuentra algún texto o código incorrecto que le interese, no dude en enviar una solicitud de extracción en el repositorio anterior. Consulte el archivo README en GitHub para saber cómo enviar una solicitud de extracción. Please contribute if you see any typos or factual errors.