Ein Leitfaden für Rails-Engines in freier Wildbahn: Reale Beispiele für Rails-Engines in Aktion

Veröffentlicht: 2022-03-11

Warum werden Rails Engines nicht häufiger verwendet? Ich kenne die Antwort nicht, aber ich denke, dass die Verallgemeinerung von „Alles ist eine Maschine“ die Problembereiche verborgen hat, zu deren Lösung sie beitragen können.

Die hervorragende Rails Guide-Dokumentation für den Einstieg in Rails Engines verweist auf vier beliebte Beispiele für Rails Engine-Implementierungen: Forem, Devise, Spree und RefineryCMS. Dies sind fantastische reale Anwendungsfälle für Engines, die jeweils einen anderen Ansatz zur Integration in eine Rails-Anwendung verwenden.

Jeder Rails-Leitfaden sollte das Thema Entwurfsmuster für Rails-Engines und deren Beispiele behandeln.

Durch die Untersuchung von Teilen der Konfiguration und Zusammensetzung dieser Edelsteine ​​erhalten fortgeschrittene Ruby on Rails-Entwickler wertvolles Wissen darüber, welche Muster oder Techniken in der Wildnis erprobt und getestet wurden, sodass Sie zu gegebener Zeit einige zusätzliche Optionen zur Bewertung haben können.

Ich erwarte, dass Sie mit der Funktionsweise einer Engine oberflächlich vertraut sind. Wenn Sie also das Gefühl haben, dass etwas nicht ganz zusammenpasst, lesen Sie bitte den hervorragenden Rails-Leitfaden Erste Schritte mit Engines .

Lassen Sie uns ohne weiteres in die wilde Welt der Beispiele für Rails-Engines eintauchen!

Forem

Eine Engine für Rails, die darauf abzielt, das beste kleine Forumssystem aller Zeiten zu sein

Dieses Juwel folgt buchstabengetreu den Anweisungen des Rails Guide on Engines. Es ist ein ansehnliches Beispiel, und wenn Sie das Repository durchsehen, erhalten Sie eine Vorstellung davon, wie weit Sie die grundlegende Einrichtung erweitern können.

Es ist ein Single-Engine-Juwel, das einige Techniken zur Integration in die Hauptanwendung verwendet.

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

Der interessante Teil hier ist das Decorators.register! -Klassenmethode, die vom Decorators-Gem verfügbar gemacht wird. Es kapselt das Laden von Dateien ein, die nicht in den automatischen Ladevorgang von Rails einbezogen würden. Sie erinnern sich vielleicht, dass die Verwendung expliziter require -Anweisungen das automatische Neuladen im Entwicklungsmodus ruiniert, also ist dies ein Lebensretter! Es wird klarer, das Beispiel aus dem Leitfaden zu verwenden, um zu veranschaulichen, was passiert:

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

Der größte Teil der Magie für die Konfiguration von Forem geschieht in der obersten Hauptmoduldefinition von Forem . Diese Datei basiert auf einer user_class Variablen, die in einer Initialisierungsdatei festgelegt wird:

 Forem.user_class = "User"

Sie erreichen dies mit mattr_accessor , aber es steht alles im Rails Guide, also werde ich das hier nicht wiederholen. Wenn dies vorhanden ist, stattet Forem die Benutzerklasse mit allem aus, was zum Ausführen seiner Anwendung erforderlich ist:

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

Was sich als ziemlich viel herausstellt! Ich habe die Mehrheit herausgeschnitten, aber eine Assoziationsdefinition sowie eine Instanzmethode hinterlassen, um Ihnen zu zeigen, welche Art von Zeilen Sie dort finden können.

Ein Blick auf die gesamte Datei kann Ihnen zeigen, wie einfach es sein kann, einen Teil Ihrer Anwendung zur Wiederverwendung auf eine Engine zu portieren.

