JavaScript? Gern, aber bitte in Maßen

Klassischer Architekturansatz für Webanwendungen

Eine moderne Webanwendung wird natürlich in JavaScript implementiert und erzeugt ihr HTML clientseitig im Browser selbst. Sie kommuniziert mit dem Server nur, um über ein HTTP/REST API Daten im JSON-Format abzuholen – das, so scheint es, ist die gängige Weisheit. Aber haben die bewährten Ansätze wie serverseitiges HTML und Progressive Enhancement tatsächlich ausgedient? Im Gegenteil, damit lassen sich Anwendungen realisieren, die oft sehr viel besser sind als die mit dem Framework der Woche realisierte Single Page App.

Sehen wir uns zunächst an, welche Probleme es bei SPAs gibt. Bei der traditionellen Trennung zwischen Server und Client liegen auf der Clientseite lediglich Aussehen und Verhalten. Zustand, Geschäftslogik, Routing sowie Präsentationslogik und Templating sind ausschließlich auf dem Server zu finden (Abb. 1).

Abb. 1: Traditionelle Trennung zwischen Server und Client
Abb. 1: Traditionelle Trennung zwischen Server und Client

Mit dem Ziel, eine App zu bauen, die schneller reagiert und vielleicht auch offlinefähig ist, wird nun oftmals Verantwortung vom Server auf den Client verschoben. In vielen Unternehmen ist ein zusätzlicher Anreiz, Abteilungen stärker zu trennen. Backend-Experten beispielsweise stellen nur ein API bereit und müssen sich um HTML/CSS/JS nicht kümmern. Zudem kursiert der Irrglaube, dass JSON kleiner sei als HTML. Jon Moore hat in einem Artikel erklärt, wieso das nicht stimmt. Häufig werden die View-Logik sowie das Templating ins Frontend verschoben. Das passiert beispielsweise mit Technologien wie React oder Vue.js (Abb. 2).

Abb. 2: View-Logik und Templating werden ins Frontend verschoben
Abb. 2: View-Logik und Templating werden ins Frontend verschoben

Ein weiterer Schritt ist oftmals, das Routing ebenfalls in den Client zu verlagern. In React zum Beispiel mit React Router, in Vue.js mit Vue Router. In Frameworks wie Angular oder Ember ist das Routing ebenfalls im Client angesiedelt (Abb. 3).

Abb. 3: Das Routing wandert in den Client
Abb. 3: Das Routing wandert in den Client

Auf den ersten Blick wirkt diese Verschiebung der Verantwortlichkeiten von Server zu Client erst nur wie das Versetzen einer Grenze. Tatsächlich ist es allerdings so, dass das Verschieben dieser Linie weitere Folgen mit sich bringt.

Vorher haben wir HTML im Client interpretiert – eine Funktionalität, die der Browser mit sich bringt. Nun müssen wir einen JSON-Client sowie ein API zur Verfügung stellen. Das ist auch eine Indirektion: In der Präsentationslogik greifen wir nun nicht mehr direkt auf die Geschäftslogik und den Zustand zu, sondern müssen eine Transformation in JSON und zurück durchführen. Gerade wenn Server und Client von separaten Teams entwickelt werden, erhöht sich hier der Koordinations- und Kommunikationsaufwand.

Wenn wir Geschäftslogik und Zustand vollständig auf dem Server belassen, bleibt die Menge der Kommunikation gleich bzw. erhöht sich in vielen Fällen im Vergleich zum Server-Rendered-Modell. Bewegen wir einen Teil der Geschäftslogik auf den Client, müssen wir nun stets entscheiden, welche Geschäftslogik auf dem Server, welche auf dem Client und welche an beiden Orten vorhanden sein muss. Dabei müssen wir beachten, dass der Client keine vertrauenswürdige Umgebung ist. Entsprechend müssen wir also viele Dinge auf dem Server prüfen. GraphQL ist eine Technologie, die eine solche Nutzung des Backends als reine „Datenbank“ zu ermöglichen versucht.

Auch der Zustand kann nicht mehr vollständig auf dem Server (Datenbank/Session/…) bleiben, wir müssen den Zustand im Client halten. Das realisieren wir mit Lösungen wie LocalStorage/PouchDB, aber auch mit Technologien wie Redux, Flux, Reflux und Vuex. Hier müssen wir ebenfalls entscheiden, welche Daten auf dem Client, welche auf dem Server und welche an beiden Orten abgelegt werden. Dabei müssen wir über (Multi-Leader-)Synchronisierung nachdenken: Da Daten auf mehreren Clients gleichzeitig bearbeitet werden, die teilweise auch längere Zeit offline sein können, kommt es zwangsläufig zu Schreibkonflikten, die es zu lösen gilt.

