Ember.js – das JavaScript-Framework für Frontends

MVC im Client
Kommentare

In der Java-Webentwicklung hat sich das Lager sehr früh zwischen Backend- und Frontend-Entwicklern geteilt. Als ehemaliger Backend-Entwickler muss ich auch gestehen, dass wir ein wenig auf die „anderen“ herabgeschaut haben: Wir nutzten Patterns, Standards und Methoden, um aus Granit gemeißelte Module zu bauen. Die „anderen“ waren froh, wenn deren Spaghetti-JS-Code unter IE4 und Firefox lief. Doch dann haben sie aufgeholt: Browser standardisierten sich, die Performance hat sich um Lichtjahre verbessert, TDD und BDD funktionieren auch für das Frontend beeindruckend gut, und neue Programmiersprachen und Frameworks haben dem Frontend die Layer näher gebracht als je zuvor.

Bevor wir weitermachen, möchte ich mich vor den Frontend-Entwicklern verbeugen: die fragmentierte HTML-/CSS-/JavaScript-Landschaft hat viel zu viel Erfahrung und Expertise benötigt, als dass ich als „Back-Endler“ da mitreden könnte. Aber da sich alles standardisiert, trau ich mich wieder ran …
Heute möchten wir uns Ember.js als JS-Framework für Browseranwendungen anschauen. Im Gegensatz zu jQuery, Backbone und Co. ist Ember.js ein One-Stop-Shop für die gesamte App-Entwicklung. Die Geschäftslogik läuft zum Großteil im Browser, man springt nicht zwischen Seiten, und es wird lediglich das Model vom Server geholt bzw. auf dem Server gespeichert. Man verfolgt ausschließlich das Model-View-Controller-Pattern – es ist fast so, als würde man mit JPA, JSF und Session Beans im Browser arbeiten.

Geschichte

Ember.js erblickte 2007 im MIT unter dem Namen Sproutit das Licht der Welt. 2008 wurde das Framework zu SproutCore umbenannt und war das Herzstück der MobileMe- und iWork-Plattform der Firma Apple. Nach einem Intermezzo mit einer eigenen Firma und Facebook entstand 2011 aus SproutCore die Open-Source-Plattform Ember.js.
Mittlerweile liegt der fünfte Release-Kandidat vor, und das API hat sich stabilisiert. Bis vor Kurzem war es sehr nervenaufreibend, dass sich Dokumentation oder Stack Overflow immer auf obsolete API-Versionen bezogen.
Die wichtigste Erweiterung der Plattform ist eindeutig Ember Data, die aber noch nicht so weit in der Entwicklung ist. Zwar hat sich das API hier weitestgehend stabilisiert, man rät aber offiziell von einem produktiven Einsatz ab. Allerdings ist absehbar, dass man sich hier in großen Schritten der Version 1.0 nähert.

Die Anwendung und das Modell

Für den Einsatz von Ember.js benötigt man jQuery, Handlebars und in der Regel auch Ember Data. jQuery sollte bekannt sein, Ember Data wurde schon erwähnt, Handlebars (wie auch Mustache) ist eine JS-basierte Templatesprache, die analog zu Velocity den Einsatz von Logik in der View Tier fast unmöglich macht. Um eine Anwendung mit Ember.js zu erstellen, genügt folgender Aufruf:

RezeptSammlung = Ember.Application.create();

RezeptSammlung ist nun eine Ember.js-Anwendung, allerdings versuche ich das Objekt eher als Namespace zu sehen denn als vollständige Anwendung. Alle weiteren Einstellungen oder Erweiterungen der Anwendung werden nun als Eigenschaften des Namespaces konfiguriert.

Da Ember.js das Model-View-Controller-Pattern verfolgt, schauen wir uns zunächst ein einfaches Model mit einer Eigenschaft an:

RezeptSammlung.Anweisung = DS.Model.extend({
  beschreibung: DS.attr('string')
});

Die DS.Model-Superklasse hat es in sich. Zunächst wird in Ember die Verbindung zwischen Model und View stets aufrecht erhalten. Die Templates erzeugen nicht einfach Strings mit den Werten in der Demo, es werden Marker eingefügt, die sich live mit dem Modell ändern. Ändert man in einem Formular eine Eigenschaft, werden zeitgleich im DOM alle Referenzen aktualisiert.

