#277 Mountable Engines
- Download:
- source codeProject Files in Zip (56.6 KB)
- mp4Full Size H.264 Video (19.6 MB)
- m4vSmaller H.264 Video (12.7 MB)
- webmFull Size VP8 Video (18.7 MB)
- ogvFull Size Theora Video (23.3 MB)
Letztes Wochenende fand das Rails 3.1 HackFest statt und dank der harten Arbeit aller Beteiligten ist nun der 5. Release Kandidat von Rails 3.1 verfügbar. Diese Release enthält wichtige Verbesserungen für Mountable Enginges. Mountable Engines erlauben es eine Railsanwendung als sogenannte Engine in einer anderen Railsanwendung einzubinden, was auch Thema dieser Folge sein soll.
Vielleicht erinnerst du dich an das Exception Notification Plugin aus Folge 104. Wenn man dieses einer Anwendung hinzufügt, speichert es jeden Fehler der Anwendung in der Datenbank. Des weiteren erlaubt das Plugin die Anzeige der Fehler über eine Benutzerschnittstelle. In dieser Folge erstellen wir das Plugin neu - allerdings dieses mal als Mountable Engine.
Vorbereitung
Bevor wir unsere Engine implementieren, müssen wir sicherstellen, dass wir Rails in der Version 3.1 Release Kandidat 5 oder neuer verwenden. Die neueste Version können wir wie folgt installieren
$ gem install rails --pre
Haben wir erst einmal die richtige Railsversion installiert, können wir damit beginnen, unsere Mountable Engine zu erzeugen. Es ist nicht notwendig, dass wir diese in einer bereits existierenden Railsanwendung erstellen. Das erstellen einer Engine ist mit der Initiierung einer Railsanwendung über das Kommando rails new
vergleichbar. Der einzige Unterschied ist, dass wir stattdessen rails plugin new
ausführen. Da unsere Anwendung Ausnahmen behandelt, nennen wir sie uhoh
. Zusätzlich müssen wir die Option --mountable
verwenden, um tatsächlich eine einbindbare - Mountable - Engine zu erhalten.
$ rails plugin new uhoh --mountable
Die Verzeichnisstruktur unserer Engine ähnelt stark der einer normalen Railsanwendung, was sie ja auch grundsätzlich ist. Diese ist eben nur so gestaltet, dass sie in eine andere Anwendung eingebunden werden kann, weshalb dann doch kleine Unterschiede erkennbar sind. Innerhalb der Anwendung befinden sich einige über einen Namensraum abgegrenzte Verzeichnisse. Beispielsweise liegt die Datei application_controller
im Verzeichnis /app/controllers/uhoh
. Das gleiche gilt für die Dateien in den Verzeichnissen assets
, helpers
und views
. Dies hilft den Code der Engine sauber vom Code der Anwendung in die sie eingebunden wird zu trennen. Das assets
Verzeichnis bewirkt, dass wir nun nicht mehr ständig öffentliche Dateien (Assets) in den public
Ordner der Railsanwendung kopieren müssen, sobald die Engine in diese eingebunden wird. Darum kümmert sich nun die Asset Pipeline.
Da öffentliche Dateien ebenfalls über einen Namensraum, dh. über einen bestimmten Ordner, abgegrenzt sind, muss dieser Ordner bei Verlinkungen mit angegeben werden. Für das Layouts Verzeichnis gilt das gleiche, auch wenn hier anscheinend ein Bug in RC5 eine zweite application.html.erb
Datei erzeugt. Die zweite Datei außerhalb des uhoh
Verzeichnisses kann einfach gelöscht werden. Wenn wir uns die andere Datei anschauen, sehen wir, dass alle Verlinkungen zu den öffentlichen Dateien auf das uhoh
Verzeichnis verweisen. Immer wenn wir Bilder oder andere Anlagen verlinken, dürfen wir diesen Namensraum nicht vergessen.
<!DOCTYPE html> <html> <head> <title>Uhoh</title> <%= stylesheet_link_tag "uhoh/application" %> <%= javascript_include_tag "uhoh/application" %> <%= csrf_meta_tags %> </head> <body> <%= yield %> </body> </html>
Als nächstes schauen wir uns eine der Schlüsseldateien der Engine, engine.rb
im Verzeichnis /lib/uhoh
, an.
module Uhoh class Engine < Rails::Engine isolate_namespace Uhoh end end
Diese Klasse erbt von Rails::Engine
und ist der zentrale Punkt für spezifische Konfigurationen. Innerhalb der Klasse wird bereits isolate_namespace
aufgerufen. Das bedeuted, dass die Engine als eigene isolierte Einheit behandelt wird und sich nicht um die Anwendung kümmern braucht, in die sie eingebunden wird.
Der letzte Teil der Engine auf den wir bei dieser kurzen Übersicht einen Blick werfen wollen, befindet sich im /test
Verzeichnis. Innerhalb des Verzeichnisses /test/dummy
ist eine Railsanwendung, welche uns zeigt, wie unsere Engine funktioniert, wenn sie eingebunden wird. Der Ordner config
enthält eine routes.rb
Datei. In dieser wiederum wird mount
mit der Klasse der Engine und einem Pfad, an dem die Engine eingebunden werden soll, aufgerufen.
Rails.application.routes.draw do mount Uhoh::Engine => "/uhoh" end
Wenn jemand deine Engine in seiner Anwendung installieren möchte, muss er genau dies tun - sie an einem selbst gewählten Pfad einbinden. Dies ist eine Rackanwendung und somit wird jeder eintreffende Request an /uhoh weitergereicht an unsere Engine
Klasse. Es ist eine gute Idee diese Zeile in die Installationsanweisung innerhalb der README Datei der Engine einzufügen, damit Nutzer wissen, wie sie die Engine in die routes Datei ihrer Anwendung einbinden.
Auch wenn diese Dummyanwendung sich im Verzeichnis /test
befindet, ist sie dennoch auch gut für manuelle Tests geeignet. Führen wir das Kommando rails s
aus dem Verzeichnis unserer Engine aus, startet die Dummyanwendung. Rufen wir die Seite http://localhost:3000/uhoh/
auf, gelangen wir zu unserer Engine, da wir sie dort eingebunden haben. Wenn wir die Seite aufrufen, sehen wir eine Fehlermeldung, da wir den entsprechenden Controller noch nicht erstellt haben.
Das werden wir jetzt nachholen und einen failures
Controller in der Engine erstellen. Hierfür können wir genauso wie in einer Railsanwendung Railsgeneratoren verwenden. Obwohl wir uns innerhalb einer Engine befinden, brauchen wir keinen Namensraum angeben. Um das alles kümmert sich Rails selbst.
$ rails g controller failures index
Hierdurch werden die gleichen Dateien erzeugt, wie wir sie auch bei einem normalen Controller erwarten, mit der Ausnahme, dass alles über den Namensraum in die richtigen Verzeichnisse verteilt wird. Als nächstes passen wir das Routing der Engine an, so dass die root
Route auf die Index Action des eben erstellten Controllers verweist. Dies erledigen wir in der Datei /config/routes.rb
der Engine.
Uhoh::Engine.routes.draw do root :to => "failures#index" end
Wenn wir nun http://localhost:3000/uhoh/
aufrufen, sehen wir die zu der Action gehörige View.
Auf dieser Seite wollen wir eine Liste der aufgetretenen Ausnahmen anzeigen. Wir benötigen ein Model in dem wir diese Informationen speichern können und erzeugen deshalb ein einfaches Failure
Model mit einem Message Feld.
$ rails g model failure message:text
Nachdem wir das Model erstellt haben, stellt sich die Frage, wie wir die Datenbankmigrationen anstoßen? Innerhalb der Engine können wir wie üblich rake db:migrate
ausführen und alles funktioniert wie erwartet. Allerdings wird die Migration so nicht funktionieren, wenn jemand versucht die Engine in seine eigene Anwendung einzubinden, da rake
keine Migrationen von eingebundenen Engines ausführt. Wir müssen unseren Nutzern mitteilen, das sie stattdessen rake uhoh:install:migrations
ausführen sollen. Dadurch werden die Migrationen der Engine in die Anwendung selbst kopiert, sodass anschließend rake db:migrate
wie gewünscht auch die Migrationen der Engine startet. Es ist eine gute Idee diese Information in die Installationsanleitung der Engine aufzunehmen.
Die Railsconsole funktioniert ebenfalls wie wir es erwarten in einer Engine und wir werden sie nun zur Erstellung eines Beispiel-Failure
s verwenden.
Uhoh::Failure.create!(:message => "hello world!")
Denk daran, dass wir immer den Namensraum mit angeben müssen, wenn wir auf eine Klasse verweisen. Da wir jetzt einen Failure erstellt haben, wollen wir ihn nun auch in unserer index
Action unseres FailuresController
s anzeigen.
module Uhoh class FailuresController < ApplicationController def index @failures = Failure.all end end end
Im Gegensatz zur Console brauchen wir hier keinen Namensraum angeben, da wir ja bereits innerhalb des Uhoh
Moduls sind. In der View schreiben wir etwas Code, um über eine Schleife alle Fehler in einer Liste anzeigen zu lassen.
<h1>Failures</h1> <ul> <% for failure in @failures %> <li><%= failure.message %></li> <% end %> </ul>
Wenn wir nun die Seite neu laden, sehen wir den eben hinzugefügten Failure
.
Ausnahmen abfangen
Jetzt wo wir die Möglichkeit haben Fehler festzuhalten, müssen wir nur jede Ausnahme, welche die Anwendung wirft, in die die Engine eingebunden ist, in unserer eigenen Fehlerklasse speichern. Um das auch Testen zu können, brauchen wir eine Möglichkeit Ausnahmen in der Dummyanwendung zu simulieren. Wir wechseln also in das Verzeichnis der Dummyanwendung unserer Engine und erstellen einen Controller simulate
mit einer failure
Action.
$ rails g controller simulate failure
Innerhalb der Action erzeugen wir eine Ausnahme.
class SimulateController < ApplicationController def failure raise "Simulating an exception" end end
Wenn wir jetzt die Action über den Browser aufrufen, sehen wir die erwartete Ausnahme.
Als nächstes müssen wir unsere Engine so anpassen, dass sie auf solche Ausnahmen wartet und entsprechend einen neuen Failure
Record in der Datenbank erstellt. Die verwendete Lösung wird zwar nicht besonders effizient sein, aber dürfte für unseren Fall erst einmal ausreichen. Wir beginnen damit einen Initilizer in unserer Engine zu erstellen. Aktuell existiert zwar kein initilizers
Verzeichnis im config Ordner unserer Engine, allerdings können wir problemlos eines erstellen und jeder Initilizer, den wir dort ablegen, sollte funktionieren. Wir legen also in diesem Verzeichnis eine Datei exception_handler.rb
an.
ActiveSupport::Notifications.subscribe "process_action.action_controller" do |name, start, finish, id, payload| if payload[:exception] name, message = *payload[:exception] Uhoh::Failure.create!(:message => message) end end
In dieser Datei registrieren wir uns bei allen Systemnachrichten (=Notifikations - diese werden detailliert in Folge 204 behandelt [watch, read]), die bei der Ausführung einer Action erzeugt werden. Wenn eine Action ausgeführt wird, können wir prüfen, ob in dem übergebenen Parameter payload
eine Ausnahme aufgeführt ist. Ist dies der Fall, wissen wir, dass die Action eine Ausnahme erzeugt hat und speichern deren Meldung in einem neuen Failure
Record.
Um das testen zu können, müssen wir den Server neu starten. Anschließend rufen wir wieder http://localhost:3000/simulate/failure
auf, um die Ausnahme erneut zu simulieren. Ist das geglückt, sehen wir sie auch in der Übersicht auf http://localhost:3000/uhoh
.
URLs in Engines
Jeden URL Helfer, welchen wir innerhalb einer Engine verwenden, wird URLs für diese Engine erzeugen. Wenn wir beispielsweise einen Link zur root URL in unserer Failures index
Seite erstellen, zeigt dieser Link zur root URL der Engine und nicht zur root URL der Anwendung in die sie eingebunden ist.
<p><%= link_to "Failures", root_url %></p>
Dieser Link verweist auf http://localhost:3000/uhoh
, also auf die root URL der Engine. Das ist die selbe Seite auf der sich der Link befindet, da in der routes Datei als root URL die index
Action des FailuresController
’s eingetragen ist. Um auf die einbindende Anwendung selbst zu verlinken, rufen wir die üblichen URL Helfer auf main_app
wie folgt auf:
<p><%= link_to "Failures", root_url %></p> <p><%= link_to "Simulate Failure", main_app.simulate_failure_path %></p>
Damit erhalten wir einen Link auf die Fehlersimulationsseite der Anwendung http://localhost:3000/simulate/failure
.
Wie können wir nun in die andere Richtung, von der Anwendung zur Engine, verlinken? Als erstes müssen wir die Zeile der routes Datei der Anwendung anpassen, in welcher die Engine eingebunden wird und der vorher definierten Route über die :as
Option einen Namen zuweisen.
Rails.application.routes.draw do get "simulate/failure" mount Uhoh::Engine => "/uhoh", :as => "uhoh_engine" end
Anschließend haben wir Zugriff auf die URL Helfer der Engine, indem wir sie als Methode von uhoh_engine
aufrufen. Um das zu demonstrieren, werden wir kurz die Failure Action so ändern, dass sie, anstatt eine Ausnahme zu erzeugen, zur root URL der Engine weiterleitet.
class SimulateController < ApplicationController def failure redirect_to uhoh_engine.root_url end end
Wenn wir die Seite http://localhost:3000/simulate/failure
aufrufen, werden wir weitergeleitet zu http://localhost:3000/uhoh
, da wir die Engine Helfer zur Weiterleitung auf eine ihrer URLs verwenden. Dies ist ein weiteres Feature welches du in der README Datei deiner Engine vielleicht erwähnen solltest.
Die Funktionalität unserer Engine ist jetzt soweit ziemlich vollständig, nur die Fehlerliste sieht noch ziemlich öde aus, so dass wir sie nun noch etwas verschönern wollen. Als erstes fügen wir ein Bild hinzu. Wir haben bereits eines gefunden und dieses in das Verzeichnis /app/assets/images/uhoh
kopiert. Um es auf der Seite einzubinden, können wir wie bei jedem anderen Bild auch den Helfer image_tag
verwenden.
<%= image_tag "uhoh/alert.png" %> <h1>Failures</h1> <ul> <% for failure in @failures %> <li><%= failure.message %></li> <% end %> </ul> <p><%= link_to "Failures", root_url %></p> <p><%= link_to "Simulate Failure", main_app.simulate_failure_path %></p>
Zusätzlich fügen wir noch etwas CSS hinzu. SASS und CoffeeScript sind zwar standardmäßig nicht in Engines verfügbar, können aber als Abhängigkeiten hinzugefügt werden. Wenn wir nun etwas CSS zu der Datei failure.css
hinzufügen, wird dieses automatisch eingebunden.
html, body { background-color: #DDD; font-family: Verdana; } body { padding: 20px 200px; } img { display: block; margin: 0 auto; } a { color: #000; } ul { list-style: none; margin: 0; padding: 0; } li { background-color: #FFF; margin-bottom: 10px; padding: 5px 10px; }
Das gleich gilt für JavaScript. Jede Ergänzung in der Datei failure.js
wird automatisch berücksichtigt.
$(function() { $("li").click(function() { $(this).slideUp(); }); });
Wenn wir die Seite nun neu laden, sieht sie deutlich besser aus und um zu testen, dass das JavaScript richtig eingebunden wurde, klicken wir auf die angezeigten Ausnahmen, welche dann ausgeblendet werden sollten.
Das wars für diese Folge. Mountable Engines sind eine großartige Neuerung in Rails 3.1. und definitiv einen Blick wert.