Dekorieren ist der Name des Spiels in der Standard-Engine-Nutzung. Als Endbenutzer des Gems können Sie Modell, Ansicht und Controller überschreiben, indem Sie Ihre eigenen Versionen der Klassen erstellen, indem Sie die Dateipfad- und Dateibenennungskonventionen verwenden, die in der Readme-Datei des Decorator-Gems festgelegt sind. Dieser Ansatz ist jedoch mit Kosten verbunden, insbesondere wenn die Engine ein größeres Versions-Upgrade erhält – die Wartung Ihrer Dekorationen kann schnell außer Kontrolle geraten. Ich zitiere hier nicht Forem, ich glaube, dass sie unerschütterlich an einer engmaschigen Kernfunktionalität festhalten, aber bedenken Sie dies, wenn Sie eine Engine erstellen und sich für eine Überholung entscheiden.

Fassen wir das zusammen: Dies ist das standardmäßige Rails-Engine-Designmuster, das darauf beruht, dass Endbenutzer Views, Controller und Modelle dekorieren und grundlegende Einstellungen über Initialisierungsdateien konfigurieren. Dies funktioniert gut für sehr fokussierte und verwandte Funktionen.

Entwickeln

Eine flexible Authentifizierungslösung für Rails

Sie werden feststellen, dass eine Engine einer Rails-Anwendung sehr ähnlich ist, mit views , controllers und models -Verzeichnissen. Devise ist ein gutes Beispiel für die Kapselung einer Anwendung und die Bereitstellung eines bequemen Integrationspunkts. Lassen Sie uns durchgehen, wie genau das funktioniert.

Sie werden diese Codezeilen erkennen, wenn Sie seit mehr als ein paar Wochen Rails-Entwickler sind:

 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

Jeder an die Methode devise übergebene Parameter repräsentiert ein Modul innerhalb der Devise Engine. Insgesamt gibt es zehn dieser Module, die vom bekannten ActiveSupport::Concern erben. Diese erweitern Ihre User -Klasse, indem sie die devise -Methode innerhalb ihres Gültigkeitsbereichs aufrufen.

Da diese Art von Integrationspunkt sehr flexibel ist, können Sie jeden dieser Parameter hinzufügen oder entfernen, um die Funktionalitätsebene zu ändern, die die Engine ausführen soll. Es bedeutet auch, dass Sie nicht fest codieren müssen, welches Modell Sie in einer Initialisierungsdatei verwenden möchten, wie im Rails Guide on Engines vorgeschlagen. Mit anderen Worten, dies ist nicht erforderlich:

 Devise.user_model = 'User'

Diese Abstraktion bedeutet auch, dass Sie dies auf mehr als ein Benutzermodell innerhalb derselben Anwendung anwenden können (z. B. admin und user ), während Sie beim Ansatz der Konfigurationsdatei an ein einziges Modell mit Authentifizierung gebunden wären. Dies ist nicht das größte Verkaufsargument, aber es veranschaulicht einen anderen Weg, ein Problem zu lösen.

Devise erweitert ActiveRecord::Base um ein eigenes Modul, das die devise -Methodendefinition enthält:

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

Jede Klasse, die von ActiveRecord::Base erbt, hat jetzt Zugriff auf die Klassenmethoden, die in Devise::Models definiert sind:

 #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

