Una guida ai motori Rails in natura: esempi reali di motori Rails in azione

Pubblicato: 2022-03-11

Perché i motori Rails non vengono utilizzati più spesso? Non conosco la risposta, ma penso che la generalizzazione di "Everything is an Engine" abbia nascosto i domini dei problemi che possono aiutare a risolvere.

La superba documentazione di Rails Guide per iniziare con Rails Engines fa riferimento a quattro esempi popolari di implementazioni di Rails Engine: Forem, Devise, Spree e RefineryCMS. Questi sono fantastici casi d'uso nel mondo reale per i motori, ognuno dei quali utilizza un approccio diverso all'integrazione con un'applicazione Rails.

Ogni guida Rails dovrebbe coprire l'argomento dei modelli di progettazione dei motori Rails e dei loro esempi.

L'esame di parti di come queste gemme sono configurate e composte fornirà agli sviluppatori avanzati di Ruby on Rails una preziosa conoscenza di quali modelli o tecniche vengono provati e testati in natura, quindi quando arriva il momento puoi avere alcune opzioni extra da valutare.

Mi aspetto che tu abbia una conoscenza superficiale di come funziona un motore, quindi se ritieni che qualcosa non stia tornando del tutto, ti preghiamo di leggere l'eccellente Rails Guide Getting Started With Engines .

Senza ulteriori indugi, avventuriamoci nel selvaggio mondo degli esempi di motori Rails!

Forem

Un motore per Rails che mira ad essere il miglior piccolo sistema di forum di sempre

Questa gemma segue alla lettera le indicazioni della Rails Guide on Engines. È un esempio considerevole e l'analisi del suo repository ti darà un'idea di quanto puoi estendere la configurazione di base.

È una gemma a motore singolo che utilizza un paio di tecniche per integrarsi con l'applicazione principale.

 module ::Forem class Engine < Rails::Engine isolate_namespace Forem # ... config.to_prepare do Decorators.register! Engine.root, Rails.root end # ... end end

La parte interessante qui è il Decorators.register! metodo class, esposto dalla gemma Decoratori. Incapsula i file di caricamento che non sarebbero inclusi nel processo di caricamento automatico di Rails. Potresti ricordare che l'uso di istruzioni require esplicite rovina il ricaricamento automatico in modalità di sviluppo, quindi questo è un vero toccasana! Sarà più chiaro utilizzare l'esempio della Guida per illustrare cosa sta succedendo:

 config.to_prepare do Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c| require_dependency(c) end end

La maggior parte della magia per la configurazione di Forem avviene nella definizione del modulo principale superiore di Forem . Questo file si basa su una variabile user_class impostata in un file di inizializzazione:

 Forem.user_class = "User"

Lo fai usando mattr_accessor ma è tutto nella Guida di Rails quindi non lo ripeterò qui. Con questo in atto, Forem decora quindi la classe utente con tutto ciò di cui ha bisogno per eseguire la sua applicazione:

 module Forem class << self def decorate_user_class! Forem.user_class.class_eval do extend Forem::Autocomplete include Forem::DefaultPermissions has_many :forem_posts, :class_name => "Forem::Post", :foreign_key => "user_id" # ... def forem_moderate_posts? Forem.moderate_first_post && !forem_approved_to_post? end alias_method :forem_needs_moderation?, :forem_moderate_posts? # ...

Che risulta essere parecchio! Ho tagliato la maggior parte ma ho lasciato una definizione di associazione e un metodo di istanza per mostrarti il ​​tipo di linee che puoi trovare lì.

Dare un'occhiata all'intero file potrebbe mostrarti quanto potrebbe essere gestibile il porting di parte della tua applicazione per il riutilizzo su un motore.

Decorare è il nome del gioco nell'utilizzo predefinito del motore. Come utente finale di gem puoi sovrascrivere il modello, la vista e i controller creando le tue versioni delle classi usando il percorso del file e le convenzioni di denominazione dei file stabilite nel README del decoratore gem. Tuttavia, c'è un costo associato a questo approccio, specialmente quando l'Engine ottiene un aggiornamento della versione principale: la manutenzione del mantenimento del funzionamento delle decorazioni può sfuggire di mano rapidamente. Non sto citando Forem qui, credo che siano fermi nel mantenere una funzionalità di base affiatata, ma tienilo a mente se crei un motore e decidi di eseguire una revisione.

