Strutturare un plugin per l'autoloading di Rails

Molti plugin includono molte definizioni di classi all’interno di plugin.rb, o utilizzano require_relative per caricare file ruby. Questo funziona, ma presenta alcuni svantaggi:

  1. Nessun ricaricamento automatico delle modifiche in fase di sviluppo. Qualsiasi modifica richiede un riavvio completo del server
  2. Ottenere le chiamate require nell’ordine corretto può essere complicato
  3. Se vengono caricate (required) al di fuori del blocco after_initialize, altre classi/moduli caricati automaticamente potrebbero non essere disponibili

Esiste una soluzione! I plugin possono sfruttare il sistema di caricamento automatico standard di Rails. Per i nuovi plugin, tutto ciò di cui hai bisogno è definito nel plugin-skeleton. Questo argomento descrive come adattare un plugin esistente ed estendere la configurazione.

1. Definire un modulo e un Rails::Engine per il tuo plugin

In plugin.rb, definisci un modulo per il tuo plugin con un PLUGIN_NAME univoco e aggiungi una riga require_relative per caricare il file engine che stiamo per creare.

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

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

require_relative "lib/my_plugin_module/engine"

Ora crea {plugin}/lib/my_plugin_module/engine.rb:

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

Cose importanti da notare:

  1. In plugin.rb, devi includere :: prima del nome del tuo modulo per definirlo nello spazio dei nomi principale (altrimenti, verrebbe definito sotto Plugin::Instance)

  2. require_relative "lib/.../engine" deve trovarsi nella radice del file plugin.rb, non all’interno di un blocco after_initialize

  3. Mettere l’engine nel suo file separato sotto lib/ è importante. Definirlo direttamente nel file plugin.rb non funzionerà. (Rails utilizza la presenza di una directory lib/ per determinare la radice dell’engine)

  4. Il percorso del file deve includere il nome del modulo, secondo le regole di Zeitwerk

  5. engine_name viene utilizzato come prefisso per i task rake e qualsiasi route definita dall’engine (:link: docs rails)

  6. isolate_namespace aiuta a prevenire perdite tra il core e il plugin (:link: docs Rails)

2. Definire file ruby nella corretta struttura di directory