Zudem bedeutet diese Verschiebung, dass wir mehr JavaScript-Code auf dem Client ausführen müssen. Die Zeit, die der Browser braucht, den JavaScript-Code zu parsen und anschließend auszuführen, unterschätzen viele. Das führt auch dazu, dass JavaScript-lastige Seiten eine längere Startzeit haben. Um das zu umgehen, bieten die meisten Frameworks die Möglichkeit der Hydration: Die Anwendung wird auf dem Server mit Hilfe von Node gerendert und als HTML ausgeliefert. Wenn das JS fertig geladen ist, übernimmt es das Markup und wird ab diesem Zeitpunkt clientseitig gerendert.

Ein weiterer Grund dafür ist SEO: Da JS weiterhin nicht zuverlässig von Suchmaschinen ausgewertet wird, ist es sinnvoll, Inhalte auch als HTML anzubinden. Für die Hydration fügen wir neben weiterer Komplexität auf dem Client also auch Infrastruktur hinzu.

Ausführung unter widrigen Umständen

Browser sind zudem eine widrigere Ausführungsumgebung als unser Server, was wir immer beachten müssen, wenn wir mehr Code in den Browser verschieben. Ein Punkt ist beispielweise die geringere Beobachtbarkeit. Auf dem Server können wir Logs erfassen sowie Exceptions abfangen und loggen. Das geht auf dem Client ebenfalls, ist aber wesentlich komplexer und fehleranfälliger. Auch in Sachen Performance sollten wir einige Faktoren bedenken. Wir testen unser JavaScript oft auf sehr performanten Maschinen. Aber gerade auf Mobilgeräten ist die Zeit, die darauf entfällt, JavaScript zu verarbeiten, sehr hoch. Details dazu finden sich hier und hier.

Browser sind eine widrigere Ausführungsumgebung als unsere Server, was wir immer beachten müssen, wenn wir Code in den Browser verschieben.

JavaScript wird in einem einzelnen Thread ausgeführt. Nur I/O-Operationen werden asynchron außerhalb des Threads ausgeführt, aber eine CPU-Aufgabe blockiert den Thread. CPU-intensive Aufgaben sorgen folglich dafür, dass man nicht mehr mit der Seite interagieren kann. Führen wir also viel Geschäftslogik aus, blockieren wir den Thread und die Seite wirkt wie eingefroren. Heute können wir Code mit Hilfe von Web-Workers auf einem separaten Thread ausführen. Allerdings ist das gerade in älteren Browsern nicht möglich, was die Performance auf älteren, schwächeren Geräten weiter verschlechtert.

Zudem zieht die unkontrollierte Ausführungsumgebung ebenfalls Konsequenzen nach sich. Auf dem Server können wir uns die Version unserer Programmiersprache aussuchen (z. B. JDK 7). Webbrowser gibt es hingegen in unglaublich vielen verschiedenen Versionen. Selbst die neuesten Versionen aller Browser unterstützen nicht alle spezifizierten Features. Die Komplexität des Testens ist also immens höher.

Zusammengefasst bringt das Verschieben von Routing, Templates und Präsentationslogik weitere Verantwortlichkeit mit sich. Zusätzlich sind weitere Infrastruktur, Koordination, Duplikation und Indirektion erforderlich und wir transportieren dabei Code von einer kontrollierten Ausführungsumgebung in eine widrige Umgebung.

Ein alternativer Architekturansatz

Gehen wir einen Schritt zurück und vergleichen den oben beschriebenen Architekturansatz mit dem, den wir bei einer klassischen GUI-Anwendung hätten, die wir beispielsweise mit .NET, Swing oder Eclipse RCP entwickelt hätten. Wir stellen fest, dass die Unterschiede gar nicht besonders groß sind. Darin liegt für viele Teams die Attraktivität, gerade wenn bislang keine Webanwendungen entwickelt wurden. Eine bekannte 3-Schichten-Architektur wird auf das Web übertragen. Als Ablaufumgebung kommt nicht Windows, die CLR oder eine JVM zum Einsatz, sondern die JavaScript- Runtime des Browsers.