Ricapitoliamo questo: questo è il modello di progettazione del motore Rails predefinito che si basa sugli utenti finali che decorano viste, controller e modelli, insieme alla configurazione delle impostazioni di base tramite i file di inizializzazione. Funziona bene per funzionalità molto mirate e correlate.

Escogitare

Una soluzione di autenticazione flessibile per Rails

Troverai che un motore è molto simile a un'applicazione Rails, con directory di views , controllers e models . Devise è un buon esempio di incapsulamento di un'applicazione ed esposizione di un comodo punto di integrazione. Esaminiamo come funziona esattamente.

Riconoscerai queste righe di codice se sei uno sviluppatore Rails da più di qualche settimana:

 class User < ActiveRecord::Base # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable end

Ogni parametro passato al metodo devise rappresenta un modulo all'interno del Devise Engine. Ci sono dieci di questi moduli in tutto che ereditano dal familiare ActiveSupport::Concern . Questi estendono la tua classe User invocando il metodo devise all'interno del suo ambito.

Avendo questo tipo di punto di integrazione è molto flessibile, puoi aggiungere o rimuovere uno qualsiasi di questi parametri per modificare il livello di funzionalità che richiedi al motore di eseguire. Significa anche che non è necessario codificare il modello che si desidera utilizzare all'interno di un file di inizializzazione, come suggerito dalla Rails Guide on Engines. In altre parole, questo non è necessario:

 Devise.user_model = 'User'

Questa astrazione significa anche che puoi applicarlo a più di un modello utente all'interno della stessa applicazione (ad esempio admin e user ), mentre l'approccio del file di configurazione ti lascerebbe legato a un unico modello con autenticazione. Questo non è il più grande punto di forza, ma illustra un modo diverso di risolvere un problema.

Devise estende ActiveRecord::Base con il proprio modulo che include la definizione del metodo devise :

 # lib/devise/orm/active_record.rb ActiveRecord::Base.extend Devise::Models

Qualsiasi classe che eredita da ActiveRecord::Base avrà ora accesso ai metodi di classe definiti in Devise::Models :

 #lib/devise/models.rb module Devise module Models # ... def devise(*modules) selected_modules = modules.map(&:to_sym).uniq # ... selected_modules.each do |m| mod = Devise::Models.const_get(m.to_s.classify) if mod.const_defined?("ClassMethods") class_mod = mod.const_get("ClassMethods") extend class_mod # ... end include mod end end # ... end end

