Un ghid pentru motoarele cu șine în sălbăticie: exemple din lumea reală de motoare cu șine în acțiune

Publicat: 2022-03-11

De ce motoarele Rails nu sunt folosite mai des? Nu știu răspunsul, dar cred că generalizarea „Totul este un motor” a ascuns domeniile problemei pe care le pot ajuta să le rezolve.

Superba documentație Rails Guide pentru începerea utilizării Rails Engines face referire la patru exemple populare de implementări Rails Engine: Forem, Devise, Spree și RefineryCMS. Acestea sunt cazuri de utilizare fantastice în lumea reală pentru motoare, fiecare folosind o abordare diferită a integrării cu o aplicație Rails.

Fiecare ghid Rails ar trebui să acopere subiectul modelelor de proiectare a motoarelor Rails și exemplele acestora.

Examinarea unor părți din modul în care sunt configurate și compuse aceste pietre prețioase le va oferi dezvoltatorilor avansați Ruby on Rails cunoștințe valoroase despre modelele sau tehnicile care sunt încercate și testate în sălbăticie, astfel încât, atunci când va veni timpul, puteți avea câteva opțiuni suplimentare pentru a evalua.

Mă aștept să cunoașteți rapid modul în care funcționează un motor, așa că, dacă simțiți că ceva nu se adună, vă rugăm să citiți cel mai excelent Ghid pentru șine Noțiuni introductive despre motoare .

Fără alte prelungiri, să ne aventurăm în lumea sălbatică a exemplelor de motoare Rails!

Forem

Un motor pentru Rails care își propune să fie cel mai bun sistem de forum mic de până acum

Această bijuterie urmează direcția Ghidului șinelor pentru motoare la literă. Este un exemplu considerabil și examinarea depozitului său vă va oferi o idee despre cât de mult puteți extinde configurația de bază.

Este o bijuterie cu un singur motor care folosește câteva tehnici pentru a se integra cu aplicația principală.

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

Partea interesantă aici este Decorators.register! metoda clasei, expusă de bijuteria Decoratori. Acesta încapsulează fișierele de încărcare care nu ar fi incluse în procesul de încărcare automată Rails. Poate vă amintiți că utilizarea declarațiilor explicite require ruinează reîncărcarea automată în modul de dezvoltare, așa că aceasta este o salvare! Va fi mai clar să folosiți exemplul din Ghid pentru a ilustra ceea ce se întâmplă:

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

Cea mai mare parte a magiei pentru configurația lui Forem are loc în definiția de sus a modulului principal din Forem . Acest fișier se bazează pe o variabilă user_class care este setată într-un fișier de inițializare:

 Forem.user_class = "User"

Reușiți acest lucru folosind mattr_accessor , dar totul este în Ghidul șinelor, așa că nu voi repeta asta aici. Cu acest lucru în loc, Forem decorează apoi clasa de utilizator cu tot ce are nevoie pentru a-și rula aplicația:

 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? # ...

Ceea ce se dovedește a fi destul de mult! Am tăiat majoritatea, dar am lăsat o definiție de asociere, precum și o metodă de instanță pentru a vă arăta tipul de linii pe care le puteți găsi acolo.

O privire asupra întregului fișier vă poate arăta cât de ușor de gestionat ar putea fi portarea unei părți a aplicației dvs. pentru reutilizare la un motor.

Decorarea este numele jocului în utilizarea implicită a motorului. În calitate de utilizator final al bijuteriei, puteți suprascrie modelul, vizualizarea și controlerele prin crearea propriilor versiuni ale claselor folosind calea fișierului și convențiile de denumire a fișierelor stabilite în bijuteria de decorare README. Cu toate acestea, există un cost asociat cu această abordare, mai ales atunci când Motorul primește o actualizare majoră a versiunii - întreținerea menținerii decorațiunilor în funcțiune poate scăpa rapid de sub control. Nu citez Forem aici, cred că sunt fermi în a păstra o funcționalitate de bază strânsă, dar țineți cont de acest lucru dacă creați un motor și decideți să mergeți la o revizie.

Să recapitulăm pe acesta: acesta este modelul implicit de proiectare a motorului Rails, bazându-se pe utilizatorii finali care decorează vederi, controlere și modele, împreună cu configurarea setărilor de bază prin fișiere de inițializare. Acest lucru funcționează bine pentru funcționalități foarte concentrate și asociate.

