Structurer un plugin pour l'autoloading de Rails

De nombreux plugins incluent de nombreuses définitions de classes dans plugin.rb, ou utilisent require_relative pour charger des fichiers ruby. Cela fonctionne, mais présente certains inconvénients :

  1. Pas de rechargement automatique des modifications en développement. Toute modification nécessite un redémarrage complet du serveur.
  2. Obtenir les appels require dans le bon ordre peut être pénible.
  3. S’ils sont require en dehors du bloc after_initialize, d’autres classes/modules auto-chargés peuvent ne pas être disponibles.

Il existe une solution ! Les plugins peuvent s’appuyer sur le système d’auto-chargement standard de Rails. Pour les nouveaux plugins, tout ce dont vous avez besoin est défini dans le squelette de plugin. Ce sujet décrit comment adapter un plugin existant et étendre la configuration.

1. Définir un module et un Rails::Engine pour votre plugin

Dans plugin.rb, définissez un module pour votre plugin avec un PLUGIN_NAME unique, et ajoutez une ligne require_relative pour charger le fichier moteur que nous allons créer.

# nom : mon-nom-de-plugin
# ...

module ::MonModulePlugin
  PLUGIN_NAME = "mon-nom-de-plugin"
end

require_relative "lib/mon_module_plugin/engine"

Créez maintenant {plugin}/lib/mon_module_plugin/engine.rb :

module ::MonModulePlugin
  class Engine < ::Rails::Engine
    engine_name PLUGIN_NAME
    isolate_namespace MonModulePlugin
  end
end

Points importants à noter :

  1. Dans plugin.rb, vous devez inclure :: avant le nom de votre module pour le définir dans l’espace de noms racine (sinon, il serait défini sous Plugin::Instance).

  2. require_relative "lib/.../engine" doit se trouver à la racine du fichier plugin.rb, pas à l’intérieur d’un bloc after_initialize.

  3. Placer le moteur dans son propre fichier sous lib/ est important. Le définir directement dans le fichier plugin.rb ne fonctionnera pas. (Rails utilise la présence d’un répertoire lib/ pour déterminer la racine du moteur).

  4. Le chemin du fichier doit inclure le nom du module, conformément aux règles de Zeitwerk.

  5. Le engine_name est utilisé comme préfixe pour les tâches rake et toutes les routes définies par le moteur (:lien: docs rails).

  6. isolate_namespace aide à éviter les fuites entre le cœur et le plugin (:lien: docs Rails).

2. Définir des fichiers ruby dans la bonne structure de répertoires