Ferner verbindet die DS.Model-Superklasse das Modell mit dem Backend. Bei DS.Model gibt es hier zwei nennenswerte Adapter. Der Fixture-Adapter erlaubt das Definieren der Objekte direkt im JS-Code (Listing 1).

RezeptSammlung.Anweisung.FIXTURES = [
  {id: 1, beschreibung: "Die Hefe mit 1 TL Zucker oder Honig in ca. 200 ml lauwarmem Wasser auflösen, das Mehl mit dem Salz vermischen, das Wasser mit der aufgelösten Hefe dazugeben sowie das Olivenöl und alles am besten mit dem Handrührgerät zu einem glatten Hefeteig kneten."},
  {id: 2, beschreibung: "Abgedeckt ca. 30 Minuten gehen lassen."},
  //..//
];

Die Eigenschaft FIXTURES wird an dem bereits genannten Modell definiert, und schon sind die Daten verfügbar. Bevorzugt definiert man die Fixtures in einer getrennten JS-Source-Datei – damit lässt sich zum Beispiel ein interaktives Mock der Anwendung bauen, bevor eine Zeile vom Backend geschrieben wurde. Da wir jedoch bevorzugt die Daten vom Server holen möchten, nutzen wir lieber den REST-Adapter von Ember Data. Der REST-Adapter ist eine Implementierung des JSON-API, ein Versuch, den Aufbau von JSON-REST-Services zu standardisieren.

Das DS.Model bietet eine Reihe von Methoden, um mit dem Server zu interagieren. Beispiele hierfür sind in Tabelle 1 zu finden.

 

Methode

Funktion

Rezeptsammlung.Anweisung.find()

Lade alle Anweisungen vom Server

Rezeptsammlung.Anweisung.find(1)

Lade Anweisung mit der ID 1 vom Server

Rezeptsammlung.Anweisung.find({})

Suche Anweisungen entsprechend dem Suchobjekt

Rezeptsammlung.Anweisung.createRecord({})

Legt ein neues Objekt entsprechend dem Parameter an

anweisung.save()

Speichert das geänderte Objekt, in diesem Fall die Anweiung

Tabelle 1: Beispiele für Methoden, die mit dem Server interagieren

Das schöne hieran ist: Solange der REST-Service dem JSON-API entspricht, ist der Integrationsaufwand gleich null. Die wichtigsten Regeln für das JSON-API lauten:

 

  • URL-Aufbau: Der URL wird immer abhängig von einem Haupt-URL „berechnet“ – der eigentliche Pfad ist standardmäßig die englische Pluralform des Model-Namens. Suchen wir den Post mit der ID 1, wird Ember.js einen URL wie /api/posts/1 aufbauen. Die Pluralformen für unregelmäßige Nomen oder fremdsprachige Wörter lassen sich natürlich getrennt bestimmen.
  • Root-Element: Das JSON-Objekt muss unter dem Modellnamen als Key auf der obersten Ebene vorliegen.
  • ID-Element: Jede JSON-Beschreibung erfordert unbedingt eine ID-Eigenschaft, die niemals explizit im DS.Model definiert werden darf.
  • Namenskonvention: Aus dem Camel Case in der Modellbeschreibung werden Underscores generiert. Aus „firstName“ im DS.Model wird also „first_name“ im JSON-Objekt.
  • Abhängige Objekte: Konstrukte wie Anweisungen DS.hasMany(‚RezeptSammlung.Anweisung‘) sind im DS.Model auch möglich. Auf der JSON-Ebene werden allerdings nur Referenzen auf die IDs gespeichert. Ember.js ist „Lazy Loading“ und wird die IDs erst im letzten Moment (z. B. wenn in der View angefordert) vom Server laden. Die genannte Abhängigkeit wird im JSON als anweisung_ids: [1,2,3,4] gespeichert.
  • Eager Loading: Wenn es ein Lazy Loading gibt, muss es auch ein Eager Loading geben. Man kann auf der obersten Ebene der JSON-Antwort auch die aufgelösten Objekte als Liste mitgeben. Durch das so genannte Side Loading kann der vorausschauende Entwickler die Anzahl der benötigten HTTP-Requests deutlich reduzieren.