Inventează

O soluție flexibilă de autentificare pentru Rails

Veți găsi că un Engine este foarte asemănător cu o aplicație Rails, cu views , controllers și directoare de models . Devise este un bun exemplu de încapsulare a unei aplicații și de expunere a unui punct de integrare convenabil. Să vedem cum funcționează exact asta.

Veți recunoaște aceste linii de cod dacă sunteți dezvoltator Rails de mai mult de câteva săptămâni:

 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

Fiecare parametru transmis metodei devise reprezintă un modul din Devise Engine. Există în total zece dintre aceste module care moștenesc din familiarul ActiveSupport::Concern . Acestea vă extind clasa de User prin invocarea metodei devise în domeniul său.

Având acest tip de punct de integrare este foarte flexibil, puteți adăuga sau elimina oricare dintre acești parametri pentru a schimba nivelul de funcționalitate pe care trebuie să îl realizeze Motorul. De asemenea, înseamnă că nu trebuie să codificați modelul pe care doriți să îl utilizați într-un fișier de inițializare, așa cum este sugerat de Rails Guide on Engines. Cu alte cuvinte, acest lucru nu este necesar:

 Devise.user_model = 'User'

Această abstractizare înseamnă, de asemenea, că puteți aplica acest lucru la mai mult de un model de utilizator în cadrul aceleiași aplicații ( admin și user , de exemplu), în timp ce abordarea fișierului de configurare vă va lăsa legat de un singur model cu autentificare. Acesta nu este cel mai mare argument de vânzare, dar ilustrează o modalitate diferită de a rezolva o problemă.

Devise extinde ActiveRecord::Base cu propriul modul care include definiția metodei devise :

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

Orice clasă care moștenește de la ActiveRecord::Base va avea acum acces la metodele de clasă definite în 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