L’engine caricherà automaticamente tutti i file in {plugin}/app/{type}/*. Ad esempio, possiamo definire un controller

{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

Questo verrà ora caricato automaticamente ogni volta che qualcosa in Rails tenterà di accedere a ::MyPluginModule::MyController. Per testare, prova ad accedere a quella classe dalla console rails.

Affinché il caricamento automatico funzioni correttamente, i percorsi dei file devono corrispondere alla gerarchia completa di moduli/classi secondo le regole definite da Zeitwerk.

3. Definire route sull’engine del plugin

Crea un file {plugin}/config/routes.rb

MyPluginModule::Engine.routes.draw do
  get "/examples" => "examples#index"
  # definisci qui le route
end

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

Questo file verrà caricato automaticamente dall’engine e le modifiche avranno effetto senza riavvio del server. In questo caso, l’azione del controller sarà disponibile su /my-plugin/examples.json.

4. Aggiungere altri percorsi caricabili automaticamente

A volte potresti voler introdurre directory aggiuntive di file Ruby caricabili automaticamente. L’esempio più comune è la directory lib/ in un plugin.

Modifica la definizione del tuo engine per aggiungere lib/ ai percorsi di caricamento automatico dell’engine:

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

Ora puoi definire un modulo lib come

{plugin}/lib/my_plugin_module/some_lib_module.rb

module ::MyPluginModule::SomeLibModule
end

E ora qualsiasi riferimento a ::MyPluginModule::SomeLibModule caricherà automaticamente il modulo da questo file.

5. Profitto!

Tutti questi file verranno ora caricati automaticamente senza chiamate require deliberate. Le modifiche verranno raccolte automaticamente da rails e ricaricate in loco senza riavvio del server.


Questo documento è controllato in versione - suggerisci modifiche su github.

17 Mi Piace

David, cosa ha portato a questa documentazione? Una modifica a monte?

Non ricordo di aver mai dovuto preoccuparmi di mantenere una struttura particolare per ottenere il ricaricamento automatico, o forse stavo solo seguendo fortunatamente la struttura appropriata? …

1 Mi Piace

Nessuna modifica upstream - sto solo documentando una struttura che abbiamo utilizzato in alcuni plugin (ad esempio, anonymous-moderators, client-performance), e vorremmo iniziare a utilizzare in più plugin.

Se accetti i tre svantaggi in cima all’OP, allora sì, qualsiasi struttura funzionerà. Se segui questa struttura, quei problemi saranno risolti (e l’esperienza dello sviluppatore dovrebbe essere molto più fluida!)

2 Mi Piace

Sì, il ricaricamento automatico è molto auspicabile.

Grazie per il chiarimento e la documentazione!

1 Mi Piace

Per essere chiari, quando dico “autoloading”, intendo la capacità di Rails (tramite Zeitwerk) di integrare le modifiche al codice in un processo in esecuzione quasi istantaneamente, senza bisogno di un riavvio. Se hai chiamate load o require nel tuo file plugin.rb, il tuo plugin quasi certamente non sta utilizzando l’autoloading.

Separately, Discourse ha un helper che rileva le modifiche ai file ruby non ricaricabili ed esegue automaticamente un riavvio completo del server. Questo richiede alcuni secondi. Quando questo sistema viene attivato, vedrai qualcosa di simile nella console:

[DEV]: Edited files which are not autoloaded. Restarting server...
       - plugins/discourse-follow/plugin.rb
2 Mi Piace

Ah! Questa è la distinzione che mi mancava, grazie! Ero contento solo con il riavvio automatico, ma questo è davvero fantastico!

2 Mi Piace

David,

Ho riscontrato un problema interessante, che credo di aver risolto, mi chiedo se avessi tempo di commentare?

Avevo una costante che veniva definita una sola volta nel mio plugin Module all’interno di /lib/locations

Ad ogni ricompilazione ricevevo il seguente errore avviso:

image

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

Spostarla in plugin.rb in questo modo, apparentemente risolve il problema:

Ma… perché?

La mia ipotesi è che valuti plugin.rb solo una volta, ma qualsiasi cosa in /lib/plugin-module potrebbe essere valutata più di una volta…? Ma allora perché non si lamenta di altro codice?

1 Mi Piace

Sì, esattamente.

Quando Rails/Zeitwerk ricarica il codice, esegue remove_const :Blah e poi carica il file con il nome pertinente. Quindi, per il tuo file lib/locations/geocode.rb, l’autoloader fa qualcosa di simile

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

Ecco perché non ottieni un errore riguardo alla costante Geocode già definita: Rails/Zeitwerk la rimuove automaticamente prima di ricaricare il file.

Tuttavia, non c’è modo per l’autoloader di sapere che il tuo file stava anche definendo Locations::REQUEST_PARTS, quindi non ha eseguito remove_const prima del caricamento.

Quindi, se volessi mantenere la costante nel file geocode.rb, potresti spostare REQUEST_PARTS all’interno della classe Geocode (Locations::Geocode::REQUEST_PARTS)

Ma se vuoi mantenerla su Locations::REQUEST_PARTS, allora penso che spostarla in plugin.rb abbia senso. Oppure, se volessi renderla completamente autoloadable, potresti spostare la definizione module ::Locations, inclusa la costante REQUEST_PARTS, in un file separato come lib/locations.rb.

3 Mi Piace

Ha perfettamente senso, grazie!

Penso che ci possa essere anche qualcos’altro?

È anche giusto dire che è inutile caricare esplicitamente questi file in plugin.rb come 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

perché sono all’interno della struttura gestita anche dall’autoloading?

Sì, dato che /lib è incluso nella configurazione di autoloading, non dovrebbe essere necessario caricarli manualmente :100:

2 Mi Piace

Chi non ama una buona semplificazione del codice?! :chefs_kiss:

1 Mi Piace