Single-Page-Anwendungen Framework-unabhängig entwickeln

Rendering

Bei Single-Page Apps findet das Rendering der dynamischen Inhalte auf dem Client statt. Die Aufgabe besteht darin, Daten der Applikation performant auf der Oberfläche darzustellen.

Was sich trivial anhört, ist bei Single-Page-Anwendungen komplexer, da sich die Daten zur Laufzeit verändern können, sei es durch die Anforderung neuer Daten vom Server oder durch Interaktionen auf der Oberfläche. Um dem zu begegnen, haben sich verschiedene Strategien durchgesetzt, die im Folgenden näher erläutert werden.

Hierbei handelt es sich um die einfachste Art des Renderings, wie man es von serverseitigem Rendering kennt. Für die initialen Daten berechnet die Anwendung eine View-Repräsentation, die Daten werden quasi 1:1 auf der Oberfläche abgebildet, was in gerenderten DOM-Elementen resultiert.

Beim manuellen Rendering sind Entwickler dafür verantwortlich, auf Veränderungen des Applikations-States zu reagieren und sie performant auf der Oberfläche darzustellen. Auf der einen Seite bietet ein solches Vorgehen eine hohe Flexibilität, auf der anderen resultiert das in einem größeren Entwicklungsaufwand. Frameworks wie Backbone.js oder Ext JS sind ein gutes Beispiel für das Prinzip des manuellen Renderings.

Betrachtet man Data-Binding bezüglich der Render-Strategie, so ist das Rendering der View nicht selbst anzustoßen. Stattdessen sind View und ViewModel über Data-Attribute verbunden. Die Synchronisation zwischen ViewModel und View übernimmt dabei ein Data-Binding-Mechanismus, ähnlich dem bereits vorgestellten Konzept innerhalb des Abschnitts MVVM.

Schaut man sich das Data-Binding-Konzept von AngularJS 1.x an, ist die View reines Markup und das ViewModel ein JavaScript-Objekt, das das Framework verwaltet. Zum Ermitteln von Veränderungen führt AngularJS über alle Attribute des ViewModels Buch, die die View verwendet, und legt sogenannte Watcher an. Wenn sich etwas in der Applikation ändert, iteriert AngularJS über alle Watcher und stößt dann das Rendering an. Das ist das Prinzip des Dirty Checking.

Das Prinzip des sogenannten Virtual DOM stellt einen alternativen Rendering-Ansatz dar. Das Grundprinzip ist vergleichbar mit serverseitigem Rendering: Immer, wenn sich etwas ändert, rendert die Anwendung alles komplett neu. Würde man jedoch bei jeder Änderung das komplette DOM neu erzeugen, ginge das auf dem Client sehr langsam. Das Virtual DOM löst das Problem, da es sich um eine JavaScript-seitige Repräsentation des DOM handelt. Sie lässt sich bei jeder Änderung performant berechnen.

Anschließend wird das virtuelle DOM mit dem echten DOM verglichen, und nur die tatsächlichen Änderungen werden im echten DOM durchgeführt. React beispielsweise nutzt Virtual DOM für das Rendering.

In jeder Single-Page App gibt es die Möglichkeit, mit dem Server zu kommunizieren, um Daten anzufordern. Dazu werden via Ajax asynchrone HTTP-Anfragen an den Server gesendet. Umsetzen lässt sich das beispielsweise via XMLHttpRequest-Objekt, das Verwenden der Ajax-Methoden von jQuery oder durch die Fetch-API, deren Einsatz immer mehr zunimmt.

Eine weitere Datenquellen für Single-Page Apps ist beispielsweise der localStorage, in dem sich clientseitig Daten speichern lassen, die die Anwendung beim Start dort auslesen muss. Bei echtzeitfähigen Anwendungen können darüber hinaus WebSockets als Quelle dienen.

Es ist empfehlenswert, die Datenzugriffslogik der Applikation in einem separaten Modul zu kapseln. So lässt sich zu einem späteren Zeitpunkt zentral auf eine andere Technik umstellen, ohne den Rest der Applikation ändern zu müssen.

Asynchronität ist im Web allgegenwärtig. Daher sollte jede Single-Page App damit umgehen können. Callbacks sind in JavaScript die einfachste Möglichkeit, um mit Asynchronität zu arbeiten.

Insbesondere bei komplexeren, verschachtelten asynchronen Operationen, kann sich das schnell zur sogenannten Pyramid of doom – auch Callback Hell genannt – entwickeln:

Der Pyramid of Doom lässt sich unter anderem durch Promises entgegenwirken (Abb. 5).


Um dem entgegenzuwirken, finden immer häufiger Promises Verwendung. Sie sind in ES2015 spezifiziert und haben sich mittlerweile in vielen Frameworks als Standard etabliert. Ihr Aufbau sieht wie folgt aus:

const promise = new Promise((resolve, reject) => {
if (/* success */) {
resolve("success");
}
else {
reject(Error("error"));
}
});

promise.then((result) => {
console.log(result); // "success"
}, (err) {
console.log(err); // Error: "error"
});

Einem Promise werden dabei zwei Funktionen übergeben: eine für den Erfolgs- und eine für den Fehlerfall. Mit promise.then(...) lässt sich ein Promise konsumieren.

Darüber hinaus können Entwickler Observables (Event-Streams) für den Umgang mit Asynchronität verwenden.

Während der Entwicklung gibt es viele wiederkehrende Aufgaben wie das Erzeugen eines JavaScript-Bundles, das Minifizieren von JavaScript beziehungsweise CSS, Linting, das Ausführen von Tests und vieles mehr. Auch das Vorbereiten der Applikation für die Produktion, Continuous Integration und Continuous Delivery zählen dazu.

Zum Erleichtern des Entwickleralltags sollten Programmierer möglichst viel automatisieren, um sich auf die Entwicklung der eigentlichen Applikation konzentrieren zu können. Zur Automatisierung eignen sich JavaScript-Taskrunner wie Grunt, Gulp oder npm-Skripte. Für viele Tasks gibt es zudem Plug-ins, die sich je nach Bedarf des Projektes zu einem Gesamtworkflow kombinieren lassen.