(Am eliminat o mulțime de coduri ( # ... ) pentru a evidenția părțile importante.)

Parafrazând codul, pentru fiecare nume de modul transmis metodei devise suntem:

  • la încărcarea modulului am specificat că locuiește sub Devise::Models ( Devise::Models.const_get(m.to_s.classify )
  • extinderea clasei User cu modulul ClassMethods dacă are unul
  • include modulul specificat ( include mod ) pentru a adăuga metodele sale de instanță la clasa care apelează metoda devise ( User )

Dacă doriți să creați un modul care ar putea fi încărcat în acest fel, ar trebui să vă asigurați că a urmat interfața obișnuită ActiveSupport::Concern , dar spațiul de nume este sub Devise:Models , deoarece aici căutăm să recuperăm constanta:

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

Uf.

Dacă ați mai folosit Rails' Concerns și ați experimentat reutilizabilitatea pe care o oferă, atunci puteți aprecia frumusețea acestei abordări. Pe scurt, ruperea funcționalității în acest fel face testarea mai ușoară, fiind extrasă dintr-un model ActiveRecord și are o suprasarcină mai mică decât modelul implicit utilizat de Forem atunci când vine vorba de extinderea funcționalității.

Acest model constă în împărțirea funcționalității dumneavoastră în Rails Concerns și expunerea unui punct de configurare pentru a le include sau exclude într-un anumit domeniu. Un motor format în acest mod este convenabil pentru utilizatorul final – un factor care contribuie la succesul și popularitatea Devise. Și acum știi și tu cum să o faci!

Sindrofie

O soluție completă de comerț electronic cu sursă deschisă pentru Ruby on Rails

Spree a trecut printr-un efort colosal pentru a-și controla aplicația monolitică cu trecerea la utilizarea Motoarelor. Designul arhitecturii cu care se rulează acum este o bijuterie „Spree” care conține multe pietre prețioase Engine.

Aceste motoare creează partiții în comportamentul pe care ați putea fi obișnuit să le vedeți într-o aplicație monolitică sau răspândite între aplicații:

  • spree_api (API-ul RESTful)
  • spree_frontend (componente orientate către utilizator)
  • spree_backend (zona de administrare)
  • spree_cmd (instrumente din linia de comandă)
  • spree_core (Modele și Mailers, componentele de bază ale Spree de care nu poate rula)
  • spree_sample (Eșantion de date)

Bijuteria cuprinzătoare le unește, lăsând dezvoltatorului posibilitatea de a alege în ceea ce privește nivelul de funcționalitate necesar. De exemplu, ai putea rula doar cu motorul spree_core și să-ți închei propria interfață în jurul acestuia.

Principala bijuterie Spree necesită aceste motoare:

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

Fiecare motor trebuie apoi să își personalizeze engine_name și calea root (cea din urmă indicând de obicei către bijuteria de nivel superior) și să se configureze prin conectarea la procesul de inițializare:

 # 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

Puteți recunoaște sau nu această metodă de inițializare: face parte din Railtie și este un cârlig care vă oferă posibilitatea de a adăuga sau elimina pași din inițializarea cadrului Rails. Spree se bazează foarte mult pe acest cârlig pentru a-și configura mediul complex pentru toate motoarele sale.

Folosind exemplul de mai sus în timpul execuției, veți avea acces la setările dvs. accesând constanta Rails de nivel superior:

 Rails.application.config.spree

Cu acest ghid de model de proiectare a motorului Rails de mai sus, l-am putea numi o zi, dar Spree are o mulțime de cod uimitor, așa că haideți să vedem cum utilizează inițializarea pentru a partaja configurația între Motoare și aplicația principală Rails.

Spree are un sistem de preferințe complex pe care îl încarcă adăugând un pas în procesul de inițializare:

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

Aici, atașăm la app.config.spree o nouă instanță Spree::Core::Environment . În cadrul aplicației șine veți putea accesa aceasta prin Rails.application.config.spree de oriunde - modele, controlere, vederi.

Mergând în jos, clasa Spree::Core::Environment pe care o creăm arată astfel:

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

Expune o variabilă :preferences setată la o nouă instanță a clasei Spree::AppConfiguration , care la rândul său utilizează o metodă de preference definită în clasa Preferences::Configuration pentru a seta opțiuni cu valori implicite pentru configurația generală a aplicației:

 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

Nu voi afișa fișierul Preferences::Configuration pentru că va fi nevoie de puțină explicație, dar în esență este zahăr sintactic pentru obținerea și setarea preferințelor. (În adevăr, aceasta este o simplificare excesivă a funcționalității sale, deoarece sistemul de preferințe va salva alte valori decât cele implicite pentru preferințele existente sau noi în baza de date, pentru orice clasă ActiveRecord cu o coloană :preference - dar nu trebuie să știi asta.)

Iată una dintre aceste opțiuni în acțiune:

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

Calculatoarele controlează tot felul de lucruri în Spree – costuri de transport, promoții, ajustări ale prețului produselor – așa că existența unui mecanism care să le schimbe în acest mod crește extensibilitatea motorului.

Unul dintre multele moduri prin care puteți suprascrie setările implicite pentru aceste preferințe este într-un inițializator din aplicația principală Rails:

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

Dacă ați citit RailsGuide despre motoare, ați luat în considerare modelele lor de proiectare sau ați construit singur un motor, veți ști că este trivial să expuneți un setter într-un fișier de inițializare pentru ca cineva să-l folosească. Deci s-ar putea să vă întrebați, de ce toată agitația cu sistemul de configurare și preferințe? Amintiți-vă, sistemul de preferințe rezolvă o problemă de domeniu pentru Spree. Conectarea la procesul de inițializare și obținerea accesului la cadrul Rails vă poate ajuta să vă îndepliniți cerințele într-un mod care poate fi întreținut.

Acest model de proiectare a motorului se concentrează pe utilizarea cadrului Rails ca constantă între numeroasele sale părți în mișcare pentru a stoca setări care nu se schimbă (în general) în timpul rulării, dar se schimbă între instalările aplicației.

Dacă ați încercat vreodată să etichetați o aplicație Rails, este posibil să fiți familiarizat cu acest scenariu de preferințe și să fi simțit durerea tabelelor complicate de „setări” bazei de date într-un proces lung de configurare pentru fiecare aplicație nouă. Acum știi că este disponibilă o cale diferită și asta este minunat - high five!

RafinărieCMS

Un sistem open source de gestionare a conținutului pentru Rails

Convenție asupra configurației cineva? Rails Engines poate părea uneori mai mult ca un exercițiu de configurare, dar RefineryCMS își amintește o parte din magia Rails. Acesta este întregul conținut al directorului său 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

Wow. Dacă nu vă puteți da seama după asta, echipa Rafinăriei știe cu adevărat ce fac. Ei rulează cu conceptul de extension care este în esență un alt motor. La fel ca Spree, are o bijuterie cuprinzătoare de cusături, dar folosește doar două cusături și reunește o colecție de motoare pentru a oferi setul complet de funcționalități.

Extensiile sunt, de asemenea, create de utilizatorii Engine, pentru a-și crea propriul amestec de funcții CMS pentru blogging, știri, portofoliu, mărturii, întrebări etc. (este o listă lungă), toate agățate în RefineryCMS de bază.

Acest design vă poate atrage atenția pentru abordarea sa modulară, iar Refinery este un exemplu excelent al acestui model de design Rails. "Cum functioneazã?" Te aud întrebi.

Motorul core prezintă câteva cârlige pentru ca celelalte motoare să le folosească:

 # 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

După cum puteți vedea, before_inclusion și after_inclusion doar stochează o listă de procese care vor fi rulate mai târziu. Procesul de includere a rafinăriei extinde aplicațiile Rails încărcate în prezent cu controlere și ajutoare ale rafinăriei. Iată unul în acțiune:

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

Sunt sigur că ați introdus metode de autentificare în ApplicationController și AdminController înainte, acesta este un mod programatic de a face acest lucru.

Privind restul fișierului motor de autentificare ne va ajuta să culegem alte câteva componente cheie:

 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

Sub capotă, extensiile Rafinării folosesc un sistem Plugin . Pasul de initializer va părea familiar din analiza codului Spree, aici doar îndeplinesc cerințele metodelor de register pentru a fi adăugate la lista de Refinery::Plugins -uri pe care extensia de core le ține evidența, iar Refinery.register_extension doar adaugă numele modulului la o listă stocată într-un accesoriu de clasă.

Iată un șocant: clasa Refinery::Authentication este într-adevăr un înveliș în jurul lui Devise, cu unele personalizări. Deci sunt țestoase până jos!

Extensiile și pluginurile sunt concepte pe care Refinery le-a dezvoltat pentru a-și susține ecosistemul bogat de aplicații și instrumente mini-șine - think rake generate refinery:engine . Modelul de design aici diferă de Spree prin impunerea unui API suplimentar în jurul Rails Engine pentru a ajuta la gestionarea compoziției acestora.

Expresia „The Rails Way” se află în centrul Rafinării, din ce în ce mai prezentă în aplicațiile lor de mini-șine, dar din exterior nu ai ști asta. Proiectarea limitelor la nivelul compoziției aplicației este la fel de importantă, poate mai mult decât crearea unui API curat pentru clasele și modulele utilizate în aplicațiile Rails.

Încheierea codului asupra căruia nu aveți control direct este un model comun, este o prevedere în reducerea timpului de întreținere pentru momentul în care codul se schimbă, limitând numărul de locuri în care va trebui să faceți modificări pentru a susține upgrade-uri. Aplicarea acestei tehnici alături de funcționalitatea de partiționare creează o platformă flexibilă pentru compoziție, iar iată un exemplu din lumea reală care se află chiar sub nasul tău - trebuie să iubești sursa deschisă!

Concluzie


Am văzut patru abordări pentru proiectarea modelelor de motoare Rails prin analizarea pietrelor prețioase populare utilizate în aplicațiile din lumea reală. Merită să citiți prin depozitele lor pentru a învăța dintr-o mulțime de experiență deja aplicată și repetată. Stați pe umerii giganților.

În acest ghid Rails, ne-am concentrat pe modelele de proiectare și tehnicile de integrare a Rails Engines și a aplicațiilor Rails ale utilizatorilor finali, astfel încât să puteți adăuga cunoștințele despre acestea la centura dvs. de scule Rails.

Sper că ați învățat la fel de mult ca mine din revizuirea acestui cod și să vă simțiți inspirați să oferiți Rails Engines o șansă atunci când se potrivesc. Un mare mulțumire întreținătorilor și colaboratorilor la pietrele pe care le-am analizat. Super treabă oameni buni!

Înrudit : Truncarea marcajului de timp: O poveste Ruby on Rails ActiveRecord