Die geerntete Frucht für das Einhalten des API ist die Konfiguration des Stores, die Verbindung zwischen Model und Server. Der Store kann ohne Parameter, Mapper oder Ähnliches genutzt werden.


RezeptSammlung.Store = DS.Store.extend({});

Der Store kann explizit genutzt werden, da alle Models ihren Store kennen. Zudem kann der Store auch als eine Art Hibernate EntityManager genutzt werden: mit RezeptSammlung.Store.commit(); können alle lokalen Änderungen des Models auf den Server gespielt werden.

Aufmacherbild: retro cartoon pet hamster von Shutterstock / Urheberrecht: lineartestpilot

[ header = Views, Controller, Routing]

Views

Views werden in Ember.js fast immer direkt als Template erzeugt. Falls man die Templates wiederverwendbar gestalten oder komplexe Events behandeln möchte, kann man auch per JS eine View erstellen. Hierzu muss der geschätzte Leser aber online die Dokumentation studieren.

Templates werden normalerweise als Script-Anweisung in den HTML-Code eingebettet. Das Haupttemplate wird mit dem Namen application angelegt und von Ember.js in den Body eingefügt (Listing 2).

<script type="text/x-handlebars" data-template-name="application">
  <div class="container-fluid">
    <div class="row-fluid">
      <,div class="span3">
        {{outlet navigation}}
      </div>
      <div class="span9">
        {{outlet}}
      </div>
    </div>
  </div>
</script>

Im Templatecode erkennt man die {{}}-Notation. In den geschweiften Klammern befinden sich die Anweisungen für Handlebars. Da das application-Template einen Layoutrahmen vorgibt, müssen weitere Templates mit den eigentlichen Daten eingefügt werden. Hierzu verwendet Handlebars die so genannten „Outlets“. Im Programmcode gibt man an, welche Templates mit welchen Daten die jeweiligen Outlets mit Leben füllen sollen (Listing 3).

