Hvorfor brukes ikke skinnemotorer oftere? Jeg vet ikke svaret, men jeg tror at generaliseringen av 'Everything is an Engine' har skjult problemdomenene de kan hjelpe til med å løse.
Det suverene Rails Guide dokumentasjon for å komme i gang med Rails Motorer refererer til fire populære eksempler på Rails Engine-implementeringer: Forem, Devise, Spree og RefineryCMS. Dette er fantastiske virkelige verdensbrukstilfeller for motorer som hver bruker en annen tilnærming til integrering med et Rails-program.
Å undersøke deler av hvordan disse perlene er konfigurert og komponert vil gi avansert Ruby on Rails-utviklere verdifull kunnskap om hvilke mønstre eller teknikker som blir prøvd ut i naturen, så når tiden kommer kan du ha noen ekstra muligheter å evaluere mot.
Jeg forventer at du har en kortvarig kjennskap til hvordan en motor fungerer, så hvis du føler at noe ikke helt legger opp, kan du lese den mest fremragende Rails Guide Komme i gang med motorer .
Uten videre, la oss våge oss i den ville verden av Rails-motoreksempler!
En motor for Rails som har som mål å være det beste lille forum-systemet noensinne
Denne perlen følger retningen til Rails Guide on Engines til punkt og prikke. Det er et betydelig eksempel, og å lese gjennom depotet ditt vil gi deg en ide om hvor langt du kan strekke det grunnleggende oppsettet.
Det er en enmotors perle som bruker et par teknikker for å integrere med hovedapplikasjonen.
module ::Forem class Engine Den interessante delen her er Decorators.register!
klassemetode, eksponert av dekoratørperlen. Den innkapsler innlasting av filer som ikke ville være inkludert i Rails autoloading-prosessen. Du husker kanskje at du bruker eksplisitt require
uttalelser ødelegger automatisk omlasting i utviklingsmodus, så dette er en livredder! Det vil være tydeligere å bruke eksemplet fra guiden for å illustrere hva som skjer:
config.to_prepare do Dir.glob(Rails.root + 'app/decorators/**/*_decorator*.rb').each do |c| require_dependency(c) end end
Det meste av magien for Forems konfigurasjon skjer i definisjonen av Forem
i hovedmodulen. Denne filen er avhengig av en user_class
variabel som blir satt i en initialiseringsfil:
Forem.user_class = 'User'
Du oppnår dette ved hjelp av mattr_accessor
men det er alt i Rails Guide, så jeg vil ikke gjenta det her. Med dette på plass, dekorerer Forem brukerklassen med alt den trenger for å kjøre applikasjonen:
module Forem class <'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? # ...
Noe som viser seg å være ganske mye! Jeg har klippet ut flertallet, men har lagt igjen en tilknytningsdefinisjon samt en forekomstmetode for å vise deg hvilken type linjer du kan finne der inne.
Et glimt av hele filen kan vise deg hvor håndterbar porting av en del av applikasjonen din for gjenbruk til en motor kan være.
Dekorering er navnet på spillet i standard motorbruk. Som sluttbruker av perlen kan du overstyre modell, visning og kontrollere ved å lage dine egne versjoner av klassene ved hjelp av filstier og filnavnkonvensjoner som er lagt ut i dekoratørperlen README. Det er imidlertid en kostnad forbundet med denne tilnærmingen, spesielt når motoren får en større versjonsoppgradering - vedlikeholdet av å holde dekorasjonene dine i bruk kan raskt komme ut av hånden. Jeg siterer ikke Forem her, jeg tror de er standhaftige i å følge en sammensveiset kjernefunksjonalitet, men husk dette hvis du lager en motor og bestemmer deg for å gå til en revisjon.
La oss oppsummere denne: dette er standard Rails-motordesignmønster som stole på at sluttbrukere dekorerer visninger, kontrollere og modeller, sammen med å konfigurere grunnleggende innstillinger via initialiseringsfiler. Dette fungerer bra for veldig fokusert og relatert funksjonalitet.
Motto
Du vil finne at en motor er veldig lik et Rails-program, med views
, controllers
og models
kataloger. Devise er et godt eksempel på å kapsle inn en applikasjon og avsløre et praktisk integreringspunkt. La oss gå gjennom hvordan akkurat det fungerer.
Du vil gjenkjenne disse kodelinjene hvis du har vært Rails-utvikler i mer enn noen få uker:
class User Hver parameter sendes til devise
metoden representerer en modul i Devise Engine. Det er ti av disse modulene som arver fra det kjente ActiveSupport::Concern
. Disse utvider User
klasse ved å påkalle devise
metode innenfor sitt virkeområde.
Å ha denne typen integreringspunkt er veldig fleksibel, du kan legge til eller fjerne noen av disse parameterne for å endre nivået på funksjonalitet du trenger at motoren skal utføre. Det betyr også at du ikke trenger å hardkode hvilken modell du vil bruke i en initialiseringsfil, som foreslått av Rails Guide on Engines. Dette er med andre ord ikke nødvendig:
Devise.user_model = 'User'
Denne abstraksjonen betyr også at du kan bruke dette på mer enn en brukermodell i samme applikasjon (admin
og user
for eksempel), mens konfigurasjonsfiltilnærmingen vil la deg være bundet til en enkelt modell med autentisering. Dette er ikke det største salgsargumentet, men det illustrerer en annen måte å løse et problem på.
Devise strekker seg ActiveRecord::Base
med sin egen modul som inkluderer devise
metode definisjon:
# lib/devise/orm/active_record.rb ActiveRecord::Base.extend Devise::Models
Enhver klasse som arver fra ActiveRecord::Base
vil nå ha tilgang til klassemetodene som er definert i 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
(Jeg har fjernet mye kode (# ...
) for å markere viktige deler.)
Omformulere koden for hvert modulnavn sendt til devise
metoden vi er:
- laster inn modulen vi spesifiserte som lever under
Devise::Models
(Devise::Models.const_get(m.to_s.classify
) - utvide
User
klasse med ClassMethods
modul hvis den har en - inkluderer den spesifiserte modulen (
include mod
) for å legge til instansmetodene til klassen som kaller devise
metode (User
)
Hvis du ønsker å lage en modul som kan lastes inn på denne måten, må du sørge for at den følger det vanlige ActiveSupport::Concern
grensesnitt, men navneområdet under Devise:Models
da det er her vi ønsker å hente konstanten:
module Devise module Models module Authenticatable extend ActiveSupport::Concern included do # ... end module ClassMethods # ... end end end end
Puh.
Hvis du har brukt Rails ’Concerns før og opplevd gjenbrukbarheten de har råd til, kan du sette pris på det fine med denne tilnærmingen. Kort sagt, å bryte opp funksjonalitet på denne måten gjør testing enklere ved å bli abstrahert fra en ActiveRecord
modell, og har lavere overhead enn standardmønsteret som brukes av Forem når det gjelder utvidelse av funksjonalitet.
Dette mønsteret består i å dele opp funksjonaliteten din i Rails Concerns og eksponere et konfigurasjonspunkt for å inkludere eller ekskludere disse innenfor et gitt omfang. En motor dannet på denne måten er praktisk for sluttbrukeren - en medvirkende faktor til suksessen og populariteten til Devise. Og nå vet du hvordan du gjør det også!
Spree
En komplett åpen kildekode-e-handelsløsning for Ruby on Rails
Spree gjennomgikk et kolossalt forsøk på å bringe deres monolitiske applikasjon under kontroll med å gå over til å bruke motorer. Arkitekturdesignet de nå ruller med er en 'Spree' -perle som inneholder mange motorperler.
Disse motorene lager partisjoner i atferd du kan være vant til å se i en monolitisk applikasjon eller spredt over applikasjoner:
- spree_api (RESTful API)
- spree_frontend (Brukervendte komponenter)
- spree_backend (Administrasjonsområde)
- spree_cmd (kommandolinjeverktøy)
- spree_core (Models & Mailers, de grunnleggende komponentene i Spree som den ikke kan kjøre uten)
- spree_sample (eksempeldata)
Den omfattende perlen syr disse sammen, slik at utvikleren har et valg i funksjonsnivået å kreve. For eksempel kan du løpe med bare spree_core
Motorer og vikle ditt eget grensesnitt rundt det.
Den viktigste Spree-perlen krever disse motorene:
# lib/spree.rb require 'spree_core' require 'spree_api' require 'spree_backend' require 'spree_frontend'
Hver motor må deretter tilpasse sin engine_name
og root
sti (sistnevnte peker vanligvis på perlen på øverste nivå) og konfigurerer seg ved å koble til initialiseringsprosessen:
# api/lib/spree/api/engine.rb require 'rails/engine' module Spree module Api class Engine :load_config_initializers do |app| app.config.spree = Spree::Core::Environment.new end # ... end end end
Du kan kanskje ikke gjenkjenne denne initialiseringsmetoden: den er en del av Railtie
og er en krok som gir deg muligheten til å legge til eller fjerne trinn fra initialiseringen av Rails-rammeverket. Spree stoler sterkt på denne kroken for å konfigurere det komplekse miljøet for alle motorene.
Ved å bruke eksemplet ovenfor under kjøretid, vil du få tilgang til innstillingene dine ved å gå til toppnivået Rails
konstant:
Rails.application.config.spree
Med denne Rails-motordesignguiden ovenfor kan vi kalle det en dag, men Spree har massevis av fantastisk kode, så la oss dykke inn i hvordan de bruker initialisering for å dele konfigurasjon mellom motorer og hovedsporapplikasjonen.
Spree har et komplekst preferansesystem som det lastes inn ved å legge til et trinn i initialiseringsprosessen:
# api/lib/spree/api/engine.rb initializer 'spree.environment', :before => :load_config_initializers do |app| app.config.spree = Spree::Core::Environment.new end
Her legger vi til app.config.spree
et nytt Spree::Core::Environment
forekomst. Innenfor skinnesøknaden vil du kunne få tilgang til dette via Rails.application.config.spree
hvor som helst - modeller, kontrollere, visninger.
Fortsetter nedover, Spree::Core::Environment
klasse vi lager ser slik ut:
module Spree module Core class Environment attr_accessor :preferences def initialize @preferences = Spree::AppConfiguration.new end end end end
Den avslører et :preferences
variabel satt til en ny forekomst av Spree::AppConfiguration
klasse, som igjen bruker en preference
metoden definert i Preferences::Configuration
klasse for å angi alternativer med standardinnstillinger for den generelle applikasjonskonfigurasjonen:
module Spree class AppConfiguration Jeg vil ikke vise Preferences::Configuration
fil fordi det tar litt forklaring, men egentlig er det syntaktisk sukker for å få og stille inn preferanser. (I sannhet er dette en forenkling av funksjonaliteten, ettersom preferansesystemet vil lagre andre enn standardverdier for eksisterende eller nye preferanser i databasen, for alle ActiveRecord
klasser med en :preference
kolonne - men du trenger ikke å vite det.)
Her er et av disse alternativene i aksjon:
module Spree class Calculator Kalkulatorer kontrollerer alle slags ting i Spree - fraktkostnader, kampanjer, produktjusteringer - så å ha en mekanisme for å bytte dem ut på denne måten øker motorens utvidbarhet.
En av de mange måtene du kan overstyre standardinnstillingene for disse innstillingene, er i en initialisering i hovedsporet:
# config/initializergs/spree.rb Spree::Config do |config| config.admin_interface_logo = 'company_logo.png' end
Hvis du har lest gjennom RailsGuide on Motors , vurderte designmønstrene deres eller bygde en motor selv, vil du vite at det er trivielt å eksponere en setter i en initialiseringsfil som noen kan bruke. Så du lurer kanskje på hvorfor alt oppstyret med oppsett- og preferansesystemet? Husk at preferansesystemet løser et domeneproblem for Spree. Å koble seg til initialiseringsprosessen og få tilgang til Rails-rammeverket kan hjelpe deg med å oppfylle dine behov på en vedlikeholdbar måte.
Dette motordesignmønsteret fokuserer på å bruke Rails-rammeverket som konstanten mellom de mange bevegelige delene for å lagre innstillinger som ikke (generelt) endres ved kjøretid, men som skifter mellom applikasjonsinstallasjoner.
Hvis du noen gang har prøvd det hvit etikett et Rails-program, kan du være kjent med dette innstillingsscenarioet, og har følt smerten ved innviklede databasens 'innstillinger' -tabeller i løpet av en lang installasjonsprosess for hvert nytt program. Nå vet du at en annen vei er tilgjengelig, og det er kjempebra - high five!
RaffineriCMS
Konvensjon om konfigurasjon noen? Rails Engines kan definitivt virke mer som en øvelse i konfigurasjon til tider, men RefineryCMS husker noe av den Rails-magien. Dette er hele innholdet i det lib
katalog:
# 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. Hvis du ikke vet hva dette vet, vet raffineriteamet virkelig hva de gjør. De ruller med konseptet med en extension
som egentlig er en annen motor. I likhet med Spree har den en omfattende sømperle, men bruker bare to masker, og samler en samling motorer for å levere sitt fulle sett med funksjonalitet.
Utvidelser er også opprettet av brukere av Engine, for å lage sin egen sammenblanding av CMS-funksjoner for blogging, nyheter, portefølje, attester, henvendelser osv. (Det er en lang liste), alt sammenkoblet til kjernen RefineryCMS.
Dette designet kan få oppmerksomhet for sin modulære tilnærming, og Refinery er et godt eksempel på dette Rails designmønsteret. 'Hvordan virker det?' Jeg hører deg spørre.
core
motoren kartlegger noen kroker for de andre motorene å bruke:
# 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
Som du kan se before_inclusion
og after_inclusion
bare lagre en liste over produkter som kjøres senere. Inkluderingsprosessen for raffineriet utvider de nåværende lastede Rails-applikasjonene med Refinery's kontrollere og hjelpere. Her er en i aksjon:
# authentication/lib/refinery/authentication/engine.rb before_inclusion do [Refinery::AdminController, ::ApplicationController].each do |c| Refinery.include_once(c, Refinery::AuthenticatedSystem) end end
Jeg er sikker på at du har lagt autentiseringsmetoder til ApplicationController
og AdminController
før, er dette en programmatisk måte å gjøre det på.
Når vi ser på resten av Authentication Engine-filen, kan vi finne et par andre viktige komponenter:
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
Under panseret bruker raffineriutvidelser en Plugin
system. initializer
trinn vil se kjent ut fra Spree-kodeanalysen, her var det bare å møte register
metodekrav som skal legges til i listen over Refinery::Plugins
at core
utvidelse holder oversikt over, og Refinery.register_extension
bare legger til modulnavnet i en liste lagret i en klassetilgang.
Her er en sjokkerer: Refinery::Authentication
klasse er virkelig en innpakning rundt Devise, med litt tilpasning. Så det er skilpadder helt ned!
Utvidelsene og pluginene er konsepter Refinery har utviklet for å støtte sitt rike økosystem med mini-rails-apper og verktøy - tenk rake generate refinery:engine
. Designmønsteret her skiller seg fra Spree ved å pålegge en ekstra API rundt Rails Engine for å hjelpe til med å administrere sammensetningen.
“The Rails Way” -idiomet er kjernen i Refinery, stadig mer til stede i deres mini-rails-apper, men utenfra ville du ikke vite det. Å designe grenser på applikasjonssammensetningsnivå er like viktig, muligens mer, enn å lage et rent API for dine klasser og moduler som brukes i Rails-applikasjonene dine.
Innpakningskode som du ikke har direkte kontroll over er et vanlig mønster. Det er en framsyn for å redusere vedlikeholdstiden for når den koden endres, og begrenser antall steder du trenger å gjøre endringer for å støtte oppgraderinger. Bruk av denne teknikken sammen med partisjoneringsfunksjonalitet skaper en fleksibel plattform for komposisjon, og her er et eksempel fra den virkelige verden rett under nesen din - må elske åpen kildekode!
Konklusjon
Vi har sett fire tilnærminger for å designe Rails-motormønstre ved å analysere populære perler som brukes i virkelige applikasjoner. Det er verdt å lese gjennom arkivene deres for å lære av et vell av erfaring som allerede er brukt og gjentatt. Stå på skuldrene til kjemper.
I denne Rails-guiden har vi fokusert på designmønstre og teknikker for integrering av Rails Engines og deres sluttbrukers Rails-applikasjoner, slik at du kan legge til kunnskapen om disse til din Skinner verktøybelte .
Jeg håper du har lært like mye som jeg av å ha gjennomgått denne koden og føler meg inspirert til å gi Rails Engines en sjanse når de passer til regningen. En stor takk til vedlikeholdere og bidragsytere til perlene vi gjennomgikk. Stor jobb folk!
I slekt: Avkorting av tidsstempel: En Ruby on Rails ActiveRecord Tale