Es gab eine Zeit, in der – vor allem für sogenannte Enterprise-Software – wenig Wert auf Ergonomie und Benutzbarkeit gelegt wurde. An die Oberfläche wurde zuletzt gedacht und sie musste vor allem schnell umgesetzt sein. Mittlerweile hat, vor allem dadurch, dass wir aus unserem privaten Umfeld gute Oberflächen gewohnt sind, dieses Thema jedoch an Relevanz gewonnen. Heute gehören Designer und Frontend affine Menschen wie selbstverständlich in ein cross-funktionales Team.
Viele Projekte setzen aktuell auf eine Microservice-Architektur, in der das große System auf mehrere kleine Services oder Anwendungen verteilt wird. Hierbei ergeben sich auch Herausforderungen an die Oberfläche. Die generelle Nutzbarkeit soll unter keinen Umständen unter der getroffenen Architekturentscheidung leiden. Hier zählt vor allem, aber nicht nur, das Aussehen. Das Gesamtsystem soll trotz der Microservices weiterhin wie aus einem Guss aussehen und nicht wie ein Flickenteppich. Um dies zu erreichen, müssen teamübergreifende Basiskomponenten entwickelt und bereitgestellt werden. Die Teams nutzen diese anschließend, um eigene höherwertige Komponenten zu bauen.
Eines der ersten Systeme für diesen Zweck war für mich Twitter Bootstrap. Plötzlich hatte ich ein Werkzeug an der Hand, bei dem ich mich bestehender Komponenten bedienen konnte. Zuerst mussten die CSS- und bei Bedarf JavaScript-Dateien eingebunden werden. Anschließend ließ sich durch ein Copy & Paste des bereitgestellten HTML-Markups der Komponenten eine ganze Oberfläche zusammenstellen, die auch noch ganz passabel aussah.
Durch den Erfolg von Bootstrap sah nach kurzer Zeit gefühlt das halbe Internet wie mit Bootstrap entworfen aus. Der positive Effekt hierbei war jedoch, dass nun in vielen Projekten auf eine eigene Pattern-Library zurückgegriffen wurde.
Pattern-Library
In einer solchen Pattern-Library entwerfen wir vor allem generische Elemente und Komponenten. Neben dem eigentlichen Markup und Styling sollten diese idealerweise auch eine Beschreibung beinhalten, die angibt, in welchem Kontext eine Komponente wie anzuwenden ist. Diese allgemeinen Komponenten werden anschließend genutzt, um sehr spezifische Oberflächen zusammenzubauen.
Anfangs wurde eine solche Pattern-Library häufig eingesetzt, um beim Bau von mehreren Systemen für einen Wiedererkennungswert zu sorgen und um nicht jedes Mal wieder von Neuem Grundlagenentscheidungen, wie die Farbe von Buttons, treffen zu müssen.
Doch natürlich bietet sich eine Pattern-Library auch für eine auf mehrere Anwendungen verteilte Oberfläche an. Es stellt sich jedoch die Frage, wie die hier entworfenen Komponenten ihren Weg in die einzelnen Anwendungen finden.
Einbinden einer Pattern-Library
Für die Verwendung von Komponenten einer Pattern-Library gibt es aktuell zwei Wege. Der erste und wohl bekanntestes Weg ist Copy & Paste. Hierbei stellt die Pattern-Library lediglich die Beschreibung der Komponente, in der Regel in Form von HTML-Markup, zur Verfügung. Jedes Team kopiert sich dieses Markup nun in das eigene Projekt und kann die Komponente so verwenden. Dieser Ansatz hat allerdings die üblichen Probleme. Zum einen können sich beim Übertragen schnell kleinere Fehler einschleichen und zum anderen müssen Änderungen an den so kopierten Komponenten von allen mitbekommen und nachgezogen werden. Dies erhöht wiederum das Fehlerpotenzial.
Der zweite Weg besteht darin, die Pattern-Library wie eine Bibliothek zu behandeln. Diese Bibliothek kann von den Anwendungen dann als Abhängigkeit eingebunden werden. Hierzu muss demnach mindestens ein Artefakt angeboten werden, welches in irgendeiner Form zur Laufzeit die Komponenten rendern kann. Die Komplexität dieses Wegs hängt stark davon ab, ob sich alle Teams auf eine gemeinsame Technologie zur Darstellung der Benutzeroberfläche einigen wollen oder können. Einigen sich alle Teams zum Beispiel auf Thymeleaf, können die Komponenten der Pattern-Library direkt in dieser Form designt und anschließend genutzt werden.
Dies setzt allerdings voraus, dass die so gewählte Template-Engine in allen Anwendungen und vor allem in allen genutzten Programmiersprachen nutzbar ist. Sollen sich Teams unabhängig entscheiden können oder konnte keine Einigung erzielt werden, wird es komplizierter. Eine Möglichkeit ist, dass die Pattern-Library dafür sorgt, für jede verwendete Template-Engine ein eigenes Artefakt zu erzeugen. Anschließend kann jedes Team das für sich passende Artefakt einbinden.
Hierdurch wird jedoch die Komplexität in der Pattern-Library deutlich erhöht. In dieser muss es nun einen Generator geben, der aus den irgendwie definierten Komponenten mehrere technologiespezifische Implementierungen generiert. Alternativ bietet sich der Einsatz einer Template-Engine an, die in möglichst allen Umgebungen zur Verfügung steht.
Template-Engines
Es gibt grundsätzlich zwei Arten von Template-Engines. Bei der ersten bettet sich diese direkt in die Sprache ein, in der sie ausgeführt wird. So lässt sich zum Beispiel in JavaServer Pages (JSP) direkt Java-Code nutzen. Dies führt dazu, dass eine solche Template-Engine in der Regel nur genutzt werden kann, wenn die Anwendung auch in der Hostsprache geschrieben wird.
Die zweite Art versucht, genau dies zu vermeiden. Um in möglichst vielen verschiedenen Sprachen nutzbar zu sein, verzichten diese absichtlich darauf, Konstrukte aus irgendeiner Programmiersprache zu unterstützen. Diese, deshalb auch logic-less beziehungsweise logiklose genannten, Template-Engines stellen demnach aber auch nur eine sehr begrenzte Anzahl an Konstrukten zur Verfügung. Hierzu gehören in der Regel Schleifen, Verzweigungen und der Zugriff auf in das Template gegebenen Daten. Ein Beispiel für diese Art sind Mustache oder handlebars.
Neben der eigentlichen Syntax, die immer Geschmackssache ist, muss uns eine Template-Engine heute aber vor allem die Möglichkeit zur Abstraktion und Komposition bieten.
Abstraktion und Komposition
Um wiederverwendbare Komponenten zu schaffen, brauchen wir irgendeine Art der Abstraktion. Durch diese erhält jede Komponente einen Namen und kann ihre benötigten Parameter definieren. Listing 1 zeigt, wie die Abstraktion einer Komponente Alert aussehen kann und wie diese Komponente anschließend eingebunden wird.
Neben der Abstraktion brauchen wir auch die Möglichkeit, mehrere Komponenten zu komponieren. Nur so können wir höherwertige Komponenten entwickeln, die als Kinder andere Komponenten erhalten. Listing 2 zeigt eine zweite Komponente Panel, die beliebige Komponenten übergeben bekommt und als Kinder der Panel-Komponente anzeigt.
JSX
Die in Listing 1 und Listing 2 verwendete Syntax ist kein Zufall. Es handelt sich um JSX. JSX ist eine von Facebook für React entwickelte Domänenspezifische Sprache (DSL), um mit einer HTML-artigen Syntax innerhalb von JavaScript Oberflächen zu beschreiben.
Der Trick an JSX ist, dass die so geschriebenen Dateien zuerst von einem
Präprozessor behandelt werden. Dieser wandelt die wie HTML-Element aussehenden
Konstrukte in Aufrufe der Methode createElement
um. Eine ungefähre Darstellung
des so generierten Codes zeigt Listing 3.
Der Vorteil hieran ist, dass der Präprozessor keinerlei Wissen über die
eigentliche Ausführung des späteren Codes hat. Es ist nun also möglich,
verschiedene Implementierungen der createElement
-Methode zu nutzen. Die von
React mitgelieferte Methode basiert beispielsweise darauf, dass der Code im
Browser läuft, und sie fügt die Komponenten in den DOM ein.
complate
In vielen meiner Projekte standen wir nun vor der zu Anfang geschilderten Problematik. Wir wollten ein auf Microservices basierendes System bauen und die Oberfläche auf diese Services verteilen. Zudem gab es Gründe, das HTML serverseitig zu generieren und keine Single-Page-Anwendung zu bauen. Gleichzeitig wollten wir aber eine Template-Engine verwenden, die zum einen das Einbinden der Pattern-Library als Bibliothek anbietet und zum anderen eine einfache Komposition von Komponenten ermöglicht.
Nach langer Überlegung beschlossen wir, eine Implementierung der
createElement
-Methode zu erstellen, die dazu genutzt werden kann, das HTML
serverseitig zu generieren. Das Ergebnis dieser Bemühungen ist complate. Im
Kern von complate steht complate-stream. Hier ist die Implementierung der
createElement
-Methode umgesetzt. In Kombination mit einem Renderer
kann
hiermit HTML generiert werden. Listing 4 zeigt die Verwendung dieses in
Kombination mit einer in JSX deklarierten View.
Nachdem ein JSX-Präprozessor seine Arbeit verrichtet hat, können wir die Datei mit Node.js ausführen. Auf der Konsole erscheint nun das in Listing 5 gezeigte HTML. Da wir jedoch nicht alle unserer Services mit Node.js implementieren wollten, brauchen wir noch ein weiteres Puzzlestück, um complate einzusetzen.
complate und Java
Da die Views mittels JSX in JavaScript definiert werden, ist auch zur Laufzeit ein JavaScript-Interpreter erforderlich. Der Vorteil hieran ist, dass es aus so gut wie jeder aktuell genutzten Sprache möglich ist, JavaScript auszuführen. Auf der JVM können wir hierfür zum Beispiel Nashorn nutzen.
Complate stellt genau für diesen Fall Adapter bereit. Diese sorgen, in verschiedenen Sprachen, dafür, dass wir uns in unserer Anwendung nicht mehr um den Aufruf von JavaScript kümmern müssen. Auf der JVM sind hier vor allem die beiden Adapter complate-java und complate-spring relevant.
complate-java stellt eine komplette Abstraktion für die JVM bereit, um von dort
aus complate-Views zu rendern. Das Herzstück ist hierbei die Klasse
NashornComplateRenderer
. Listing 6 zeigt, wie wir diesen aus Java heraus
nutzen können. Damit dies funktioniert, müssen wir die in Listing 4 definierte
View jedoch noch leicht anpassen. Sämtliche complate-Adapter erwarten, dass aus
JavaScript eine Funktion render
exportiert wird, welche drei Argumente erhält.
Als erstes Argument wird dieser Funktion der Name einer View, genau genommen
einer Komponente, übergeben. Das zweite Argument ist ein beliebiges
JavaScript-Objekt. Dieses wird dazu genutzt, Werte von außen an die View zu
übergeben. Mit dem letzten Argument wird ein Stream übergeben. Als Stream kann
jede beliebige Klasse verwendet werden, welche die drei Methoden write
,
writeln
und flush
anbietet. Dieser Stream wird dazu verwendet, die View
auszugeben. Die fertige View, ohne Wiederholung der Komponenten, zeigt
Listing 7.
Da der Support aktueller JavaScript-Features von Nashorn nicht immer vollständig ist und sich in der Praxis unsere Views aus mehreren JavaScript-Modulen zusammensetzen, müssen wir diese Datei, zusätzlich zum JSX-Präprozessor, noch mit einem Bundler und Transpiler für JavaScript bearbeiten. Im Spring-Sample-Projekt von complate wird hierzu faucet verwendet.
Neben dem JavaScript-Support besitzt Nashorn noch zusätzliche kleinere
Eigenheiten, die wir beachten müssen. Um zum Beispiel über eine in die View
gegebene Java-Liste mit JavaScript-Mitteln zu iterieren, muss die Liste vorher
mit Java.from
umgewandelt werden. Im Großen und Ganzen halten sich diese
Sonderlocken jedoch im Rahmen und im Alltag müssen wir nur wenig Rücksicht auf
Nashorn nehmen.
Ähnlich wie complate-java bietet complate-spring eine fertige Integration von
complate in Spring Web MVC. Hierzu muss neben einem
ComplateRenderer
noch ein ComplateViewResolver
definiert werden. Der
eigentliche Anwendungscode sieht dabei weiterhin aus wie auch mit jeder anderen
Template-Engine. Die genauen Details können am besten im bereits erwähnten
Sample-Projekt nachvollzogen werden.
Fazit
Die Verteilung von Oberflächen im Web auf mehrere Services stellt uns vor neue Herausforderungen. Eine Lösung, um Komponenten in diesen wiederverwenden zu können, ist es, mit JSX auf eine JavaScript basierte Template-Engine zu setzen.
JSX ist eine von Facebook für React entwickelte DSL um HTML basierte Views deklarativ zu beschreiben. Der Trick dabei ist vor allem, dass ein Präprozessor diese definierten Komponenten in JavaScript-Methodenaufrufe übersetzt. Die Implementierung dieser Methodenaufrufe kann anschließend von verschiedenen Implementierungen kommen.
Mit complate gibt es eine solche Implementierung, die es uns ermöglicht, diese HTML-Views serverseitig zu rendern. Neben diesem Feature bietet complate eine Menge von Adaptern, welche es ermöglichen, diese Views auch aus anderen Umgebungen als JavaScript zu nutzen.
Sollten wir nun also für jedes Projekt complate nutzen? Wir haben es bereits in mehreren Kundenprojekten erfolgreich eingesetzt und es läuft dabei stabil und unauffällig. Der zusätzliche Schritt, die Views zu transpilieren, die leider aktuell noch manchmal dünne Dokumentation und die Tatsache, dass uns innerhalb der Views nicht alle gewohnten Spring-Features zur Verfügung stehen, sind jedoch Gründe, die gegen einen Einsatz sprechen können. Auf der anderen Seite gewinnen wir eine wirkliche gute Syntax mit einfacher Möglichkeit von Komposition und der Einbindung von Komponenten als Bibliothek.
Ich kann nur empfehlen, complate einmal auszuprobieren, und freue mich, wie immer, auf Fragen, Anregungen und Feedback zum Artikel und auch gerne zur Idee und der Implementierung von complate.
Hinweis
Ich bin an der im Artikel vorgestellten Implementierung von complate beteiligt. Diese entstand gemeinsam innerhalb von Projekten bei INNOQ und steht Open Source zur Verfügung.
In diesem Artikel geht es mir nicht primär um diese konkrete Implementierung, sondern vor allem darum, die Idee einer Template-Engine vorzustellen, welche unabhängig von der eingesetzten Programmiersprache funktioniert und eine einfache Komposition von Komponenten ermöglicht.