<script type="text/x-handlebars" data-template-name="rezept">
  <div class="hero-unit">
    <h1>{{name}}</h1>
    <p>{{beschreibung}}</p>
  </div>
  <div class="row-fluid">
    <div class="span4">
      <h2>Zutaten</h2>
      <ul>
        {{#each zutaten}}
          <li>{{beschreibung}}</li>
        {{/each}}
      </ul>
      <!-- ... -->

In unserem Beispiel wird das Outlet durch das Template index befüllt. Ember.js sorgt dafür, dass alle Eigenschaften des Modells verfügbar sind. Der Attributname in den {{}}}-Klammern reicht schon zur Ausgabe. Es sind auch Schleifen oder bedingte Anweisungen möglich. In dem Beispiel wird für jede Zutat ein List Item mit der Beschreibung der Zutat als Inhalt angelegt.

Controller

Ember.js verhindert sehr gut, dass man irgendwelche Logik in die View Layer einbauen kann; dafür gibt es die Controller. Intern können die View-Komponenten nur mit dem Controller sprechen, der wiederum alle Eigenschaften des zugehörigen Models kennt. Falls man im Browser exklusive Zustände verwalten, auf Events reagieren oder das Modell mit Sondereigenschaften (z. B. darf der aktuelle User das Modell bearbeiten) dekorieren möchte, muss dies im Controller geschehen.

Ein Controller lässt sich sehr einfach definieren, wie in Listing 4 zu sehen ist.

Ember.ObjectController.extend({
  cartPurchasable: true,
  purchase: function(){
    //purchase logik
    this.controller.set('cartPurchasable': false)
  }
})

Damit der User das Event auslösen kann, wird ein Button im Template wie folgt umgesetzt :

{{#if cartPurchasable}}
  
{{else}}
  Warenkorb bereits bestellt
{{/if}}

Routing

Wir haben gelernt, dass die View ausschließlich mit dem Controller spricht, der Controller das Modell dekorieren und Events abfangen kann. Wie spielt das alles nun zusammen? In Ember.js werden die Komponenten über Routen verbunden.
Mit Ember.js werden Single-Page-Anwendungen erstellt. Damit der Browser vernünftig mit Bookmarks und der History klarkommt, wird der Fragment Identifier des URL durch Ember.js ausgewertet. So sehen die URLs wie index.html#/rezept/12 aus, praktisch ein URL im URL. Hierfür muss zunächst der Router eingestellt werden:

RezeptSammlung.Router.map(function() {
  this.resource('rezepte', { path: '/rezepte' });
  this.resource('rezept', { path: '/rezepte/:rezept_id' });
});

Jede Route bekommt einen Namen, der erste Parameter, und eine Konfiguration, die in unserem Beispiel nur den Fragment Identifier beinhaltet. Fangen wir mit dem ersten Pfad an: rezepte.

Ember wird nun eine Route mit dem Namen RezeptSammlung.RezepteRouter, den RezeptSammlung.RezepteController-Controller und das rezepte-Template suchen. In unserem Beispiel haben wir die RezepteRoute mit dem folgenden Code definiert. Wir geben lediglich an, dass wir als Modell alle gefundenen Rezepte anzeigen wollen – in der Produktion sollte man so etwas mit einer großen Menge an Rezepten verhindern.

RezeptSammlung.RezepteRoute = Ember.Route.extend({
  model: function(){
    return RezeptSammlung.Rezept.find();
  }
});

Da wir keinen RezepteController definiert haben, baut sich Ember.js einfach selbst einen und übernimmt das Modell der Route als Controller-Modell. Schlussendlich wird das Template gerendert (Listing 5).

<script type="text/x-handlebars" data-template-name="rezepte">
  <div class="hero-unit">
    <h1>Alle Rezepte</h1>
    <ul>
      {{#each rezept in controller}}
        <li>{{#linkTo 'rezept' rezept}}
          {{rezept.name}}{{/linkTo}}</li>
      {{/each}}
    </ul>
  </div>
</script>

Der implizit erzeugte Controller hält alle Rezepte vor. Wir iterieren über diese und generieren pro Rezept einen Link. Die Links sehen wie index.html#/rezepte/1 aus.

Durch das Aufrufen einer der Links öffnen wir die zweite Route mit dem Namen rezept. Wieder sucht Ember nach der entsprechenden Route, Controller und Template. Ember erkennt anhand des Pfads /rezepte/:rezept_id, dass ein dynamisches Segment vorliegt und kann dadurch sogar implizit die Route anlegen. In diesem Fall wird die Route mit dem Modell RezeptSammlung.Rezept.find(rezept_id) bestückt – genau das, was wir vorhaben. Daher können wir getrost die Route und den Controller weglassen und lediglich die schon beschriebene View implementieren!

Fazit

Ember.js ist deutlich leistungsfähiger als in der Kürze des Artikels beschrieben werden konnte. Im Artikel wurden bevorzugt die Punkte angesprochen, in denen Ember.js per Konvention viele Aspekte intuitiv richtig lösen kann. Dennoch wird die Leistungsfähigkeit nicht eingeschränkt, da alles explizit übersteuert werden kann. Zudem ist es schwierig, mit Ember.js Spaghetticode zu schreiben, was für große Anwendungen hilfreich ist.

Als Wermutstropfen muss aber erwähnt werden, dass außerhalb von PHP und Ruby keine serverseitigen Implementierungen des JSON-API vorliegen. Dadurch muss man sich darauf einstellen, dass die REST-Services in Java und Co händisch angepasst werden müssen. Allerdings gewinnt man dadurch sehr klar strukturierte APIs, die auch außerhalb des Ember.js-Einsatzes sinnvoll verwendbar sind. Sicherlich werden nach der Verabschiedung der Version Ember Data 1.0 mehr serverseitige Frameworks folgen, mit denen das Format Out of the Box generiert werden kann.

Sonst sind meine Erfahrungen sehr positiv. Man schreibt erstaunlich wenig Code, um saubere Anwendungen zu schreiben. Das nachträgliche Hinzufügen neuer Funktionen kann sehr effizient erfolgen, da man die impliziten Klassen einfach mit zwei bis drei Zeilen Code selbst nachbauen und anschließend anpassen kann.

Als Backend Entwickler fühle ich mich endlich auch im JavaScript-Frontend wohl.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -