Welches JavaScript Framework passt zu mir? – Ember

Nachdem im letzten Post der Blog Serie “Welches JavaScript Framework passt zu mir?” Vue.js vorgestellt wurde, widmet sich dieser Post dem Framework Ember und dessen Besonderheiten im Vergleich zu Angular, React und Vue.js.

Alle Beiträge dieser Blog-Serie:

Einführung

Ember ist ein Open-Source-Framework zum Erstellen von Single-Page-Webanwendungen anhand des Model-View-Viewmodel (MVVM) Musters. Ember wurde erstmals im Jahr 2011 veröffentlicht und wurde im Laufe der Jahre von immer mehr großen Webseiten wie Groupon, LinkedIn, Vine und Twitch.tv eingesetzt. Ember bietet neben dem Erstellen von Webseiten auch die Möglichkeit, Desktop-Anwendungen zu entwickeln, was unter anderem von Apple Music eingesetzt wird.

Ember legt – im Gegensatz zu Vue.js und React – Wert darauf, nicht nur das V in MVVM zu sein, sondern ist eine umfassende Lösung, die sich sowohl um die View, als auch um Model und Viewmodel kümmert. Ember bringt von Haus aus eine umfangreiche Toolingpalette mit, die mit über 4000 Addons erweitert werden kann. Ember ist für seine frühe Adaption von Standards bekannt und unterstützt daher bereits Promises, die ECMAScript 6 Syntax und bietet außerdem eine Integration für Web Components an.

Grundkonzept

Im Gegensatz zu den anderen in dieser Blog-Serie betrachteten Frameworks ist der zentrale Bestandteil von Ember nicht die Komponente sondern das Object Model, Routing, Models, Services und Components.

Das Object Model

Für die Ember Implementierung wurde ein Ember.Object entwickelt, von dem alle Bestandteile erben. Ein Ember.Object ist eine Abstraktion eines JavaScript Objekts, das um diverse Funktionalitäten erweitert wurde. Eines der wohl wichtigsten Features sind die Getter- und Setter-Methoden. Normalerweise werden im JavaScript-Umfeld die Werte von Objekten mit

var name = myobject.name;

abgefragt und mit

myobject.name = "Loris";

überschrieben. In Ember bieten die Objekte, die von Ember.Object erben Getter- und Setter-Methoden an um auf Werteänderungen reagieren zu können.

// Getter
this.get('name');
// Setter
this.set('name', 'Loris');

Diese Syntax scheint zwar umständlicher, allerdings wird so garantiert, dass beispielweise abgeleitete Werte neu berechnet werden können und das Template bei einer Änderung der Daten neu gerendert wird.

Routing

Beim Routing geht es darum, auf eine Änderung in der URL zu reagieren. Ember bietet die Möglichkeit, am Router beliebig viele Routen zu definieren:

// app/router.js
this.route('todos');

Hierbei kann noch optional die URL mitgegeben werden. Wird keine URL mitgegeben, so wird der übergebene String als URL verwendet. Wenn nun ein Benutzer auf die URL “/todos” navigiert, sucht Ember per Namenskonvention nach einer Route für diese URL. Da Ember die Philosophie “Konvention vor Konfiguration” verfolgt, ist das manuelle Erstellen von Routen optional. Wenn Ember keine Route für eine URL findet wird eine Route autogeneriert.

// app/routes/todos.js
import Route from '@ember/routing/route';

export default Route.extend({
    // Konfiguration der Route
});

Nur wenn eine spezielle Konfiguration, wie beispielsweise ein Model, für eine Route benötigt wird, muss die Route manuell implementiert werden.

Models und Services

Models repräsentieren die Datenstruktur einer Applikation. Models sind meist persistent, das heißt, dass die Daten auch nach dem Schließen des Browsers erhalten bleiben. Um das zu ermöglichen, müssen die Daten in einem Store abgelegt werden. Dieser Store kann beispielsweise die IndexedDB des Browsers oder auch ein Server mit einer einfachen JSON- oder REST-Schnittstelle sein. Das Model ist Teil des Ember Data Pakets, das zusammen mit Ember ausgeliefert wird. Um jegliche Server zu unterstützen, bietet Ember die Möglichkeit, Models durch einen selbst geschrieben Adapter zu übersetzen, sodass der Server mit den übergebenen Daten umgehen kann.

$.getJSON('/todos').then(data => {
    this.set('todos', data);
});

Normalerweise würde man mit dem oberen Beispiel eine JSON-Schnittstelle aufrufen, die eine JSON Struktur zurückliefert. Um die Oberfläche vom Server zu entkoppeln bieten Models eine Abstraktionsebene, mit deren Hilfe die verwendete Datenstruktur definiert und Serveraufrufe durchgeführt werden können.

// app/models/todo.js
import DS from 'ember-data';

export default DS.Model.extend({
    text: DS.attr('string')
});

Die Datenstruktur für ein Todo benötigt nur einen Text. Dieser ist vom Typ String. Nun können einfach Abfragen auf diese Datenstruktur gemacht werden:

// app/services/todo-service.js
import Service, { inject } from '@ember/service';

export default Service.extend({
    store: inject(),

    addTodo(text) {
        let record = this.get('store').createRecord('todo', {
            text,
            // zusätzliche Attribute...
        });
        return record.save();
    },

    removeTodo(todo) {
        return this.get('store').findRecord('todo', todo, { backgroundReload: false }).then(function(todo) {
            todo.destroyRecord();
            return todo;
        });
    },

    getAll() {
        return this.get('store').findAll("todo");
    }
});

Die Kommunikation mit dem Store erfolgt meist über sogenannte Services, die einfach eine zusätzliche Schicht darstellen und von den Routen verwendet werden. Im obigen Beispiel ist der todo-service, der Methoden zum Hinzufügen, Löschen und Abfragen von Todos anbietet. In der Methode addTodo wird erst ein Eintrag erstellt. Nachdem dieser Eintrag erstellt wurde, ist er bereits im lokalen Store der Applikation verfügbar. Allerdings erst nach dem Aufruf von save() wird dieser Eintrag auch auf dem Server mittels eines HTTP-Requests persistiert. Das heißt, solange der Eintrag noch nicht gespeichert wurde, ist er nur im Applikationsspeicher und somit gehen die Daten verloren, sobald der Benutzer beispielsweise die Seite neu lädt. In der removeTodo-Methode wird erst nach einem Eintrag, für den anschließend die Methode destroyRecord() ausgeführt wird. destroyRecord bündelt intern nur die beiden Methoden deleteRecord und save. Das heißt, dass auch hier der Eintrag erst aus dem lokalen Store entfernt wird und anschließend mit dem Aufruf von save auch auf dem Server entfernt wird. Die Methode getAll fragt einfach nur alle Todos ab.

Um die Daten in der Applikation bereitzustellen wird nun das Model der Route befüllt.

// app/routes/todos.js
import Route from '@ember/routing/route';
import { inject } from '@ember/service';

export default Route.extend({
    todoService: inject(),
    model() {
        return this.get('todoService').getAll();
    }
});

Ab jetzt sind die Daten als Model der Route verfügbar und können beispielsweise in Komponenten verwendet werden.

Komponenten, Templates & Actions

Komponenten sind – wie in allen anderen betrachteten Frameworks – wiederverwendbare Bausteine, aus denen sich eine Oberfläche zusammensetzt. Komponenten bestehen aus zwei Teilen: Dem Template und der eigentlichen Logik (Vorsicht: dies wird nicht Controller genannt, da Controller in Ember etwas anderes sind). Die Templates werden mit Handlebars geschrieben. Die Logik der Komponente enthält Aktionen, die vom Benutzer aufgerufen werden können und auch Eigenschaften, die im Template zur Anzeige benötigt werden.

// app/templates/components/list-entry.hbs
<div>
    {{text}}
    <span {{action "removeClicked"}}>Delete</span>
</div>

Das Beispiel zeigt ein einfaches Template zur Anzeige eines Textes. Zusätzlich wird beim Klick auf das <span>-Element die Aktion “removeClicked” ausgeführt, die in der Logik der Komponente definiert ist.

Das Zusammenspiel

Ember verfolgt das Prinzip “data down, actions up”. Das heißt, dass Daten immer nur nach unten fließen und über Actions nach oben kommuniziert wird. Doch wo ist eigentlich “oben” und wo ist “unten”?

Ember - Data Down, Actions Up

Ember – Data Down, Actions Up


Der Prozess beginnt damit, dass der Benutzer auf eine URL navigiert. Der Router erkennt die URL und ruft die entsprechende Route auf. Die Route enthält ein Model, das wieder an die angezeigten Komponenten weitergereicht wird. Nun wird in der Komponente eine Aktion wie beispielsweise das Löschen eines Todos ausgelöst. Diese Aktion wird bis zur Route weitergeleitet und von der Route verarbeitet. Die Route kommuniziert mit dem Service und ruft die Methode zum Löschen des Todos auf. Der Service übergibt die Änderung dem Store und die Route aktualisiert das Model entsprechend. Durch die Änderung im Model wird abschließend die Komponente neu gerendert.

Formulare

Das 2-Wege-Databinding zwischen Eingabefeldern im Template und der Logik ist in Ember sehr simpel.

// input.hbs
{{input type="text" value=todo}}

// input.js
import Component from '@ember/component';

export default Component.extend({
    todo: ""
});

Die Variable todo wird in der Logik definiert. Anschließend wird dem Eingabefeld im Template die Variable übergeben. Der Wert der Variablen wird somit jedes Mal geändert, wenn der Benutzer den Wert im Eingabefeld ändert.

Testen

Die Ember Dokumentation unterscheidet unter anderem zwischen Modultests und Integrationstests. Modultests prüfen eine isolierte Einheit oder Funktion. Beispielsweise wird mit Modultests sichergestellt, dass Objekte korrekt serialisiert und deserialisiert werden, dynamisch berechnete Werte korrekt sind oder ein Datum richtig formatiert ist. Integrationstests hingegen prüfen den kompletten Durchstich der Applikation. Es wird geprüft, wie sich beispielsweise eine Komponente im realen Kontext verhält und welche Eigenschaften sie hat.

Standardmäßig wird in Ember QUnit als Testframework eingesetzt, allerdings werden dank Addons nahezu alle Testframeworks unterstützt. Um die Tests auszuführen, wird Testem als TestRunner eingesetzt.

Quellen

Short URL for this post: https://wp.me/p4nxik-2XT
This entry was posted in Web as a Platform and tagged , , , . Bookmark the permalink.

Leave a Reply