Es gibt keine Alternative zu Angular, Verzeihung, React, Entschuldigung, Vue.js. Nun gut, vielleicht ist noch nicht endgültig entschieden, ob eines – und wenn ja, welches – dieser Frameworks und ihrer vielen Konkurrenten nun gewinnen wird, aber ganz offensichtlich ist doch klar, dass die einzig richtige Architektur für moderne Webanwendungen aus einem „API First“-Ansatz auf der Serverseite und einer „Single-Page App“(SPA) auf der Clientseite besteht, die per JavaScript HTML erzeugt. Oder etwa nicht?
Nein! Eine nach klassischen Mustern erstellte Webanwendung, die HTML auf dem Server erzeugt, bietet erhebliche Vorteile, und zwar aus Anwendungs-, Entwicklungs- und Architektursicht. Im Gegensatz dazu hat die SPA, die heute leider häufig ohne größere Analyse als Standardansatz gewählt wird, eine ganze Reihe signifikanter Nachteile. In diesem Artikel soll es daher um gute Gründe für eine Server-getriebene Architektur und gegen die SPA-Variante gehen.
Ergonomie und Server-Interaktion
Ein häufiger Einwand gegen die Erzeugung von HTML auf dem Server („Server-side rendering“, SSR) ist die vermeintlich schlechtere User-Experience (UX) für die Endanwender. Der Grund dafür ist die Annahme, dass dadurch Seitenwechsel notwendig werden, was von den Benutzern als wenig interaktiv wahrgenommen wird.
Diese Annahme ist jedoch falsch: Der Mechanismus, über den bei einer Webanwendung dynamisch Inhalte aktualisiert werden, ist ein JavaScript-API, über das eine beliebige Verarbeitung auf dem Server angestoßen und das Ergebnis ausgewertet werden kann – und dieses API ist nicht nur von SPAs benutzbar.
Für eine JavaScript-zentrische Anwendung wird hier in der Regel eine Antwort im JSON-Format geliefert, die von JavaScript-Code interpretiert und in HTML und/oder äquivalente DOM-Manipulationsbefehle umgewandelt wird. Aber es ist selbstverständlich möglich, genau dasselbe API zu benutzen, um vom Server eine Antwort im HTML-Format zu bekommen. Dabei kann es sich um eine ganze Seite oder ein einzelnes Fragment handeln. Der Browser ist – wenig überraschend – in der Lage, dieses HTML höchst effizient zu interpretieren. Das ist schließlich seine Kernaufgabe.
Mit dem SSR-Ansatz lassen sich daher Anwendungen realisieren, die sich mindestens genau so interaktiv anfühlen, aber den Vorteil bieten, auch zu funktionieren, wenn JavaScript nicht verfügbar oder noch nicht geladen ist. Anders formuliert: Ob auf der Serverseite HTML oder JSON erzeugt wird, ist für die Interaktivität einer Webanwendung völlig irrelevant.
Architekturmodell
Eine konzeptionell schlüssig Vorgehensweise für die Entwicklung einer Webanwendung besteht darin, sie zunächst ohne JavaScript inhaltlich und funktional vollständig zu entwickeln, sodass eine sinnvolle Unterstützung der Arbeitsabläufe der Benutzer entsteht. Gerade der Verzicht auf JavaScript in dieser Phase führt in der Regel dazu, dass ein klares Softwaredesign auf der Serverseite entsteht, das vernünftige Entscheidungen für die Abbildung von Logik in Ressourcen, Hypermedia-Elemente wie Formulare und Links,trifft.
Legt man bei diesem Schritt Wert auf gutes inhaltliches Design, idealerweise nach REST-Prinzipien (egal, ob man sie so nennt oder nicht), entsteht eine schlanke, möglicherweise leicht altmodisch wirkende Anwendung, die ähnlich einem MVP (Model-View-Presenter) prinzipiell ausreicht, um die Benutzer zu bedienen.
Ein Beispiel für eine Entwurfsentscheidung, die dabei zunächst ungewöhnlich erscheinen mag, ist die bewusste Trennung von Ressourcen, also Informations- oder Bearbeitungsseiten, die eine eigene URL bekommen und über Links oder Formulare erst nach einer Serverinteraktion dargestellt werden. Die Applikation ist damit eine „echte“ Webanwendung, deren individuell Elemente auf natürliche Weise adressiert und an beliebigen Stellen verlinkt werden können. Folgt man dem Prinzip der statuslosen Kommunikation, wird dabei sämtlicher Status entweder auf dem Server in „echten“, adressierbaren Ressourcen abgelegt oder aber in die Seiten und die in ihnen enthaltenen Formulare und Links codiert.
Anwendungen dieser Art sind leichtgewichtig und skalieren hervorragend. Wenn das HTML, das auf der Serverseite erzeugt wird, sauber strukturiert ist und semantische Elemente korrekt verwendet, ist die Anwendung darüber hinaus auf sehr natürliche Art barrierefrei, also zum Beispiel mit Hilfswerkzeugen wie einem Screenreader verwendbar. Gerade Barrierefreiheit ist nur realisierbar, wenn sie von Anfang an berücksichtigt wird – sie lässt sich genau so wenig im Nachhinein „dazubasteln“ wie gutes Design oder Sicherheit.
JavaScript: Gerne! Aber bitte in Maßen
Bei der Diskussion über dieses Thema wird man häufig für einen JavaScript-Hasser gehalten, weil es so klingt, als würde man JavaScript ablehnen. Das ist jedoch eine falsche Sicht: Niemand, der heute eine moderne Anwendung fürs Web entwickelt, kann ernsthaft annehmen, auf den Einsatz von JavaScript (oder einer Alternative wie TypeScript) verzichten zu können. Im Gegenteil: JavaScript wird – wie alle dynamisch typisierten Programmiersprachen – gerade von Java-Entwicklern häufig unterschätzt, aber Web-Fans, die auf eine korrekte Verwendung von HTML und CSS pochen, sehen immer auch einen Einsatzbereich für JavaScript – jedoch nicht als Ersatz, sondern als sinnvolle Ergänzung an den Stellen, an denen es benötigt wird.
Deswegen ist die Anwendungsarchitektur, so wie sie oben geschildert wird, auch noch nicht vollständig. Damit sie für eine Anwendung geeignet ist, die auch noch im 21. Jahrhundert bestehen kann, muss sie natürlich interaktiver werden – zum Beispiel bestimmte Validierungen clientseitig ausführen, UI-Elemente unterstützen, die über die Basis-HTML-Controls hinausgehen, unnötige Seitenwechsel vermeiden, elegante Visualisierungen unterstützen und manchmal hochkomplizierte textuelle oder grafische Editoren zur Verfügung stellen. Jenseits der erfreulicherweise mehr oder weniger ausgestorbenen Plug-ins, Applets und anderen nativen Elementen ist vieles davon sinnvoll nur mit JavaScript-Komponenten zu realisieren, und dagegen spricht überhaupt nichts. Der entscheidende Unterschied besteht darin, wie JavaScript-Komponenten aneinander gekoppelt werden und wie sie mit dem Server interagieren, um mit Daten versorgt zu werden oder Logik aufzurufen.
Der SPA-Ansatz dazu ist, die Kontrolle über die Konfiguration, den Lebenszyklus und die Interaktion dem JavaScript-Framework zu überlassen, das damit praktisch die Kontrolle über den Client übernimmt, ganz ähnlich dem Ansatz, den man in vielen UI-Frameworks im .NET- oder Java-Umfeld vorfindet. Sicher liegt darin auch ein Grund für die Popularität mancher Frameworks, allen voran Angular: Sie fühlen sich für Entwickler, die vorher Rich Clients mit .NET, Eclipse RCP oder Swing entwickelt haben, wie eine natürliche Fortsetzung sehr ähnlicher Ideen an. Der Nachteil ist die Menge an Abstraktionen, die zwischen Entwicklern und der von ihnen benutzten Plattform stehen (siehe Kasten 1).
Kasten 1: Das Verstecken von Unbekanntem
Deklarative Vorteile
Im Gegensatz dazu setzt die oben beschriebene Architektur darauf, dass es der Browser selbst ist, der die Kontrolle behält. Man könnte den Mechanismus damit erklären, dass der Browser über das vom Server zurückgelieferte HTML konfiguriert wird. Dieser deklarative Charakter, den nicht nur HTML, sondern auch CSS hat, hat drei wesentliche Vorteile:
Erstens ist er deutlich fehlertoleranter – Fehler in HTML oder CSS führen in der Regel zu eingeschränkter Funktionalität, aber Anwendungen bleiben dennoch benutzbar.
Zweitens ermöglicht die explizite Abtrennung der deklarativen Konfiguration, dass die Interpretation durch die hocheffiziente Browser-Engine durchgeführt wird. Und diese Engine ist in einer Systemsprache wie C, C++ oder Rust umgesetzt, was die Performanz deutlich verbessert. Anders bei der JavaScript-zentrischen Variante: Hier wird diese Logik nicht deklarativ, sondern imperativ in JavaScript-Programmcode umgesetzt.
Drittens – und vielleicht am wichtigsten – ist durch das deklarative Vorgehen eine Vorwärts- und Rückwärtskompatibilität sehr viel einfacher zu gewährleisten. Sprachen wie HTML und CSS werden seit Jahren beziehungsweise Jahrzehnten so entwickelt, dass neue Features optional verwendet werden können, aber nicht müssen; es ist immer noch möglich, eine moderne Seite mit einem alten Browser aufzurufen und andersherum.
Ein Trivialbeispiel sind „abgerundete Ecken“ für Rahmen, die seit einiger Zeit mit dem CSS „border radius“-Property realisiert werden können. Sie sind in alten Browsern schlicht nicht abgerundet, sondern nur in modernen Versionen sichtbar. Aber auch für komplexere Kontrollelemente zeigt sich der Vorteil: So wird zum Beispiel ein HTML-Datumseingabefeld von alten Browsern zwar nicht unterstützt, seine Verwendung führt aber trotzdem nicht zu einem Fehler, sondern es wird als einfaches Texteingabefeld dargestellt.
Mit JavaScript lässt sich dieses aber auch in alten Browsern in einen typischen Datepicker verwandeln, während moderne Browser daraus das für die jeweilige Plattform am besten funktionierende Datumsfeld erzeugen (z. B. mit den typischen „Rädern“ auf Mobiltelefonen). Über „Custom Elements“ (siehe Kasten 2) wird mittlerweile ein standardisiertes Programmiermodell für Komponenten angeboten, das für die Unterstützung dieses Ansatzes verwendet werden kann.
Kasten 2: Web Components und Custom Elements
Progressive Enhancement
Der Schlüssel liegt also darin, die Vorteile von JavaScript im Client zu nutzen, ohne dabei die Vorteile serverseitigen HTMLs aufzugeben. Dazu wird sinnvollerweise der Ansatz „Progressive Enhancement“ angewandt. Das bedeutet, dass auf das Standardverhalten des Browsers aufgesetzt und dieses fortschreitend erweitert wird, wenn es möglich ist.
Ein Beispiel: Der Standardmechanismus, um Informationen vom Server zu holen, ist bei einer Abfrage ein Link (wenn das genaue Ziel bekannt ist) oder ein GET-Formular (also ein Formular, bei dem der Parameter method den Wert GET hat), wenn der Benutzer für die Abfrage noch Informationen eingeben muss. Für das Anstoßen einer Verarbeitung wird ein POST-Formular verwendet. In allen Fällen führt das entsprechende HTML dazu, dass der Browser den Link beziehungsweise das Formular darstellt, auch ohne JavaScript. Die Benutzerinteraktion ist damit möglich, auch wenn sie vielleicht nicht den Vorstellungen des Designers oder UX-Experten entspricht.
Durch den Einsatz von JavaScript lässt sich nun auf das HTML aufsetzen und auf dieser Basis eine beliebig komplexe JavaScript-Komponente erzeugen, die den Link beispielsweise als Bild darstellt, aus einem Formulareingabefeld einen virtuellen Drehknopf macht, Texteingaben direkt validiert, per Autocompletion die Eingabe vereinfacht oder Formulare automatisch absendet – der Fantasie sind keine Grenzen gesetzt. Die Interaktion ist auch dann möglich, wenn das JavaScript nicht oder noch nicht geladen wurde, fehlerhaft ist oder (was tatsächlich nur in Ausnahmefällen vorkommt) vom Benutzer deaktiviert wurde. Da das zugrunde liegende Informationsmodell im Browser dem entspricht, was im HTML codiert wurde, funktionieren die Standardmechanismen wie Tab-Taste, Enter, Selektion von Elementen per Leertaste, Refresh-, Back- und Forward-Schaltflächen und -tastaturkürzel. Auch die Serverseite ändert sich nicht: Ob Formular oder Link, der Server erhält genau den gleichen Request für den Fall, dass nur Basisbrowsermittel verwendet wurden wie im JavaScript-Fall.
Unter [CRMS] ist eine einfache, prototypische Versicherungsanwendung verfügbar, die diese Mechanismen illustriert. Abbildung 1 zeigt einen Teil der Einstiegsmaske, ein Eingabefeld, über das man nach versicherten Personen suchen kann. Dabei wird – wie man es heutzutage erwarten würde – bereits beim Tippen eine Suche angestoßen und eventuelle Treffer werden angezeigt.
Abbildung 2 zeigt dasselbe Kontrollelement bei deaktiviertem JavaScript: Anstelle der Autocompletion ist nun eine Schaltfläche für das Auslösen der Suche zuständig. In beiden Fällen wird der gleiche Servercode aufgerufen, die JavaScript-Komponente, die für die Autocompletion zuständig ist, ist generisch und kann an beliebigen anderen Stellen für ähnliche Anwendungsfälle eingesetzt werden.
Auch Abbildung 3 zeigt, wie Inhalt in eine Seite eingebettet wird, wenn (wie zu erwarten) JavaScript aktiviert und verfügbar ist; Abbildung 4 illustriert, wie die Seite aussieht, auf die verzweigt wird, wenn die Einbettung mangels funktionierendem JavaScript nicht funktioniert: Es ist dieselbe Seite, dasselbe HTML, das in einem Fall als eigenständige Seite gerendert und im anderen in die Seite eingebettet wird.
Unter [RAIR] ist eine Demo-Anwendung verfügbar, die zeigt, wie eine Sitzplatzreservierung für eine Flugbuchung auf dieser Basis realisiert werden kann.
Abb. 1: Suchmaske mit aktiviertem JavaScript
Abb. 2: Suchmaske ohne JavaScript
Abb. 3: Wiedervorlage per JavaScript eingebettet
Abb. 4: Wiedervorlage als eigenständige Seite
Der richtige Ort für Anwendungslogik
Ein Argument, das gelegentlich für SPAs ins Feld geführt wird, ist die klare Trennung von Client- und Serverlogik: Alles, was sich auf die Benutzeroberfläche bezieht, wird gebündelt im Client in JavaScript implementiert. Das erscheint zunächst sinnvoller als die Variante, bei der Teile auf dem Server und Teile im Client realisiert werden. Tatsächlich ist jedoch Geschäftslogik bei einer Webanwendung allein aus Sicherheitsgründen auf dem Client immer falsch aufgehoben: Selbst Validierungen müssen auf dem Server erneut durchgeführt werden, da man einem Client niemals vertrauen kann, von Berechnungen oder anderer Logik ganz zu schweigen.
Daraus, diese Logik auf dem Server anzusiedeln, ergibt sich aber noch ein weiterer Vorteil: Sie ist damit auch für andere Clients verfügbar, sei es für API-Clients oder für (native) mobile Anwendungen. Tatsächlich beobachten wir häufig in konkreten Projekten, dass fachliche Änderungen fast nie nur in einem Service, der durch ein API zugänglich ist, durchgeführt werden, sondern immer auch eine Änderung an der Aufruflogik zur Folge haben. Ist diese in mehreren Clients redundant implementiert, ergeben sich die zu erwartenden Nachteile – mehrfacher Aufwand, Koordinationsnotwendigkeit, Inkonsistenz, unnötig lange Wartezeit bis zur Verfügbarkeit. In der Regel profitieren Webanwendungen weniger davon, durch APIs in Client- und Serverlogik aufgeteilt zu werden, als APIs davon profitieren, sich mehr wie eine Webanwendung zu verhalten.
Eine Reihe von Best Practices, die wir in Projekten gesammelt haben, haben wir bereits vor einigen Jahren unter [ROCA] beschrieben. Die häufigste Kritik ist, dass dort keine konkrete Framework-Empfehlung ausgesprochen wird. Die Tatsache, dass die Referenz uns seit Längerem gute Dienste leistet und wir davon ausgehen, dass sie das sicher auch noch eine ganze Weile tun wird, ist das beste Gegenargument.
Fazit
Selbstverständlich ist die Welt nicht schwarz-weiß. Es gibt Anwendungsfälle, in denen eine SPA der richtige (oder zumindest ein valider) Lösungsansatz ist, zum Beispiel, wenn es sich um eine Ein-Personen-Anwendung handelt, mit der ein lokales Gerät administriert wird, oder wenn der überwiegende Teil der Anwendungslogik auch lauffähig sein muss, wenn keine Verbindung zum Netzwerk besteht. Oft gilt aber auch diese Einschränkung nur für einen Teil der Anwendung, sodass eine hybride Lösung gewählt werden kann. Dieselbe Logik lässt sich auch häufig auf native mobile Applikationen anwenden: Es ist unbestritten, dass für bestimmte Aspekte eine webbasierte Lösung nicht oder nur mit enormem Aufwand die gleiche Qualität wie eine native Lösung erreichen kann. Aber auch hier gilt dies in der Regel eben nur für bestimmte Aspekte, sodass eine hybride Lösung oft sehr gut funktionieren kann.
Die Tatsache, dass eine Webanwendung mit beliebigen anderen Anwendungen im gleichen Unternehmen oder anderswo auf der Welt verbunden werden kann, die Möglichkeit, Bookmarks zu setzen, neue Tabs und Fenster zu öffnen, Links zu versenden und zu verfolgen, durch die Historie vor und zurück zu gehen, mit Fehlern in Inhalten und Code umzugehen, eine ältere oder neuere Ablaufumgebung verwenden zu können und sich trotzdem darauf verlassen zu können, dass die Anwendung mit hoher Wahrscheinlichkeit funktioniert – all diese Dinge machen Webanwendungen zu etwas Besonderem, das wir nicht ohne Grund ignorieren sollten.
Literatur & Links
[CRMS] Customer-Relationship-Management-Demo, siehe: https://crimson-portal.herokuapp.com
[RAIR] ROCA Airways online check-in, einfache Demo-Anwendung, siehe:
http://roca-airways.herokuapp.com
[ROCA] Resource-Oriented Client Architecture, siehe: https://roca-style.org