Strukturierung eines Plugins für Rails-Autoloading

Viele Plugins enthalten viele Klassendefinitionen in plugin.rb oder verwenden require_relative, um Ruby-Dateien zu laden. Das funktioniert, hat aber einige Nachteile:

  1. Keine automatische Neuladung von Änderungen in der Entwicklung. Jede Änderung erfordert einen vollständigen Serverneustart
  2. Die require-Aufrufe in die richtige Reihenfolge zu bringen, kann mühsam sein
  3. Wenn sie außerhalb des after_initialize-Blocks requiret werden, sind andere automatisch geladene Klassen/Module möglicherweise nicht verfügbar

Es gibt eine Lösung! Plugins können sich auf das Standard-Autoloading-System von Rails verlassen. Für neue Plugins ist alles, was Sie benötigen, im Plugin-Skeleton definiert. Dieses Thema beschreibt, wie Sie ein vorhandenes Plugin anpassen und die Konfiguration erweitern.

1. Definieren Sie ein Modul und eine Rails::Engine für Ihr Plugin

Definieren Sie in plugin.rb ein Modul für Ihr Plugin mit einem eindeutigen PLUGIN_NAME und fügen Sie eine require_relative-Zeile hinzu, um die Engine-Datei zu laden, die wir gerade erstellen.

# name: my-plugin-name
# ...

module ::MyPluginModule
  PLUGIN_NAME = "my-plugin-name"
end

require_relative "lib/my_plugin_module/engine"

Erstellen Sie nun {plugin}/lib/my_plugin_module/engine.rb:

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

Wichtige Punkte:

  1. In plugin.rb müssen Sie :: vor Ihrem Modulnamen einfügen, um es im Stamm-Namespace zu definieren (andernfalls würde es unter Plugin::Instance definiert werden).

  2. require_relative "lib/.../engine" muss sich im Stammverzeichnis der Datei plugin.rb befinden, nicht innerhalb eines after_initialize-Blocks.

  3. Das Platzieren der Engine in einer eigenen Datei unter lib/ ist wichtig. Die Definition direkt in der Datei plugin.rb funktioniert nicht. (Rails verwendet die Anwesenheit eines lib/-Verzeichnisses, um die Stammdatei der Engine zu bestimmen.)

  4. Der Dateipfad sollte den Modulnamen gemäß den Zeitwerk-Regeln enthalten.

  5. Der engine_name wird als Präfix für Rake-Aufgaben und alle von der Engine definierten Routen verwendet (:link: Rails Docs).

  6. isolate_namespace hilft, das Auslaufen von Dingen zwischen dem Kern und dem Plugin zu verhindern (:link: Rails Docs).

2. Definieren Sie Ruby-Dateien in der richtigen Verzeichnisstruktur