(Ich habe viel Code entfernt ( # ... ), um die wichtigen Teile hervorzuheben. )

Paraphrasieren des Codes, für jeden Modulnamen, der an die devise -Methode übergeben wird, sind wir:

  • Laden des von uns angegebenen Moduls, das sich unter Devise::Models befindet ( Devise::Models.const_get(m.to_s.classify )
  • Erweitern der User -Klasse mit dem ClassMethods -Modul, falls vorhanden
  • das angegebene Modul ( include mod ) einbinden, um seine Instanzmethoden der Klasse hinzuzufügen, die die devise -Methode aufruft ( User )

Wenn Sie ein Modul erstellen möchten, das auf diese Weise geladen werden kann, müssen Sie sicherstellen, dass es der üblichen ActiveSupport::Concern -Schnittstelle folgt, aber es unter Devise:Models benennen, da wir hier suchen, um die Konstante abzurufen:

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

Puh.

Wenn Sie Rails' Concerns schon einmal verwendet und die Wiederverwendbarkeit erlebt haben, die sie bieten, dann werden Sie die Feinheiten dieses Ansatzes zu schätzen wissen. Kurz gesagt, das Aufteilen der Funktionalität auf diese Weise erleichtert das Testen, indem es von einem ActiveRecord -Modell abstrahiert wird, und hat einen geringeren Overhead als das Standardmuster, das von Forem verwendet wird, wenn es um die Erweiterung der Funktionalität geht.

Dieses Muster besteht darin, Ihre Funktionalität in Rails Concerns aufzuteilen und einen Konfigurationspunkt bereitzustellen, um diese innerhalb eines bestimmten Bereichs einzubeziehen oder auszuschließen. Eine auf diese Weise gebildete Engine ist für den Endbenutzer bequem – ein Faktor, der zum Erfolg und zur Popularität von Devise beiträgt. Und jetzt weißt du auch, wie es geht!

Spree

Eine vollständige Open-Source-E-Commerce-Lösung für Ruby on Rails

Spree unternahm enorme Anstrengungen, um ihre monolithische Anwendung mit der Umstellung auf die Verwendung von Engines unter Kontrolle zu bringen. Das Architekturdesign, mit dem sie jetzt rollen, ist ein „Spree“-Juwel, das viele Engine-Juwelen enthält.

Diese Engines erstellen Partitionen in einem Verhalten, das Sie möglicherweise innerhalb einer monolithischen Anwendung oder über Anwendungen verteilt sehen:

  • spree_api (RESTful-API)
  • spree_frontend (Benutzerseitige Komponenten)
  • spree_backend (Admin-Bereich)
  • spree_cmd (Befehlszeilentools)
  • spree_core (Models & Mailers, die Grundkomponenten von Spree, ohne die es nicht laufen kann)
  • spree_sample (Beispieldaten)

Der umfassende Edelstein fügt diese zusammen und überlässt dem Entwickler die Wahl hinsichtlich des erforderlichen Funktionsumfangs. Sie könnten zum Beispiel nur mit der spree_core Engine laufen und Ihre eigene Schnittstelle darum wickeln.

Das Hauptjuwel der Spree benötigt diese Motoren:

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

Jede Engine muss dann ihren engine_name und root Pfad anpassen (letzterer zeigt normalerweise auf das Top-Level-Gem) und sich selbst konfigurieren, indem sie sich in den Initialisierungsprozess einklinkt:

 # 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

Sie können diese Initialisierungsmethode erkennen oder auch nicht: Sie ist Teil von Railtie und ein Hook, der Ihnen die Möglichkeit gibt, Schritte zur Initialisierung des Rails-Frameworks hinzuzufügen oder zu entfernen. Spree verlässt sich stark auf diesen Hook, um seine komplexe Umgebung für alle seine Engines zu konfigurieren.

Wenn Sie das obige Beispiel zur Laufzeit verwenden, haben Sie Zugriff auf Ihre Einstellungen, indem Sie auf die Rails -Konstante der obersten Ebene zugreifen:

 Rails.application.config.spree

Mit diesem Leitfaden zum Design von Rails-Engines oben könnten wir es einen Tag nennen, aber Spree hat eine Menge erstaunlichen Code, also lassen Sie uns eintauchen, wie sie die Initialisierung verwenden, um die Konfiguration zwischen Engines und der Haupt-Rails-Anwendung zu teilen.

Spree hat ein komplexes Präferenzsystem, das geladen wird, indem ein Schritt in den Initialisierungsprozess eingefügt wird:

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

Hier hängen wir an app.config.spree eine neue Spree::Core::Environment -Instanz an. Innerhalb der Rails-Anwendung können Sie über Rails.application.config.spree von überall darauf zugreifen - Modelle, Controller, Ansichten.

Weiter unten sieht die Klasse Spree::Core::Environment , die wir erstellen, so aus:

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

Es stellt eine :preferences -Variablen bereit, die einer neuen Instanz der Spree::AppConfiguration -Klasse zugewiesen wird, die wiederum eine in der preference Preferences::Configuration -Klasse definierte Einstellungsmethode verwendet, um Optionen mit Standardwerten für die allgemeine Anwendungskonfiguration festzulegen:

 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

Ich werde die Preferences::Configuration -Datei nicht zeigen, weil es ein wenig Erklärung braucht, aber im Wesentlichen ist es syntaktischer Zucker zum Abrufen und Festlegen von Einstellungen. (In Wahrheit ist dies eine zu starke Vereinfachung seiner Funktionalität, da das Präferenzsystem andere als die Standardwerte für vorhandene oder neue Präferenzen in der Datenbank für jede ActiveRecord -Klasse mit einer :preference -Spalte speichert - aber Sie müssen es nicht weiß das.)

Hier ist eine dieser Optionen in Aktion:

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

Kalkulatoren steuern alle möglichen Dinge in Spree – Versandkosten, Werbeaktionen, Produktpreisanpassungen –, sodass ein Mechanismus zum Austauschen dieser Werte die Erweiterbarkeit der Engine erhöht.

Eine der vielen Möglichkeiten, wie Sie die Standardeinstellungen für diese Einstellungen überschreiben können, ist innerhalb eines Initialisierers in der Rails-Hauptanwendung:

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

Wenn Sie den RailsGuide zu Engines gelesen, ihre Entwurfsmuster betrachtet oder selbst eine Engine gebaut haben, wissen Sie, dass es trivial ist, einen Setter in einer Initialisierungsdatei für jemanden verfügbar zu machen. Sie fragen sich vielleicht, warum all die Aufregung mit dem Einrichtungs- und Einstellungssystem? Denken Sie daran, dass das Präferenzsystem ein Domänenproblem für Spree löst. Wenn Sie sich in den Initialisierungsprozess einklinken und Zugriff auf das Rails-Framework erhalten, können Sie Ihre Anforderungen auf wartbare Weise erfüllen.

Dieses Engine-Entwurfsmuster konzentriert sich auf die Verwendung des Rails-Frameworks als Konstante zwischen seinen vielen beweglichen Teilen, um Einstellungen zu speichern, die sich (im Allgemeinen) nicht zur Laufzeit ändern, sich jedoch zwischen Anwendungsinstallationen ändern.

Wenn Sie jemals versucht haben, eine Rails-Anwendung mit einem Whitelabel zu versehen, sind Sie vielleicht mit diesem Einstellungsszenario vertraut und haben den Schmerz verworrener Datenbank-„Einstellungstabellen“ innerhalb eines langen Einrichtungsprozesses für jede neue Anwendung gespürt. Jetzt wissen Sie, dass ein anderer Weg verfügbar ist, und das ist großartig - High Five!

Raffinerie-CMS

Ein Open-Source-Content-Management-System für Rails

Konvention über Konfiguration irgendjemand? Rails Engines können manchmal definitiv eher wie eine Übung in der Konfiguration erscheinen, aber RefineryCMS erinnert sich an etwas von dieser Rails-Magie. Dies ist der gesamte Inhalt des lib -Verzeichnisses:

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

Beeindruckend. Wenn Sie es nicht erkennen können, das Refinery-Team weiß wirklich, was es tut. Sie rollen mit dem Konzept einer extension , die im Wesentlichen eine andere Engine ist. Wie Spree hat es ein umfassendes Stitching-Juwel, verwendet jedoch nur zwei Stiche und vereint eine Sammlung von Engines, um seine volle Funktionalität bereitzustellen.

Erweiterungen werden auch von Benutzern der Engine erstellt, um ihr eigenes Mashup von CMS-Funktionen für Blogging, Nachrichten, Portfolios, Testimonials, Anfragen usw. zu erstellen (es ist eine lange Liste), die sich alle in das Kern-CMS von Refinery einklinken.

Dieses Design kann Ihre Aufmerksamkeit wegen seines modularen Ansatzes erregen, und Refinery ist ein großartiges Beispiel für dieses Rails-Designmuster. "Wie funktioniert es?" Ich höre dich fragen.

Die core -Engine bildet einige Hooks ab, die die anderen Engines verwenden können:

 # 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

Wie Sie sehen können, speichern before_inclusion und after_inclusion nur eine Liste von Prozessen, die später ausgeführt werden. Der Refinery-Inklusionsprozess erweitert die aktuell geladenen Rails-Anwendungen um die Controller und Helfer von Refinery. Hier ist einer in Aktion:

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

Ich bin sicher, dass Sie zuvor Authentifizierungsmethoden in Ihren ApplicationController und AdminController haben, dies ist eine programmatische Methode.

Ein Blick auf den Rest dieser Authentication Engine-Datei hilft uns, einige andere Schlüsselkomponenten zu finden:

 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

Unter der Haube verwenden Refinery-Erweiterungen ein Plugin -System. Der initializer wird Ihnen aus der Spree-Code-Analyse bekannt vorkommen, hier werden nur die Anforderungen der register erfüllt, die der Liste der Refinery::Plugins hinzugefügt werden müssen, die die core verfolgt, und die Refinery.register_extension fügt nur den Modulnamen hinzu zu einer Liste, die in einem Klassen-Accessor gespeichert ist.

Hier ist ein Schocker: Die Refinery::Authentication -Klasse ist wirklich ein Wrapper um Devise, mit einigen Anpassungen. Es sind also Schildkröten bis ganz nach unten!

Die Erweiterungen und Plugins sind Konzepte, die Refinery entwickelt hat, um ihr reichhaltiges Ökosystem aus Mini-Rails-Apps und -Werkzeugen zu unterstützen - denken Sie an rake generate refinery:engine . Das Entwurfsmuster unterscheidet sich hier von Spree, indem es eine zusätzliche API um die Rails-Engine herum auferlegt, um bei der Verwaltung ihrer Zusammensetzung zu helfen.

Die Redewendung „The Rails Way“ ist der Kern von Refinery, immer präsenter in ihren Mini-Rails-Apps, aber von außen würde man das nicht wissen. Das Entwerfen von Grenzen auf der Ebene der Anwendungskomposition ist genauso wichtig, möglicherweise wichtiger, als das Erstellen einer sauberen API für Ihre Klassen und Module, die in Ihren Rails-Anwendungen verwendet werden.

Das Umschließen von Code, über den Sie keine direkte Kontrolle haben, ist ein gängiges Muster. Es ist eine Voraussicht, um die Wartungszeit zu reduzieren, wenn sich dieser Code ändert, und die Anzahl der Stellen zu begrenzen, an denen Sie Änderungen vornehmen müssen, um Upgrades zu unterstützen. Die Anwendung dieser Technik zusammen mit der Partitionierungsfunktionalität schafft eine flexible Plattform für die Komposition, und hier ist ein Beispiel aus der realen Welt, das direkt vor Ihrer Nase sitzt - man muss Open Source lieben!

Fazit


Wir haben vier Ansätze zum Entwerfen von Rails-Engine-Mustern gesehen, indem wir beliebte Gems analysiert haben, die in realen Anwendungen verwendet werden. Es lohnt sich, ihre Repositories durchzulesen, um aus einer Fülle von Erfahrungen zu lernen, die bereits angewendet und wiederholt wurden. Auf den Schultern von Riesen stehen.

In diesem Rails-Leitfaden haben wir uns auf die Entwurfsmuster und -techniken zur Integration von Rails-Engines und den Rails-Anwendungen ihrer Endbenutzer konzentriert, damit Sie das Wissen darüber zu Ihrem Rails-Tool-Gürtel hinzufügen können.

Ich hoffe, Sie haben durch die Überprüfung dieses Codes genauso viel gelernt wie ich und fühlen sich inspiriert, Rails Engines eine Chance zu geben, wenn sie den Anforderungen entsprechen. Ein riesiges Dankeschön an die Betreuer und Mitwirkenden der Edelsteine, die wir überprüft haben. Tolle Arbeit Leute!

Siehe auch : Timestamp Truncation: A Ruby on Rails ActiveRecord Tale