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 durante lo sviluppo. Qualsiasi modifica richiede un riavvio completo del server
  2. Mettere le chiamate require nell’ordine corretto può essere difficile
  3. Se vengono caricati (require’d) al di fuori del blocco after_initialize, altre classi/moduli caricati automaticamente potrebbero non essere disponibili

C’è una soluzione! I plugin possono fare affidamento sul sistema di caricamento automatico standard di Rails. Per i nuovi plugin, tutto ciò di cui hai bisogno è definito nello scheletro del plugin. 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 del motore 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 il motore nel proprio file sotto lib/ è importante. Definiscerlo direttamente nel file plugin.rb non funzionerà. (Rails utilizza la presenza di una directory lib/ per determinare la radice del motore)

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

  5. L’engine_name viene utilizzato come prefisso per le attività rake e qualsiasi route definita dal motore (:link: docs di rails)

  6. isolate_namespace aiuta a prevenire la fuoriuscita di elementi tra il core e il plugin (:link: docs di Rails)

2. Definire file ruby nella corretta struttura di directory

Il motore ora 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 tenta di accedere a ::MyPluginModule::MyController. Per testare le cose, prova ad accedere a quella classe dalla console rails.

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

3. Definire le route sul motore del plugin

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

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

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

Questo file verrà caricato automaticamente dal motore e le modifiche avranno effetto senza riavviare il server. In questo caso, l’azione del controller sarebbe disponibile su /my-plugin/examples.json.

4. Aggiungere più 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 motore per aggiungere lib/ ai percorsi di caricamento automatico del motore:

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 alcuna chiamata require deliberata. Le modifiche verranno rilevate automaticamente da rails e ricaricate sul posto 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