(Ho rimosso molto codice ( # ... ) per evidenziare le parti importanti.)

Parafrasando il codice, per ogni nome di modulo passato al metodo devise siamo:

  • caricando il modulo che abbiamo specificato che vive in Devise::Models ( Devise::Models.const_get(m.to_s.classify )
  • estendendo la classe User con il modulo ClassMethods se ne ha uno
  • include il modulo specificato ( include mod ) per aggiungere i suoi metodi di istanza alla classe che chiama il metodo devise ( User )

Se volevi creare un modulo che potesse essere caricato in questo modo, dovresti assicurarti che seguisse la consueta interfaccia ActiveSupport::Concern , ma assegnalo allo spazio dei nomi sotto Devise:Models poiché è qui che cerchiamo di recuperare la costante:

 module Devise module Models module Authenticatable extend ActiveSupport::Concern included do # ... end module ClassMethods # ... end end end end

Uff.

Se hai già utilizzato Rails' Concerns e hai sperimentato la riutilizzabilità che offrono, allora puoi apprezzare le sottigliezze di questo approccio. In breve, suddividere la funzionalità in questo modo rende più semplice il test essendo astratto da un modello ActiveRecord e ha un sovraccarico inferiore rispetto al modello predefinito utilizzato da Forem quando si tratta di estendere la funzionalità.

Questo modello consiste nel suddividere la tua funzionalità in Rails Concerns ed esporre un punto di configurazione per includerli o escluderli all'interno di un determinato ambito. Un motore così formato è conveniente per l'utente finale, un fattore che contribuisce al successo e alla popolarità di Devise. E ora sai come farlo anche tu!

Baldoria

Una soluzione di e-commerce open source completa per Ruby on Rails

Spree ha compiuto uno sforzo colossale per tenere sotto controllo la loro applicazione monolitica con il passaggio all'utilizzo dei motori. Il design dell'architettura con cui stanno ora rotolando è una gemma "Spree" che contiene molte gemme del motore.

Questi motori creano partizioni nel comportamento che potresti essere abituato a vedere all'interno di un'applicazione monolitica o distribuite tra le applicazioni:

  • spree_api (API RESTful)
  • spree_frontend (componenti rivolti all'utente)
  • spree_backend (Area amministrativa)
  • spree_cmd (Strumenti da riga di comando)
  • spree_core (Modelli e mailer, i componenti di base di Spree di cui non può funzionare senza)
  • spree_sample (dati di esempio)

La gemma avvolgente li unisce, lasciando allo sviluppatore la possibilità di scegliere il livello di funzionalità da richiedere. Ad esempio, potresti eseguire solo con lo spree_core Engine e avvolgere la tua interfaccia attorno ad esso.

La gemma principale della Sprea richiede questi motori:

 # lib/spree.rb require 'spree_core' require 'spree_api' require 'spree_backend' require 'spree_frontend'

Ogni Engine deve quindi personalizzare il proprio engine_name e il percorso root (quest'ultimo di solito punta alla gemma di livello superiore) e configurarsi agganciandosi al processo di inizializzazione:

 # api/lib/spree/api/engine.rb require 'rails/engine' module Spree module Api class Engine < Rails::Engine isolate_namespace Spree engine_name 'spree_api' def self.root @root ||= Pathname.new(File.expand_path('../../../../', __FILE__)) end initializer "spree.environment", :before => :load_config_initializers do |app| app.config.spree = Spree::Core::Environment.new end # ... end end end

Potresti riconoscere o meno questo metodo di inizializzazione: fa parte di Railtie ed è un hook che ti dà l'opportunità di aggiungere o rimuovere passaggi dall'inizializzazione del framework Rails. Spree fa molto affidamento su questo hook per configurare il suo ambiente complesso per tutti i suoi motori.

Utilizzando l'esempio sopra in fase di esecuzione, avrai accesso alle tue impostazioni accedendo alla costante Rails di livello superiore:

 Rails.application.config.spree

Con questa guida al modello di progettazione del motore Rails sopra, potremmo chiamarla un giorno, ma Spree ha un sacco di codice straordinario, quindi immergiamoci nel modo in cui utilizzano l'inizializzazione per condividere la configurazione tra i motori e l'applicazione Rails principale.

Spree ha un complesso sistema di preferenze che carica aggiungendo un passaggio nel processo di inizializzazione:

 # api/lib/spree/api/engine.rb initializer "spree.environment", :before => :load_config_initializers do |app| app.config.spree = Spree::Core::Environment.new end

Qui alleghiamo ad app.config.spree una nuova istanza Spree::Core::Environment . All'interno dell'applicazione rails potrai accedervi tramite Rails.application.config.spree da qualsiasi luogo: modelli, controller, viste.

Andando verso il basso, la classe Spree::Core::Environment che creiamo assomiglia a questa:

 module Spree module Core class Environment attr_accessor :preferences def initialize @preferences = Spree::AppConfiguration.new end end end end

Espone una variabile :preferences impostata su una nuova istanza della classe Spree::AppConfiguration , che a sua volta utilizza un metodo di preference definito nella classe Preferences::Configuration per impostare le opzioni con i valori predefiniti per la configurazione generale dell'applicazione:

 module Spree class AppConfiguration < Preferences::Configuration # Alphabetized to more easily lookup particular preferences preference :address_requires_state, :boolean, default: true # should state/state_name be required preference :admin_interface_logo, :string, default: 'logo/spree_50.png' preference :admin_products_per_page, :integer, default: 10 preference :allow_checkout_on_gateway_error, :boolean, default: false # ... end end

Non mostrerò il file Preferences::Configuration perché ci vorrà un po' di spiegazione ma essenzialmente è zucchero sintattico per ottenere e impostare le preferenze. (In verità, questa è una semplificazione eccessiva della sua funzionalità, poiché il sistema delle preferenze salverà valori diversi da quelli predefiniti per le preferenze esistenti o nuove nel database, per qualsiasi classe ActiveRecord con una colonna :preference - ma non è necessario sapere che.)

Ecco una di quelle opzioni in azione:

 module Spree class Calculator < Spree::Base def self.calculators Rails.application.config.spree.calculators end # ... end end

I calcolatori controllano ogni sorta di cose in Spree - costi di spedizione, promozioni, adeguamenti del prezzo dei prodotti - quindi avere un meccanismo per scambiarli in questo modo aumenta l'estensibilità del motore.

Uno dei tanti modi in cui puoi sovrascrivere le impostazioni predefinite per queste preferenze è all'interno di un inizializzatore nell'applicazione Rails principale:

 # config/initializergs/spree.rb Spree::Config do |config| config.admin_interface_logo = 'company_logo.png' end

Se hai letto la RailsGuide sui motori, considerato i loro modelli di progettazione o costruito tu stesso un motore, saprai che è banale esporre un setter in un file di inizializzazione che qualcuno può usare. Quindi ti starai chiedendo, perché tutto questo trambusto con il sistema di configurazione e preferenza? Ricorda, il sistema delle preferenze risolve un problema di dominio per Spree. L'aggancio al processo di inizializzazione e l'accesso al framework Rails potrebbero aiutarti a soddisfare i tuoi requisiti in modo sostenibile.

Questo modello di progettazione del motore si concentra sull'utilizzo del framework Rails come costante tra le sue numerose parti mobili per memorizzare le impostazioni che (generalmente) non cambiano in fase di esecuzione, ma cambiano tra le installazioni dell'applicazione.

Se hai mai provato a etichettare un'applicazione Rails, potresti avere familiarità con questo scenario di preferenze e aver sentito il dolore delle tabelle "impostazioni" del database contorte all'interno di un lungo processo di installazione per ogni nuova applicazione. Ora sai che è disponibile un percorso diverso ed è fantastico: dai il cinque!

Raffineria CMS

Un sistema di gestione dei contenuti open source per Rails

Convenzione sulla configurazione chiunque? Rails Engines può sicuramente sembrare più un esercizio di configurazione a volte, ma RefineryCMS ricorda parte della magia di Rails. Questo è l'intero contenuto della sua directory lib :

 # lib/refinerycms.rb require 'refinery/all' # lib/refinery/all.rb %w(core authentication dashboard images resources pages).each do |extension| require "refinerycms-#{extension}" end

Oh. Se non puoi dirlo da questo, il team della raffineria sa davvero cosa stanno facendo. Rotolano con il concetto di extension che è in sostanza un altro motore. Come Spree, ha una gemma di cucitura avvolgente, ma utilizza solo due punti e riunisce una raccolta di motori per offrire la sua serie completa di funzionalità.

Le estensioni vengono create anche dagli utenti del motore, per creare il proprio mash-up di funzionalità CMS per blog, notizie, portfolio, testimonianze, richieste, ecc. (è una lunga lista), il tutto collegato al core RefineryCMS.

Questo design potrebbe attirare la tua attenzione per il suo approccio modulare e Refinery è un ottimo esempio di questo modello di progettazione Rails. "Come funziona?" Ti sento chiedere.

Il motore core mappa alcuni hook che gli altri motori possono utilizzare:

 # core/lib/refinery/engine.rb module Refinery module Engine def after_inclusion(&block) if block && block.respond_to?(:call) after_inclusion_procs << block else raise 'Anything added to be called after_inclusion must be callable (respond to #call).' end end def before_inclusion(&block) if block && block.respond_to?(:call) before_inclusion_procs << block else raise 'Anything added to be called before_inclusion must be callable (respond to #call).' end end private def after_inclusion_procs @@after_inclusion_procs ||= [] end def before_inclusion_procs @@before_inclusion_procs ||= [] end end end

Come puoi vedere, before_inclusion e after_inclusion memorizzano solo un elenco di processi che verranno eseguiti in seguito. Il processo di inclusione di Refinery estende le applicazioni Rails attualmente caricate con i controller e gli helper di Refinery. Eccone uno in azione:

 # authentication/lib/refinery/authentication/engine.rb before_inclusion do [Refinery::AdminController, ::ApplicationController].each do |c| Refinery.include_once(c, Refinery::AuthenticatedSystem) end end

Sono sicuro che hai già inserito metodi di autenticazione in ApplicationController e AdminController , questo è un modo programmatico per farlo.

Osservare il resto del file del motore di autenticazione ci aiuterà a raccogliere alcuni altri componenti chiave:

 module Refinery module Authentication class Engine < ::Rails::Engine extend Refinery::Engine isolate_namespace Refinery engine_name :refinery_authentication config.autoload_paths += %W( #{config.root}/lib ) initializer "register refinery_user plugin" do Refinery::Plugin.register do |plugin| plugin.pathname = root plugin.name = 'refinery_users' plugin.menu_match = %r{refinery/users$} plugin.url = proc { Refinery::Core::Engine.routes.url_helpers.admin_users_path } end end end config.after_initialize do Refinery.register_extension(Refinery::Authentication) end # ... end end

Sotto il cofano, le estensioni della raffineria utilizzano un sistema di Plugin -in. Il passaggio initializer sembrerà familiare dall'analisi del codice Spree, qui sono solo soddisfatti i requisiti dei metodi di register da aggiungere all'elenco di Refinery::Plugins di cui tiene traccia l'estensione core e Refinery.register_extension aggiunge semplicemente il nome del modulo in un elenco archiviato in una funzione di accesso di classe.

Ecco uno shock: la classe Refinery::Authentication è davvero un wrapper attorno a Devise, con alcune personalizzazioni. Quindi sono le tartarughe fino in fondo!

Le estensioni e i plug-in sono concetti sviluppati da Refinery per supportare il loro ricco ecosistema di app e strumenti mini-rails - think rake generate refinery:engine . Il modello di progettazione qui differisce da Spree imponendo un'API aggiuntiva attorno al Rails Engine per assistere nella gestione della loro composizione.

L'idioma "The Rails Way" è al centro di Refinery, sempre più presente nelle loro app mini-rails, ma dall'esterno non lo sapresti. Progettare i confini a livello di composizione dell'applicazione è importante, forse di più, della creazione di un'API pulita per le tue classi e moduli utilizzati all'interno delle tue applicazioni Rails.

Il wrapping del codice su cui non si ha il controllo diretto è un modello comune, è una previsione nel ridurre i tempi di manutenzione per quando quel codice cambia, limitando il numero di posti in cui sarà necessario apportare modifiche per supportare gli aggiornamenti. L'applicazione di questa tecnica insieme alla funzionalità di partizionamento crea una piattaforma flessibile per la composizione, ed ecco un esempio del mondo reale seduto proprio sotto il tuo naso: devo amare l'open source!

Conclusione


Abbiamo visto quattro approcci alla progettazione di modelli di motori Rails analizzando le gemme popolari utilizzate nelle applicazioni del mondo reale. Vale la pena leggere i loro repository per imparare da una vasta esperienza già applicata e ripetuta. Stare sulle spalle dei giganti.

In questa guida Rails, ci siamo concentrati sui modelli di progettazione e sulle tecniche per integrare i motori Rails e le applicazioni Rails dei loro utenti finali, in modo che tu possa aggiungere la conoscenza di questi alla cintura degli attrezzi Rails.

Spero che tu abbia imparato tanto quanto me dalla revisione di questo codice e ti senta ispirato a dare una possibilità a Rails Engines quando si adattano al conto. Un enorme grazie ai manutentori e ai contributori delle gemme che abbiamo recensito. Ottimo lavoro gente!

Relazionato: Troncamento timestamp: A Ruby on Rails ActiveRecord Tale