Die Engine lädt nun automatisch alle Dateien in {plugin}/app/{type}/*. Wir können zum Beispiel einen Controller definieren

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

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

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

Diese wird nun automatisch geladen, wenn etwas in Rails versucht, auf ::MyPluginModule::MyController zuzugreifen. Um Dinge zu testen, versuchen Sie, von der Rails-Konsole auf diese Klasse zuzugreifen.

Damit das automatische Laden korrekt funktioniert, müssen die Dateipfade der vollständigen Modul-/Klassenhierarchie gemäß den von Zeitwerk definierten Regeln entsprechen.

3. Routen auf der Engine des Plugins definieren

Erstellen Sie eine Datei {plugin}/config/routes.rb

MyPluginModule::Engine.routes.draw do
  get "/examples" => "examples#index"
  # Routen hier definieren
end

Discourse::Application.routes.draw do
  mount ::MyPluginModule::Engine, at: "my-plugin"
end

Diese Datei wird automatisch von der Engine geladen, und Änderungen werden wirksam, ohne dass ein Serverneustart erforderlich ist. In diesem Fall wäre die Controller-Aktion unter /my-plugin/examples.json verfügbar.

4. Weitere automatisch ladbare Pfade hinzufügen

Manchmal möchten Sie zusätzliche Verzeichnisse mit automatisch ladbaren Ruby-Dateien einführen. Das häufigste Beispiel ist das lib/-Verzeichnis in einem Plugin.

Ändern Sie Ihre Engine-Definition, um lib/ zu den Autoload-Pfaden der Engine hinzuzufügen:

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

Jetzt können Sie ein Lib-Modul definieren wie

{plugin}/lib/my_plugin_module/some_lib_module.rb

module ::MyPluginModule::SomeLibModule
end

Und nun werden alle Verweise auf ::MyPluginModule::SomeLibModule das Modul automatisch aus dieser Datei laden.

5. Profitieren!

All diese Dateien werden nun automatisch geladen, ohne dass bewusste require-Aufrufe erforderlich sind. Änderungen werden automatisch von Rails erkannt und ohne Serverneustart an Ort und Stelle neu geladen.


Dieses Dokument wird versioniert - schlagen Sie Änderungen auf GitHub vor.

17 „Gefällt mir“

David, was hat diese Dokumentation veranlasst? Eine Änderung im Upstream?

Ich kann mich nicht erinnern, jemals darauf achten zu müssen, eine bestimmte Struktur beizubehalten, um das automatische Neuladen zu erreichen, oder vielleicht habe ich einfach nur glücklicherweise die entsprechende Struktur befolgt? …

1 „Gefällt mir“

Keine Änderung am Upstream – wir dokumentieren lediglich eine Struktur, die wir in einigen Plugins verwendet haben (z. B. anonymous-moderators, client-performance) und die wir in mehr unserer Plugins verwenden möchten.

Wenn Sie die drei Nachteile am Anfang des OP akzeptieren, dann funktioniert jede Struktur. Wenn Sie dieser Struktur folgen, werden diese Probleme behoben (und die Entwicklererfahrung sollte viel reibungsloser sein!)

2 „Gefällt mir“

Ja, das automatische Neuladen ist sehr wünschenswert.

Vielen Dank für die Klärung und die Dokumentation!

1 „Gefällt mir“

Um klar zu sein: Wenn ich „Autoloading“ sage, meine ich die Fähigkeit von Rails (über Zeitwerk), Codeänderungen fast augenblicklich in einen laufenden Prozess zu übernehmen, ohne dass ein Neustart erforderlich ist. Wenn Sie load- oder require-Aufrufe in Ihrer plugin.rb-Datei haben, verwendet Ihr Plugin mit ziemlicher Sicherheit kein Autoloading.

Separat dazu verfügt Discourse über eine Hilfsfunktion, die Änderungen an nicht neu ladbaren Ruby-Dateien erkennt und automatisch einen vollständigen Serverneustart durchführt. Dies dauert einige Sekunden. Wenn dieses System ausgelöst wird, sehen Sie etwas wie dieses in der Konsole:

[DEV]: Nicht automatisch geladene Dateien bearbeitet. Server wird neu gestartet...
       - plugins/discourse-follow/plugin.rb
2 „Gefällt mir“

Ah!! Das ist die Unterscheidung, die mir gefehlt hat, danke! Ich war schon mit dem automatischen Neustart zufrieden, aber das ist wirklich cool!

2 „Gefällt mir“

David,

Ich hatte ein interessantes Problem, das ich glaube ich gelöst habe. Ich frage mich, ob Sie Zeit hatten, einen Kommentar dazu abzugeben?

Ich hatte eine Konstante, die nur einmal in meinem Plugin-Modul in /lib/locations definiert wurde.

Bei jedem Rebuild erhielt ich die folgende Fehler Warnung:

image

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

Dies nach plugin.rb zu verschieben, löst das Problem anscheinend:

Aber … warum?

Meiner Meinung nach wird plugin.rb nur einmal ausgewertet, aber alles in /lib/plugin-module könnte mehr als einmal ausgewertet werden …? Aber warum beschwert es sich dann nicht über anderen Code?

1 „Gefällt mir“

Yup, genau.

Wenn Rails/Zeitwerk Code neu lädt, führt es remove_const :Blah aus und lädt dann die Datei mit dem entsprechenden Namen. Für Ihre Datei lib/locations/geocode.rb macht der Autoloader also etwas wie

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

Deshalb erhalten Sie keine Fehlermeldung, dass die Konstante Geocode bereits definiert war – Rails/Zeitwerk entfernt sie automatisch, bevor die Datei neu geladen wird.

Es gibt jedoch keine Möglichkeit für den Autoloader zu wissen, dass Ihre Datei auch Locations::REQUEST_PARTS definiert hat, sodass er vor dem Laden nicht remove_const ausgeführt hat.

Wenn Sie also die Konstante in der Datei geocode.rb beibehalten wollten, könnten Sie REQUEST_PARTS in die Klasse Geocode verschieben (Locations::Geocode::REQUEST_PARTS).

Wenn Sie sie jedoch auf Locations::REQUEST_PARTS beibehalten möchten, dann macht das Verschieben nach plugin.rb meiner Meinung nach Sinn. Oder, wenn Sie sie vollständig autoloadfähig machen wollten, könnten Sie die Definition von module ::Locations, einschließlich der Konstante REQUEST_PARTS, in eine eigene Datei wie lib/locations.rb verschieben.

3 „Gefällt mir“

Das ergibt absolut Sinn, danke!

Ich glaube, es gibt noch etwas anderes?

Ist es auch fair zu sagen, dass es unnötig ist, diese Dateien explizit in plugin.rb zu laden, wie in:

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

weil sie sich innerhalb der Struktur befinden, die auch von Autoloading gehandhabt wird?

Ja, da /lib in Ihrer Autoloading-Konfiguration enthalten ist, sollte es nicht notwendig sein, sie manuell zu laden :100:

2 „Gefällt mir“

Wer liebt nicht eine gute Codevereinfachung?! :chefs_kiss:

1 „Gefällt mir“