Ember.js 1.1 im Einsatz

Laut der Projekt-Webseite ist Ember.js das perfekte JavaScript-Framework für ambitionierte Webapplikationen. Ambitioniert sollte allerdings auch jeder Programmierer sein, der mit Ember neu anfängt. Im Gegensatz zu Angular.js ist die Einstiegshürde vergleichsweise hoch.

In Pocket speichern vorlesen Druckansicht 6 Kommentare lesen
Lesezeit: 13 Min.
Von
  • Stefan Wintermeyer
Inhaltsverzeichnis

Laut der Projekt-Webseite ist Ember.js das perfekte JavaScript-Framework für ambitionierte Webapplikationen. Ambitioniert sollte allerdings auch jeder Programmierer sein, der mit Ember neu anfängt. Im Gegensatz zu Angular.js ist die Einstiegshürde vergleichsweise hoch.

Wie bei den meisten IT-Themen sind JavaScript-Frameworks und Webapplikationen gewissen Moden unterworfen. Neue Frameworks für die Entwicklung von Webapplikationen schießen gerade wie Pilze aus dem Boden. Eine solche Webanwendung ist aber etwas anderes als eine statische oder auf dem Server gerenderte HTML-Seite. Als Programmierer muss man sich immer fragen, was für den eigenen Anwendungsfall wirklich die optimale Lösung ist. Ember.js, Angular.js und Backbone.js sollten nicht l’art pour l’art eingesetzt werden. Wer online eine echte Applikation mit dynamischen Elementen zur Verfügung stellen will, braucht ein solches Framework. Wer aber nur eine normale Webseite mit Informationen zur eigenen Firma veröffentlichen will, ist mit klassischem HTML und der Programmierung von dynamischen Elementen mit PHP, Python oder Ruby in Kombination mit einem Framework wie Django und Ruby on Rails besser beraten.

Ember ist ein "opinionated" MVC-Framework (Model-View-Controller). Der Zusatz "opinionated" heißt im Klartext: "Sachen werden so gemacht, wie wir uns das vorstellen oder gar nicht." Wer auf der Serverseite mit den erwähnten Frameworks arbeitet, wird an Ember.js Freude haben. Wer mit strikten Konventionen wenig anfangen kann, der sollte lieber in Richtung Angular.js gehen. Um die Wahl etwas zu erleichtern, sind in der folgenden Tabelle die wichtigsten Entscheidungsfaktoren gegenübergestellt.

Backbone.js AngularJS Ember.js
Flexibilität + - - - -
Erlernbarkeit + - - -
Routing Engine - + ++
Two Way Bindings - - ++ ++
Abbildung von Datenbank Relationships im Modell - - - - +
Programmierproduktivität - + ++
Größe der Entwickler-Community ++ + +
Geschwindigkeit + o +
Sicherheit vor Speicherlecks - ++ ++
automatisch testbar + + ++


(++ sehr gut, + gut, o noch in Ordnung, - schlecht, - - sehr schlecht)

Möchte man sich mit Ember.js beschäftigen, ist es wichtig darauf zu achten, bei der Suche nach Tutorials im Internet nur aktuelle Artikel zu lesen. Allein 2013 hat sich so viel getan, dass ein Artikel vom Januar im November oft völlig unbrauchbar ist. Innerhalb der Community wird darüber diskutiert, ob Ember vielleicht zu früh "sichtbar" geworden ist und sich dadurch – unnötigerweise – zu viele neue Entwickler durch Fehler abschrecken ließen. Das Ember-Core-Team stellt zwar mehr als deutlich klar, welche Version von Ember und der Bibliothek Ember-Data gerade Beta sind und welche nicht, viele Interessierte schätzen eine Beta allerdings schnell als "stabil genug" ein (was nicht der Fall ist) und stehen so bald vor Schwierigkeiten.

Ein guter Start für den Anfänger mit Englisch-Kenntnissen sind die Guides des Ember-Projekts. Auf emberwatch.com findet man aktuelle Vorträge und Tutorials zu Ember. In Deutschland gibt es außerdem Meet-ups von Ember-Entwicklern, die auf der Community-Seite verzeichnet sind.

Die im Folgenden als Beispiel verwendete Nutzerverwaltung besteht aus nur einer Datei ( index.html ). Bei einem großen Ember.js-Projekt würde man die einzelnen Elemente allerdings in eigene Dateien auslagern. Die folgenden Grundbibliotheken sind für das Projekt später notwendig:

<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
<script src="http://builds.emberjs.com/handlebars-1.0.0.js"></script>
<script src="http://builds.emberjs.com/tags/v1.1.2/ember.js"></script>
<script src="http://builds.emberjs.com/tags/v1.0.0-beta.3/ember-data.js">
</script>
<script src="http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups
/md5.js"></script>

jQuery lässt sich in der Version 1.x oder 2.x benutzen, während Ember.js in Version 1.1 Verwendung findet. Sie ist, nach einem für normale Ember.js-Entwickler anstrengendem 2013er-Release-Process, eine stabile und performante Version, die die Kinderkrankheiten der 1.0 vergessen lässt. Ember-Data muss momentan (November 2013) noch als Beta-Version eingebunden werden. Handlebars kommt darüber hinaus zum Einsatz, um die HTML-Templates zu schreiben. Übrigens: In all diesen Open-Source-Projekten arbeitet der Ember-Erfinder Yehuda Katz im Core-Entwicklerteam. Die als Letztes eingebundene Google Crypto-Bibliothek md5.js soll im Beispiel dazu dienen, um aus der E-Mail-Adresse eine MD5-Summe erstellen zu können und damit per Gravatar Fotos der Personen anzuzeigen.

Obwohl Ember-Data eine eigenständige Bibliothek ist, feiern sie viele Entwickler als die eigentliche Revolution des Ember-Frameworks. Mit ihr lässt sich von einer Ember-Applikation aus eine beliebige RESTful Schnittstelle per JSON bedienen. Wer die auf der Projektseite beschriebenen Konventionen auf Serverseite einhält, kann mit Ember-Data Daten aus der JavaScript-Applikation heraus transparent auf dem Server lesen und schreiben. Die Highlights bestehen dabei in Transaktionen, Rollbacks und die von ActiveRecord bekannten Associations has_many und belongs_to. Es gab vor Ember.js schon andere JavaScript-Frameworks, mit denen man gute Webapplikationen programmieren konnte, aber Ember-Data betritt Neuland.

Das User-Model lässt sich mit folgendem Code definieren und per FixtureAdapter lokal im RAM speichern. Mit anderen Adaptern kann man später in der Produktion LocalStorage oder RESTful Server anbinden.

App.ApplicationAdapter = DS.FixtureAdapter;
App.User = DS.Model.extend({
firstName: DS.attr('string'),
lastName : DS.attr('string'),
email : DS.attr('string'),
phone : DS.attr('string'),
fullName : function() {
return this.get('firstName') + ' ' + this.get('lastName');
}.property('firstName', 'lastName'),
});

fullName wird als Computed Property automatisch bei jeder Änderung der Attribute firstName oder lastName neu berechnet und zur Verfügung gestellt. Für den HTML-Inhalt der späteren Webseite kommt ein Handlebar-Template zum Einsatz. Das Erste ohne eine definierte ID bekommt von Ember per Konvention automatisch die ID application und wird damit als Haupt-Template benutzt. Innerhalb des Templates lässt sich per {{outlet}} definieren, an welcher Stelle Unter-Templates Inhalte einfügen können.

Beim Aufrufen einer Ember-Applikation überprüft das Framework anhand der Routen, was gemacht werden soll. Letztere sind per App.Router.map(function){…} zu definieren:

App.Router.map(function(){
this.resource('users', function(){
this.resource('user', { path:'/:user_id' }, function(){
this.route('edit');
});
this.route('create');
});
});

Die unter Ember-Entwicklern beliebte Chrome-Erweiterung Ember Inspector zeigt die Routen übersichtlich an. Mit dem Befehl Ember.keys(App.Router.router.recognizer.names) kann man sie sich alternativ in der JavaScript-Console anschauen (Tipps zum Debuggen finden sich auf der Webseite des Ember-Projekts unter dem Punkt Debugging). Das Definieren und Verwenden sauberer Routen ist einer der Schlüssel für eine gute Ember-Applikation. Das Framework nutzt sie, um zu prüfen, ob ein entsprechender Controller im Code vorhanden ist. Wurde er nicht vom Programmierer erstellt, erzeugt Ember den Controller anhand von Defaults automatisch.

Mit "Convention over Configuration" und einer klaren Namenskonvention erspart man sich so viel Tipparbeit und behält auch in großen Teams leichter den Überblick. Die Index-Route wird per Convention automatisch definiert. Im Beispiel ist sie allerdings mit App.IndexRoute = Em.Route.extend(…) manuell festgelegt und lässt sich dazu nutzen, mit this.transitionTo('users'); eine Transition zur Route users.index durchzuführen. Dort füllt man das Modell mit allen Usern aus dem definierten Store und setzt per activate-Hook aus jQuery der HTML-Titel der Seite:

App.UsersRoute = Em.Route.extend({
model: function(){
return this.store.find('user');
},
activate: function() {
$(document).attr(’title’, ’Listenansicht’);
}
});

Nach der Route geht es in den Controller App.UsersController in dem man mit sortProperties: ['lastName'], sortAscending: true die Datensätze nach dem Nachnamen sortiert. Am Ende der Kette rendert das Skript das Handlebar-Template mit der ID users.

<script type="text/x-handlebars" id="users">
<div class="row">
<div class="col-md-7">
<h2>Listenansicht</h2>
<table class="table">
<thead>
<tr><th>Name</th><th>E-Mail</th><th></th></tr>
</thead>
{{#each user in controller}}
<tr {{bindAttr class="user.isDirty:warning"}}>
<td>{{user.fullName}}</td>
<td>{{user.email}}</td>
<td>{{#link-to "user" user class="btn btn-default"}}
Detailansicht{{/link-to}}</td>
</tr>
{{else}}
<tr><td colspan="3">Kein User vorhanden.</td></tr>
{{/each}}
</table>
<p>
{{#link-to "users.create" class="btn btn-default"}}User anlegen
{{/link-to}}
</p>
</div>
<div class="col-md-5">
{{outlet}}
</div>
</div>
</script>

Der Handlebar Each-Loop {{#each user in controller}} im obigen Beispiel stellt eine else-Verzweigung zur Verfügung. Mit ihr kann man eine Alternativ-Anzeige erstellen – für den Fall, dass kein Nutzer gespeichert ist. Die Einzelansicht eines Users lässt sich mit der URL index#users/1 aufrufen. Auch hier wird als Erstes die entsprechende Route App.UserRoute eingebunden, in der sich mit this.store.find('user', params.user_id) der Datensatz suchen und an den Controller liefern lässt. Am Schluss rendert das Beispiel das Handlebar-Template mit der ID user in das im users Handlebar-Template zur Verfügung gestellte {{outlet}}.

Detailansicht eines einzelnen Datensatzes

Das in der User-Detailansicht angezeigte Avatar-Bild stellt ein sogenannter Component zur Verfügung. Er greift die vom W3C diskutierte Idee selbst definierter HTML-Tags auf und setzt sie mit Handlebar um. Das folgende Codebeispiel zeigt den Quellcode, der den Component {{gravatar-image email=email size=“100“}} erst ermöglicht.

<script type="text/x-handlebars" id="components/gravatar-image">
<img {{bind-attr src=gravatarUrl}} class="img-rounded">
</script>

[...]

App.GravatarImageComponent = Ember.Component.extend({
size: 100,
email: '',

gravatarUrl: function() {
var email = this.get('email'),
size = this.get('size');
return 'http://www.gravatar.com/avatar/'
+CryptoJS.MD5(email) + '?s=' + size;
}.property('email', 'size')
});

Natürlich lassen sich nicht alle Ereignisse einer Webapplikation per URL und damit per Route abbilden. Beim Abspeichern eines gerade editierten Users kommt deshalb eine per Button ausgelöste Aktion im UserEditController zum Einsatz:

App.UserEditController = Em.ObjectController.extend({
actions: {
save: function(){
var user = this.get('model');
user.save();
this.transitionToRoute('user', user);
}
}
});

Wenn man sich an die Namenskonventionen gewöhnt hat, kann man sich schnell in fremden Code einarbeiten und auch selbstgeschriebener Code profitiert letztlich. Der Name eines Controllers beziehungsweise einer Route zieht sich wie ein roter Faden durch die Applikation.

Ember.js ist eine gute Möglichkeit, eine Webapplikation performant zu gestalten. Dabei sollte man allerdings nicht die guten alten Caching-Optionen des Webs außer Acht lassen. Kein Bit ist so schnell wie ein Bit, das überhaupt nicht ausgeliefert werden muss! Eine Webapplikation unterliegt dabei der gleichen Auslieferungslogik wie eine klassische Desktop-Applikation, die auch nicht jeden Tag vom Hersteller per Post neu an den Kunden auszuliefern und von ihm neu zu installieren ist.

Jeder moderne Webbrowser benutzt einen lokalen Cache, um Grafiken oder HTML-Seiten für den späteren Gebrauch wieder zu verwenden. Meistens wird der Browser dafür beim Webserver nachfragen, ob eine bestimmte Datei noch aktuell ist. Dabei kommen ETags zur Versionierung oder Zeitangaben für einen Zeitvergleich zum Einsatz. Wenn sich die Datei nicht verändert hat, gibt der Server den HTTP-Statuscode 304 (Not Modified) zurück. In dem Fall kann der Browser die Datei aus dem Cache benutzen. Gerade beim Gebrauch von Smartphones, die über 3G oder 4G surfen, ist der Geschwindigkeitsgewinn enorm.

Geht man vom Grundgedanken einer normalen Applikation aus, müsste der Browser den Stand der Dateien überhaupt nicht ständig beim Webserver nachfragen. Wer als Anbieter eine Version der Software nach intensivem Testen als stabil einstuft, kann die Datei per Konfiguration des Webservers mit einem Expires:- oder Cache-Control: max-age=-Header ausliefern. Dann muss der Browser beim erneuten Aufruf der Applikation (die für ihn nichts anderes als eine normale Webseite ist) keinen Kontakt zum Server aufnehmen. Er kann anhand des Zeitstempels im Header eigenständig überprüfen, ob eine Datei noch aktuell ist.

Es gibt keine für alle gültige Daumenregel für einen sinnvollen Expires:-Wert. Bei der Mehrzahl der Webapplikationen sind beispielsweise 24 Stunden unproblematisch, oft sind gar sieben Tage möglich. Der Gewinn dadurch liegt dann nicht nur beim Kunden, sondern auch beim Betreiber des Webservers, der weniger Server benötigt und dadurch Geld sparen kann. Ein optimales Caching endet allerdings nicht bei JavaScript und HTML-Dateien. Lädt die Ember.js-Applikation die Daten mit Ember-Data vom Server als JSON-Dateien, gilt dabei der gleiche Grundgedanke. Im schlimmsten Fall sollte man auf jeden Fall mit dem Statuscode 304 arbeiten, da sich der Traffic so stark reduzieren lässt.

Wer gerne mit opinionated Frameworks arbeitet und neu mit dem Thema Webapplikationen anfängt, ist bei Ember im Vergleich zu den anderen Frameworks am besten aufgehoben. Allein am Thema saubere URLs beißt man sich sonst schnell die Zähne aus. Mit Ember-Data gibt es zusätzlich eine Datenschnittstelle, die sich perfekt in das restliche Framework einbinden lässt.

Stefan Wintermeyer
ist auf Webperformance spezialisiert, Gründer und Geschäftsführer der AMOOMA GmbH und Autor des Ruby-on-Rails-Buchs http://ruby-auf-schienen.de.
(jul)