Leider ignoriert dieser Ansatz trotz seiner Popularität die eigentlichen Stärken des Webs, die vor allem von deklarativen Sprachen geprägt sind und damit zu einer außerordentlich fehlertoleranten Architektur führen. Ein Terminologiewechsel trägt dazu bei, diesen Punkt zu illustrieren: Der Browser ist eine High-Performance- Grafik-Engine und ist auf praktisch allen Plattformen die optimierteste Anwendung. Er ist in C, C++ oder anderen systemnahen Sprachen wie Rust implementiert und nutzt die Hardwarebeschleunigung des darunter liegenden Systems bestmöglich aus. Diese Möglichkeiten können wir nutzen, indem wir aus JavaScript per API darauf zugreifen. Alternativ lassen sich die eigens dafür entwickelten deklarativen Sprachen HTML und CSS verwenden. Der systemnahe Code des Browsers kann sie schneller als alles andere parsen, interpretieren und in das für die Darstellung benötigte Document Object Model (DOM) umsetzen.

Weil die Sprachen genau für diesen Einsatzzweck gebaut wurden, sind sie aus Performancesicht optimal. Außerdem sind sie deklarativ und besonders fehlertolerant in der oben skizzierten widrigen Ausführungsumgebung Browser. Gibt es einen Fehler im JavaScript- Code, wird eine Exception geworfen und der Rest des Codes nicht ausgeführt. In CSS werden unbekannte Regeln hingegen einfach übersprungen und in HTML unbekannte Tags einfach als <span> behandelt. Die deklarative Natur erlaubt es zusätzlich, außerordentlich große Kompatibilität über verschiedene Browserversionen hinweg zu gewährleisten. So können serverseitig gerenderte HTML- und CSS-Seiten von älteren und neueren Browsern und selbst auf Geräten, die es zum Zeitpunkt der Erstellung des Codes noch gar nicht gab, in der Regel zumindest so weit interpretiert werden, dass sie benutzbar sind.

Für diejenigen, die den serverseitigen Code schreiben, lässt sich das HTML als Komponentenkonfiguration betrachten: Die bestehenden Komponenten wie Text- und Eingabefelder, Select-Elemente (Listen), Video- und Audioelemente müssen nicht implementiert, sondern benutzt werden. JavaScript schließlich kommt selbstverständlich auch zum Einsatz, aber nicht als notwendige Zutat für ein Funktionieren der Seite, sondern als zusätzliches Element, um sie im Bezug auf Ergonomie und Optik zu verbessern.

Für den Ansatz, sich auf die Basisfeatures des Browsers zu verlassen und die drei Schlüsseltechnologien konform zu diesen Grundprinzipien zu verwenden, haben wir eine Website mit Best Practices angelegt und ihr einen Namen gegeben: ROCA (für Resource Oriented Client Architecture).

Was macht man denn nun konkret?

Bis jetzt haben wir über eine abstrakte Architektur gesprochen, aber wie sieht das nun in der Praxis aus? Können wir uns wirklich eine Webanwendung ohne JavaScript vorstellen? Wir haben uns vielleicht daran gewöhnt, dass wir irgendwelche JSON-Strukturen vom Server erhalten und daraus DOM-Elemente generieren. JSON ist aber nicht die Sprache des Webs. HTML ist die Sprache des Webs. Warum müssen wir also unbedingt den Zwischenschritt über JSON gehen, aus dem dann ein JavaScript-Framework HTML erzeugt? Fast jedes Backend- Framework bringt eine Templating Engine mit, über die man direkt HTML generieren und ausliefern kann. Die Basis-Spring-Boot-Anwendung kommt beispielweise mit Thymeleaf gebundled, womit wir direkt HTML-Seiten ausliefern können, ohne irgendwelche JavaScript-Frameworks zu verwenden.

Make it work: HTML

Wenn wir jetzt eine von JavaScript unabhängige Anwendung entwickeln wollen, ist der erste Schritt, eine funktionierende Webanwendung zu schreiben, die nur aus HTML besteht. HTML5 versammelt eine ganze Reihe von Elementen mit viel Funktionalität. Drei Beispiele dafür sind:

  • <input type="date"> erzeugt einen Datepicker.
  • <audio> erzeugt einen Audiospieler.
  • <video> erzeugt einen Videospieler.

Außerdem gibt es immer einen Fallback für diese Elemente, damit ältere Browser auch weiterhin funktionieren können. Oft ist es auch so, dass die Browser bessere Entscheidungen über das Rendering solcher Elemente treffen können als wir. Ein <input type="date"> auf einem Smartphone ist darauf ausgerichtet, dass der Benutzer das Datum mit seinem Finger auswählen möchte, und ist deswegen einfacher zu benutzen als der Datepicker einer JavaScript-Bibliothek. Wir dürfen auch nicht vergessen, dass HTML nicht nur für Darstellungslogik gedacht ist. Mit einem einfachen Link können wir von einer Seite zur anderen springen. Außerdem sind HTML-Formulare extrem mächtig und erlauben uns anhand vom Benutzereingaben dynamisch HTTP Requests zu generieren, die dann zum Server geschickt werden. Der Server führt schließlich die Änderung aus (Abb. 4).

