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.

// Deklaration einer Komponente
const Alert = ({ message }) => {
  return <span class="alert">{message}</span>;
}

// Verwendung einer Komponente
<Alert message="Achtung!"/>
Listing 1: Beispielhafte Deklaration und Verwendung einer Komponente

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.

// Deklaration mit Komposition
const Panel = ({ title }, ...children) => {
  return <div class="panel">
    <span class="title">{title}</span>
    {children}
  </div>;
};

// Verwendung von Komposition
<Panel title="Eine wichtige Mitteilung!">
  <Alert message="Achtung!"/>
</Panel>
Listing 2: Komposition von Komponenten

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.

const Alert = ({ message }) => {
  return createElement("span", { "class": "alert" }, message);
}

const Panel = ({ title }, ...children) => {
  return createElement("div", { "class": "panel" },
    createElement("span", { "class": "title" }, title),
    children);
}

createElement(Panel, { title: "Eine wichtige Mitteilung!" },
  createElement(Alert, { message: "Achtung!" }));
Listing 3: Beispiel für vom JSX-Präprozessor generierten Code

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.

import { createElement } from "complate-stream";
import Renderer from "complate-stream";

// Komponenten
const Alert = ({ message }) => {
  return <span class="alert">{message}</span>;
}

const Panel = ({ title }, ...children) => {
  return <div class="panel">
    <span class="title">{title}</span>
    {children}
  </div>;
};

// View
const View = ({ title }) => {
  return <html>
    <head>
      <meta charset="utf-8"/>
      <title>{title}</title>
    </head>
    <body>
      <Panel title="Eine wichtige Mitteilung!">
        <Alert message="Achtung!"/>
      </Panel>
    </body>
  </html>;
};

// setup
let renderer = new Renderer("<!DOCTYPE html>");
renderer.registerView(View);

class StdoutStream {
  write(msg) { process.stdout.write(msg); }
  writeln(msg) { this.write(`${msg}\n`); }
  flush() { }
}

// rendering
renderer.renderView("View", { title: "Hallo!" }, new StdoutStream());
Listing 4: Beispiel für das Rendern einer View mit complate

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.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Hallo!</title>
  </head>
  <body>
    <div class="panel">
      <span class="title">Eine wichtige Mitteilung!</span>
      <span class="alert">Achtung!</span>
    </div>
  </body>
</html>
Listing 5: Formatierte Ausgabe der complate-View

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.

ComplateClasspathSource source =
  new ComplateClasspathSource("/templates/complate/bundle.js");

NashornComplateRenderer renderer =
  new NashornComplateRenderer(source);

renderer.render("View",
  singletonMap("title", "Hallo!"),
  new ComplatePrintWriterStream(new PrintWriter(System.out)));
Listing 6: Rendern einer complate-View mit Java

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.

import { createElement } from "complate-stream";
import Renderer from "complate-stream";

const Alert = ({ message }) => { ... };
const Panel = ({ title }, ...children) => { ... };
const View = ({title}) => { ... };

let renderer = new Renderer("<!DOCTYPE html>");
renderer.registerView(View);

export default function render(view, params, stream) {
  renderer.renderView(view, params, stream);
}
Listing 7: Fertig nutzbare View für complate-java

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

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.