Le moteur auto-chargera maintenant tous les fichiers dans {plugin}/app/{type}/*. Par exemple, nous pouvons définir un contrôleur

{plugin}/app/controllers/mon_module_plugin/examples_controller.rb

module ::MonModulePlugin
  class ExamplesController < ::ApplicationController
    requires_plugin PLUGIN_NAME

    def index
      render json: { hello: "world" }
    end
  end
end

Celui-ci sera maintenant auto-chargé chaque fois que quelque chose dans Rails essaiera d’accéder à ::MonModulePlugin::MyController. Pour tester, essayez d’accéder à cette classe depuis la console rails.

Pour que l’auto-chargement fonctionne correctement, les chemins de fichiers doivent correspondre à la hiérarchie complète des modules/classes selon les règles définies par Zeitwerk.

3. Définir des routes sur le moteur du plugin

Créez un fichier {plugin}/config/routes.rb

MonModulePlugin::Engine.routes.draw do
  get "/examples" => "examples#index"
  # définir les routes ici
end

Discourse::Application.routes.draw do
  mount ::MonModulePlugin::Engine, at: "mon-plugin"
end

Ce fichier sera automatiquement chargé par le moteur, et les modifications prendront effet sans redémarrage du serveur. Dans ce cas, l’action du contrôleur serait disponible à /mon-plugin/examples.json.

4. Ajouter d’autres chemins auto-chargés

Parfois, vous pourriez vouloir introduire des répertoires supplémentaires de fichiers Ruby auto-chargés. L’exemple le plus courant est le répertoire lib/ dans un plugin.

Modifiez la définition de votre moteur pour ajouter lib/ aux chemins d’auto-chargement du moteur :

class Engine < ::Rails::Engine
  engine_name PLUGIN_NAME
  isolate_namespace MonModulePlugin
  config.autoload_paths << File.join(config.root, "lib")
end

Vous pouvez maintenant définir un module lib comme

{plugin}/lib/mon_module_plugin/some_lib_module.rb

module ::MonModulePlugin::SomeLibModule
end

Et maintenant, toute référence à ::MonModulePlugin::SomeLibModule chargera automatiquement le module à partir de ce fichier.

5. Profit !

Tous ces fichiers seront maintenant chargés automatiquement sans aucun appel require délibéré. Les modifications seront automatiquement détectées par rails et rechargées sur place sans redémarrage du serveur.


Ce document est contrôlé par version - suggérez des modifications sur github.

17 « J'aime »

David, qu’est-ce qui a motivé cette documentation ? Un changement en amont ?

Je ne me souviens pas avoir jamais eu à me soucier de maintenir une structure particulière pour obtenir le rechargement automatique, ou peut-être suivais-je simplement la structure appropriée par chance ? …

1 « J'aime »

Aucun changement en amont - nous documentons simplement une structure que nous avons utilisée dans quelques plugins (par exemple, anonymous-moderators, client-performance), et que nous aimerions commencer à utiliser dans davantage de nos plugins.

Si vous acceptez les trois inconvénients en haut du message d’origine, alors oui, n’importe quelle structure fonctionnera. Si vous suivez cette structure, ces problèmes seront résolus (et l’expérience du développeur devrait être beaucoup plus fluide !)

2 « J'aime »

Oui, le rechargement automatique est très souhaitable.

Merci pour la clarification et la documentation !

1 « J'aime »

Pour être clair également - quand je dis « autoloading », je parle de la capacité de Rails (via Zeitwerk) à intégrer les changements de code dans un processus en cours d’exécution presque instantanément, sans nécessiter de redémarrage. Si vous avez des appels load ou require dans votre fichier plugin.rb, votre plugin n’utilise presque certainement pas l’autoloading.

Séparément, Discourse dispose d’une aide qui détecte les modifications apportées aux fichiers Ruby non rechargeables et effectue automatiquement un redémarrage complet du serveur. Cela prend quelques secondes. Lorsque ce système est déclenché, vous verrez quelque chose comme ceci dans la console :

[DEV]: Edited files which are not autoloaded. Restarting server...
       - plugins/discourse-follow/plugin.rb
2 « J'aime »

Ah !! C’est la distinction qui me manquait, merci ! J’étais déjà satisfait du redémarrage automatique, mais c’est vraiment génial !

2 « J'aime »

David,

J’ai eu un problème intéressant, que je pense avoir résolu, je me demandais si vous aviez le temps de commenter ?

J’avais une constante qui n’était définie qu’une seule fois dans mon module de plugin dans /lib/locations

À chaque reconstruction, j’obtenais l’avertissement suivant :

image

warning: already initialized constant Locations::REQUEST_PARTS
warning: already previous definition of REQUEST_PARTS was here

Le déplacer dans plugin.rb comme ceci résout apparemment le problème :

Mais… pourquoi ?

Je pense que plugin.rb n’est évalué qu’une seule fois, mais tout ce qui se trouve dans /lib/plugin-module pourrait être évalué plus d’une fois… ? Mais alors pourquoi ne se plaint-il pas d’autres codes ?

1 « J'aime »

Oui, exactement.

Lorsque Rails/Zeitwerk recharge le code, il exécute remove_const :Blah, puis charge le fichier portant le nom pertinent. Ainsi, pour votre fichier lib/locations/geocode.rb, l’autoloader fait quelque chose comme

Locations.remove_const :Geocode
load "lib/locations/geocode.rb"

C’est pourquoi vous n’obtenez pas d’erreur concernant la constante Geocode déjà définie - Rails/Zeitwerk la supprime automatiquement avant de recharger le fichier.

Cependant, il n’y a aucun moyen pour l’autoloader de savoir que votre fichier définissait également Locations::REQUEST_PARTS, il n’a donc pas exécuté remove_const avant le chargement.

Donc, si vous vouliez conserver la constante dans le fichier geocode.rb, vous pourriez déplacer REQUEST_PARTS à l’intérieur de la classe Geocode (Locations::Geocode::REQUEST_PARTS).

Mais si vous voulez la conserver sur Locations::REQUEST_PARTS, alors je pense que le déplacer dans plugin.rb est logique. Ou, si vous vouliez la rendre entièrement chargeable automatiquement, vous pourriez déplacer la définition du module ::Locations, y compris la constante REQUEST_PARTS, dans son propre fichier comme lib/locations.rb.

3 « J'aime »

C’est tout à fait logique, merci !

Je pense qu’il pourrait y avoir autre chose ?

Est-il également juste de dire qu’il est inutile de charger explicitement ces fichiers dans plugin.rb comme suit :

after_initialize do
  %w(
    ../app/models/location_country_default_site_setting.rb
    ../app/models/location_geocoding_language_site_setting.rb
    ../app/models/locations/user_location.rb
    ../app/models/locations/topic_location.rb
**    ../lib/locations/user_location_process.rb**
**    ../lib/locations/topic_location_process.rb**
**    ../lib/locations/country.rb**
**    ../lib/locations/geocode.rb**
**    ../lib/locations/helper.rb**
**    ../lib/locations/map.rb**
    ../lib/users_map.rb
    ../app/serializers/locations/geo_location_serializer.rb
    ../app/controllers/locations/geocode_controller.rb
    ../app/controllers/locations/users_map_controller.rb
  ).each do |path|
    load File.expand_path(path, __FILE__)
  end

parce qu’ils sont dans la structure également gérée par l’autoloading ?

Oui, puisque /lib est inclus dans votre configuration d’autoloading, il ne devrait pas être nécessaire de les load manuellement :100:

2 « J'aime »

Qui n’aime pas une bonne simplification de code ?! :chefs_kiss:

1 « J'aime »