Abb. 4: HTML-Formulare erlauben es, dynamisch HTTP Requests zu generieren
Abb. 4: HTML-Formulare erlauben es, dynamisch HTTP Requests zu generieren

Manchmal sollen anhand der Benutzereingaben unterschiedliche Seiten angezeigt werden. Da der Server jetzt die Funktionalität in der Anwendung steuert, er auch mit einem Redirect entscheiden, was als Nächstes passiert (Abb. 5).

Abb. 5: Der Server entscheidet via Redirect, was als Nächstes passiert
Abb. 5: Der Server entscheidet via Redirect, was als Nächstes passiert

Einer der größten Vorteile dieses Ansatzes besteht darin, dass HTML ohne viel Aufwand von Screenreadern konsumiert werden kann. Deswegen ist es relativ einfach, Barrierefreiheit in der Anwendung zu implementieren. Das A11Y Project ist eine gute Ressource für weitere Verbesserungen. Natürlich ist es auch möglich, eine JavaScript-zentrische Anwendung barrierefrei zu gestalten, erfordert in der Regel aber zusätzlichen Aufwand und besonderes Augenmerk.

Wiederverwendbarkeit

Ein weiterer Vorteil von HTML lautet, dass man die Elemente beliebig ineinander verschachteln kann. Um die Wiederverwendbarkeit unserer UI-Elemente zu verbessern, können wir einige Elemente in Komponenten extrahieren. Komponenten sind eine wiederverwendbare HTML-Struktur, die wir als Abstraktion für unsere Funktionalität benutzen können.

Um die Übersichtlichkeit unserer Komponenten zu verbessern, können wir sie mit einem System wie Atomic Design strukturieren. Näheres dazu findet sich im Artikel von Ute Mayer ab Seite 58. Hier aggregieren größere Komponenten immer kleinere Komponenten. Der einfachste Weg das praktisch umzusetzen ist, einmal ein HTML-Schnipsel zu definieren und danach immer wieder das Markup zu kopieren sowie den Inhalt auszutauschen. Langfristig ist das aber nicht wartungsfreundlich. Die meisten Templating-Bibliotheken bieten daher einen Weg, wie wir HTML-Schnipsel einmal definieren und dann wiederverwenden können. In Thymeleaf etwa funktioniert das mittels th:replace oder th:insert.

Zu den großen Vorteilen dieses Ansatzes zählt auch, dass man anhand dieser HTML-Struktur die Komponenten stylen kann. Damit fangen die größeren Herausforderungen an.

Make it pretty: CSS

Jetzt kommen wir an die Stelle, an der es leicht passieren kann, dass wir scheitern. Einerseits könnte man argumentieren, dass es am wichtigsten ist, dass die Anwendung funktioniert, auch wenn sie nicht sonderlich gut aussieht. Andererseits möchte niemand eine schlecht aussehende Anwendung bedienen. In diesem Zusammenhang sind einige ästhetische Aspekte wichtig (s. Box: „Ästhetik fürs Web“).

Ästhetik fürs Web

Obwohl die „Schönheit“ der Seite eine subjektive Beurteilung ist, lässt sich an diesem Punkt viel Zeit in Themen wie Design und Typographie investieren und dabei ein Gefühl entwickeln, wie die Dinge am Ende aussehen sollen. Leider fehlt es vielen Entwicklern an der nötigen Zeit oder Lust, sich viel mit dem Thema auseinander zu setzen.

Zum Glück müssen wir aber nicht immer das Rad neu erfinden. Es ist möglich, fertige Komponentenbibliotheken wie Bootstrap oder Foundation zu verwenden. Hier haben Designer schon die wichtigsten Entscheidungen getroffen, damit die Anwendung für Endbenutzer brauchbar ist.

Wer diese Komponentenbibliotheken anpassen und um eigene Komponenten erweitern möchte, dem ist ein Pattern-Library-Werkzeug wie Fractal sehr dienlich. Es ist besonders hilfreich, wenn die Komponenten zwischen Projekten wiederverwendet werden sollen. Für größere CSS-Projekte empfiehlt es sich, einen Präprozessor wie Sass zu verwenden, damit das CSS in mehrere Dateien aufgesplittet werden kann. Für eine HTML-Komponente kann das dazu passende CSS in einer Datei direkt daneben in den gleichen Ordner gelegt werden. Hier können wir auch eine Namenskonvention wie BEM für die CSS-Komponenten nutzen. Eine Asset-Pipeline wie faucet-pipeline kann dann dafür sorgen, dass das CSS kompiliert wird. Wenn wir ebenso JavaScript schreiben, lässt sich faucet-pipeline auch dafür verwenden.

Make it fast: JavaScript

Wir haben einen Punkt erreicht, an dem wir aufhören könnten: Wir haben eine funktionierende Webanwendung, die auch schön aussieht. Allerdings können wir mit JavaScript jetzt die Anwendung optimieren und verbessern. Der Grund, warum eine Single Page App sich oft schneller anfühlt, ist, dass man CSS und JavaScript nicht bei jedem Seitenwechsel neu parsen und evaluieren muss. Mit einer Bibliothek wie Turbolinks erreichen wir genau das: Wenn ein Benutzer auf einen Link klickt, kann das HTML auf der Seite ausgetauscht werden, ohne dass das CSS und JavaScript neu geladen werden müssen.

Mit HTML-Formularen lässt sich etwas Ähnliches erreichen: Anstatt den Browser den Inhalt eines Formulars zum Server schicken und mit dem Ergebnis eine neue Seite aufbauen zu lassen, kann man den Request stattdessen per Ajax abschicken und das HTML, das zurückkommt, bearbeiten sowie Teile der Seite austauschen.

Man muss keine Single Page App bauen, um das erneute Parsen von CSS und JavaScript bei jedem Seitenwechsel zu vermeiden

Um die Inhalte von mehreren Seiten im Browser zusammenzuführen, können wir Links transkludieren, das bedeutet, einen Link durch seinen Inhalt zu ersetzen. Dazu kann ein Stück generischer JavaScript-Code dem Link „folgen“ – sprich: per Ajax den Request zum Server schicken – und das Ergebnis direkt in die Seite einbauen.

Besonders charmant daran ist, dass in allen drei Fällen der Effekt dem einer Single Page App ähnelt und sich die Seite genauso flüssig anfühlt. Sie funktioniert aber auch dann, wenn das JavaScript nicht vorhanden, fehlerhaft, noch nicht geladen oder aus anderen Gründen nicht ausgeführt wird.

Für die JavaScript-Verbesserungen unserer Anwendung können wir seit Kurzem Custom Elements verwenden, um eigene HTML-Elemente im Browser zu definieren:

class MeineKomponente extends HTMLElement {
  connectedCallback() {
  // Instanziiere Komponente
  }
}
customElements.define("meine-komponente", MeineKomponente)

Vorteilhaft hierbei ist, dass die Logik, die im connectedCallback implementiert ist, immer ausgeführt wird, wenn ein <meine-komponente>-Element im HTML DOM erstellt wird. Das funktioniert auch zusammen mit Turbolinks oder Transklusion, weil der Browser sich um die Instanzierung des JavaScripts kümmert, sobald das Element im DOM erscheint.

SPA vs. ROCA

Wir haben in der Praxis die Erfahrung gemacht, dass sich mit serverseitig gerendertem HTML in Verbindung mit Custom Elements, sorgsam modularisiertem CSS und dem sparsamen Einsatz von JavaScript architektonisch saubere, schlanke, schnelle und ergonomische Anwendungen entwickeln lassen. Sie brauchen den Vergleich mit dem populären SPA-Ansatz nicht zu scheuen und sind ihm im Bezug auf viele Aspekte häufig überlegen. Dazu zählt neben dem geringen Datenvolumen für die Kommunikation, der höheren Fehlertoleranz und der einfacheren Barrierefreiheit auch die langfristige Wartbarkeit. Man verlässt sich auf den Browser und seine Standards, anstatt sich von einem SPA-Framework abhängig zu machen, das in der nächsten Version schon wieder völlig anders aussehen kann oder durch die nächste Alternative abgelöst wird. Wir empfehlen daher, die knappe Zeit für das Erlernen von Technologien lieber in die Basisstandards HTML, CSS und JavaScript zu investieren als in ein einzelnes SPA-Ökosystem. Wer einmal eine Anwendung im ROCA-Stil gebaut hat, wählt unserer Erfahrung nach danach nur noch in Ausnahmefällen und aus gutem Grund – beispielsweise wegen der Offlinefähigkeit – einen SPA-Ansatz.

TAGS

Comments

Please accept our cookie agreement to see full comments functionality. Read more