Kolumne

Isomorphe JavaScript-Webanwendungen mit Java (EE) und React.JS

Niko Köbler

Niko Köbler

Kolumnist und JAX-Speaker Niko Köbler beschreibt in diesem Beitrag, wie das alte Versprechen „Write once, run everywhere“ wahr werden könnte. Was in Java nie so richtig funktionierte, könnte jetzt mit JavaScript gelingen: Wiederverwendbaren Code zu schreiben und diesen in unterschiedlichen Kontexten auszuführen.

Wir träumen alle davon: Wiederverwendbaren Code zu schreiben und diesen in unterschiedlichen Kontexten auszuführen. Was uns die Java-Plattform mit „Write once, run everywhere“ irgendwie versucht zu versprechen, gelingt meist nur mit großer Mühe und selbst dann nur mit Abstrichen. Für Server und Client gleichermaßen wiederverwendbaren Code auszuführen, das haben Java-Applets seinerseits versucht.

Applets sind heute tot.

Isomorphic to the rescue!

Iso-was?
Der Begriff „isomorph“ ist ein zusammengesetztes Wort und stammt aus dem Griechischen. „isos“ steht für „gleich“ und „morph“ für die „Form“. Isomorph bedeutet also „gleiche Form“ bzw. „gleichförmig“. Die Website isomorphic-net beschreibt den Begriff hinsichtlich der Webentwicklung wie folgt:

Isomorphism describes that if you look at the same entity in two different contexts, you should get the same thing.

Wenn wir nun also „entity“ mit „code“, „contexts“ mit „client and server“ und „thing“ mit „result“ bzw. „HTML/DOM“ ersetzen, ergibt sich daraus, dass Isomorphismus dann gegeben ist, wenn man den gleichen Code im Kontext von Client und Server ausführt, man zum gleichen Ergebnis bzw. HTML/DOM gelangt.

Nun können wir Java-Code aber nicht so ohne weiteres auf dem Server wie auch im Browser ausführen, und auch der Java-Code, den wir für unterschiedliche Laufzeitumgebungen schreiben, sieht meist unterschiedlich aus, nutzt unterschiedliche APIs. Und um Java-Code auf dem Client auszuführen, sind zusätzliche Technologien notwendig, die nicht per Default verfügbar sind.

JAX 2016 Logo

Treffen Sie Niko Köbler auf der JAX 2016:

Weitere Programminfos unter: www.jax.de

 

Anders sieht die Sache bei JavaScript aus. Jeder Browser bringt eine JavaScript-Laufzeitumgebung von Haus aus mit, steht also auf jedem Client zur Verfügung, und auf dem Server ist dank Nashorn die JavaScript-Ausführung auch ohne Zusatzbibliotheken möglich.

Unsere Anwendung kann also beispielhaft wie auf folgender Abbildung ein API aus identischem Code auf Client und Server zur Verfügung stellen:

isomorphic

Wozu brauche ich das und warum sollte ich das machen?

Isomorpher Code unterstützt uns bei der Anwendung des DRY-Principles (Don’t Repeat Yourself), da die gleiche (identische) Logik über mehrere Kontexte geteilt werden kann. Die Applikationslogik selbst muss im Idealfall nur ein einziges Mal entwickelt werden, und man muss damit nur ein einziges Code-Repository pflegen. Im Fehlerfalle macht sich das darin bemerkbar, dass der Fehler nur an einer Stelle gesucht und gefixed werden muss (Single Point of Truth). Weiterhin kann ein Team mit nur einer Sprache bzw. einer Technologie den gesamten Stack vom Backend bis zum Frontend entwickeln und muss nicht mehrere Sprachen beherrschen.

Passwort-Validierung als Beispiel

Nehmen wir an, wir haben eine komplexe, für uns perfekte Passwort-Validierung im Browser entwickelt:

   function isPasswordValid(password) {
        var score = scorePasswordStrength(password);
        return score >= 3;
    }

(Ja, das ist ein sehr abstraktes Code-Beispiel!)

Das hat uns viel Zeit (und Nerven) gekostet, und wir möchten nicht noch mal so viel Zeit aufwenden, um die gleiche Validierung im Server zu implementieren (das müssen wir ja, denn schließlich könnte ein Benutzer auch ohne das von uns programmierte Frontend die Daten senden, z.B. über das API). Zusätzlich zum Aufwand möchten wir auch vermeiden, dass sich neue Fehler einschleichen, da wir in einer anderen Sprache programmieren. Wie wäre es also, wenn wir den gleichen Programmcode auch auf dem Server verwenden könnten?

    public Boolean isPasswordValid(String password) {
        try {
            return (Boolean) nashorn.invokeFunction("isPasswordValid", password);
        } catch (ScriptException | NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }

Im oberen Code-Beispiel setzen wir voraus, dass eine Instanz der Nashorn ScriptEngine unter der Variablen `nashorn` bereits initialisiert und zu `Invocable` gecasted zur Verfügung steht. Auch wurde die JS-Datei mit der Validierungs-Funktion bereits in den Nashorn-Kontext geladen. (Wer eine umfängliche Einführung zu Nashorn sucht, darf sich gerne mein Tutorial Riding the Nashorn anschauen).

Auf der `nashorn` Variablen rufen wir nun unsere obige JavaScript-Funktion `isPasswordValid()` auf und übergeben dieser das Passwort. Den Rückgabewert (`Object`) casten wir noch zu einem Boolean und geben dies in unserer Methode ebenfalls an den Aufrufer zurück. Mit recht wenig Glue-Code haben wir somit eine bestehende JS-Funktion aus unserem Java-Code aufgerufen, wiederverwendet und uns damit viel Zeit und ggf. Ärger erspart.

Single-Page-Applications

Der derzeitige Trend der Single-Page-(Web-)Applications (SPA) führt dazu, dass der eigentliche HTML-Code, der vom Server zum Client übertragen wird, nur noch so oder so ähnlich aussieht:

    <html>
      <head>
          <title>Awesome Website</title>
          <script src="./app-bundle.js"></script>
      </head>
      <body></body>
    </html>

Das ist für den Client (technisch) in Ordnung, da die gesamte Anwendung aus dem JavaScript-Code heraus aufgebaut und initialisiert wird. Und unsere Anwendung ist nun mal in JavaScript geschrieben.

Nun sprechen aber auch ein paar Argumente gegen diesen Ansatz:

  • UX und Performance: Die User unserer Anwendung sehen nichts, bevor nicht unsere komplette Anwendung geladen und initialisiert ist. Bei älteren (langsamen) Browsern und langsamen Netzwerkverbindungen kann das durchaus negativ ins Gewicht fallen.
  • Ältere Browser können ggf. unser modernes JavaScript nicht ausführen (und es gibt leider noch ausreichend ältere Browser draußen in der großen weiten Welt).
  • Search Engine Optimization (SEO): Google & Co. sehen einfach nur eine leere Website, ohne Inhalt, den sie indizieren können. Google kann zwar mittlerweile JavaScript-Code erkennen, ausführen und damit auch den Inhalt rendern, aber das ist immer noch sehr experimentell, und man sollte sich in keinster Weise darauf verlassen!

Ist es also doch besser, alles auf dem Server zu rendern?

Ja.., nein.., vielleicht..!

Immerhin haben moderne SPA’s ja auch durchaus Vorteile, was UX, Seiten-Rendering und Daten-Austausch anbelangt. Das ist aber eine separate Diskussion Wert. Legt man nun also zu Grunde, dass wir gleichermaßen Vor- und Nachteile bei SPAs haben, können wir folgendes Szenario als eine mögliche Lösung sehen:

  1. der Anwender fordert die erste URL an
  2. der Server sammelt die Daten für diesen Request
  3. der Server rendert die Seite und liefert sie an den Client aus
  4. der Anwender kann zeitnah den Inhalt konsumieren
  5. in der Zwischenzeit wird der Client initialisiert
  6. der Anweder ruft eine weitere URL auf
  7. nun bezieht der Client die Daten (z.B. von einem API)
  8. und der Client rendert die nächsten Seiten für die Anzeige.

Mit diesem Ansatz bekommt der Anwender schnell eine Antwort vom Server, mit der er „arbeiten“ kann. Gleichermaßen erhält auch eine Suchmaschine eine indizierbare Antwort. Wenn nun der Client (Browser oder Suchmaschine) JavaScript-Code versteht und verarbeiten kann, können alle weiteren Requests vom Client aus beantwortet werden. Umständliche und langwierige Roundtrips zum Server können vermieden werden. Sollte keine JS-Ausführung zur Verfügung stehen, muss aber auch der Server wieder in der Lage sein, den Request zu verarbeiten. Dennoch müssen wir im Idealfall unsere Anwendung nur ein einziges Mal entwickeln, da ja der gleiche Code auf beiden Seiten ausgeführt werden kann.

React.JS

React.JS ist seit einigen Monaten der neue Stern am Himmel der Web- und SPA-Entwicklung und wurde von Facebook entworfen. Dabei ist React kein vollständiges Frontend-Framework, sondern beschreibt sich selbst als  „JavaScript library for building user interfaces„. In einer MVC-Architektur stellt React also nur das „V“ dar, das „M“ und das „C“ muss man anderweitig hinzufügen. Im Falle von React ist das meistens eine Architektur basierend auf dem Flux-Ansatz, ebenfalls von Facebook entwickelt. Flux selbst ist nur die Beschreibung der Architektur (welche sehr stark auf Immutables setzt), Implementierungen gibt es viele verschiedene, wobei sich in den letzten Monaten Redux als eine der beliebtesten in der React-Community herauskristallisiert hat.

Für die Beschreibung der Komponenten macht sich React einer Technolgie namens JSX zu Nutze. JSX ist eine Mischung aus JavaScript und HTML und im ersten Augenblick vielleicht etwas gewöhnungsbedürftig:

    class Book extends React.Component {
        render() {
            return (
                <div className="book">
                    <h3>{this.props.author}</h3>
                    <div className="lead">{this.props.children.toString()}</div>
                </div>
            );
        }
    }

(Der ein oder andere mag sich vielleicht etwas an JSP erinnert fühlen).

Dieser JSX-Code wird mit Hilfe von Babel zu von allen Browsern ausführbarem ECMAScript 5.1 Code transpiliert:

    var Book = React.createClass({displayName: "Book",
        render: function () {
            return (
                React.createElement("div", {className: "book"},
                    React.createElement("h3", null, this.props.author),
                    React.createElement("div", {className: "lead"}, this.props.children.toString())
                )
            );
        }
    });

Und zur Laufzeit wird dies zu validem HTML-Code gerendert:

    <div class="book" data-reactid=".1c3dv7jhtco.1.$0">
        <h3 data-reactid=".1c3dv7jhtco.1.$0.0">George Orwell</h3>
        <div class="lead" data-reactid=".1c3dv7jhtco.1.$0.1">1984</div>
    </div>

Anhand der `data-reactid` Attribute ist React in der Lage, die Elemente aus dem DOM in seinem eigenen, sog. Virtual-DOM wiederzufinden und zu ermitteln, ob diese Elemente bei einer Seitenänderung aktualisert werden müssen, oder ob diese unverändert bleiben können.

Wer mehr über React, JSX und Flux wissen möchte, wird unter den angegebenen Links fündig.

React.JS und Java

React.JS ist aber nicht nur ein weiteres Web-UI-Framework in der Reihe von vielen anderen. React bietet von Haus aus die Möglichkeit, die Anwendung nicht nur auf dem Client, sondern auch auf dem Server zu rendern. Als Server ist hier vorrangig eine Node.js-Instanz gedacht. Aber im Grunde ist jeder Server möglich, der JavaScript-Code ausführen kann – so also auch Java-basierte Server, die mit Nashorn in der Lage sind, JavaScript-Code auszuführen.

Spring Boot

Schon früh gab es erste Ansätze und Implementierungen, die React mit Spring Boot in Kombination nutzten. Als Beispiel sollen folgende zwei genügen:

sdeleuze/spring-react-isomorphic
von Sébastien Deleuze, Developer des Spring Framework Teams bei Pivotal, Frankreich

winterbe/spring-react-example
von Benjamin Winterberg, Entwickler aus Deutschland

Java EE MVC

Für den neuen MVC 1.0 Standard in Java EE 8 habe ich für dessen Referenz-Implementierung Ozark eine `ViewEngine` geschrieben, die React.JS als Templating-Framework nutzt:

dasniko/ozark-react
die eigentliche `ViewEngine`-Implementierung
dasniko/ozark-react-example
ein lauffähiges Beispiel, welches die `ReactViewEngine` nutzt, inhaltlich leicht angelehnt an das offizielle React-Tutorial.

Eine ausführliche Beschreibung der Arbeitsweise der `ReactViewEngine` gibt es im entsprechenden Kapitel meines Riding-the-Nashorn Tutorials. Hier nur eine grobe Beschreibung des Ablaufs und der Zuständigkeiten:

Die Klasse `ReactViewEngine` ermittelt aus dem `ViewEngineContext` das in der Klasse `ReactController` spezifierte HTML-Template und die JavaScript-Funktion, die React zum Rendern der Seite verwenden soll. Ebenfalls stellt der `ReactController` die Daten im Kontext zur Verfügung, die zum Rendern verwendet werden sollen. Die `ReactViewEngine` delegiert zum eigentlichen Rendern an die Klasse `React`, die einen Thread-sicheren Nashorn-Kontext mit allen benötigten JS-Bibliotheken (React und ein paar notwendige Polyfills) zur Verfügung hält.

Thread-Sicherheit ist in unserem Falle deshalb notwendig, da React von Haus aus nicht Thread-safe implementiert ist. Das ist für React selbst insofern auch nicht wichtig, da JavaScript im Browser und in Node.js nur Single-Threaded ausgeführt wird (werden kann). Auf der JVM gibt es aber Threads, und mit Nashorn wäre auch eine parallele Ausführung von JavaScript-Code möglich. Globale Variablen würden sich dann aber ggf. gegenseitig beeinflussen.

Die JavaScript-Datei `bookBox.js` enthält den benötigten JSX-Code, der sowohl auf Client- wie auch auf Server-Seite ausgeführt werden kann. Der einzige Unterschied sind die beiden Funktionen `renderServer()` bzw. `renderClient()`, die den Rendering-Prozess auf die jeweilig passende Art in React antriggern. Da `bookBox.js` mit JSX-Code geschrieben ist, muss diese Datei vor der Verwendung/Ausführung (natürlich) noch nach ES5 transpiliert werden. Dies geschieht in meinem Beispiel zum Build-Zeitpunkt, könnte aber seit einem der letzten Updates der JVM (>=8u72) mit Babel auch zur Laufzeit geschehen.

Das HTML-Template (der Einfachheit halber basierend auf einer JSP) `react.jsp` stellt das HTML-Skeleton für die benötigen Ressourcen im Browser zur Verfügung und bestimmt das `<div>` Element, in dem der von React gerenderte Inhalt dargestellt werden soll. Die im Header-Bereich referenzierten JavaScript-Biblitheken stammen aus WebJars, welche im Maven- bzw. Gradle-Buildfile angegeben wurden, um diese auch in den React-Klassen verwenden zu können.

Würde die Anwendung nun ohne Server-seitiges Rendering ausgeführt werden, wäre im HTML-Quellcode nicht viel zu sehen. Im Grunde nur das, was auch im `react.jsp` Template angegeben ist, zuzüglich einiger JSON-Daten, mit denen der Client die Anwendung rendern kann. Wird nun das Server-seitige Rendering aktiviert, sind im Quellcode die gesamten HTML-Elemente zu finden, die zur Darstellung der Seite notwendig sind. Der React-Client wird nach dem Laden der Seite im Hintergrund aktualisiert und dann ausgeführt. Dank des VirutalDOM von React ist kein „Flackern“ beim Aktualiseren der Seite sichtbar und ein flüssiges Arbeiten ist möglich. Alle weiteren Requests können nun vom Client aus bearbeitet werden.

Isomorphic vs. Universal JavaScript

Zum Abschluss noch eine Anmerkung zum Begriff „Isomorphic JavaScript“. Isomorph ist nicht nur in der deutschen Sprache recht unbekannt und nichtssagend. Deshalb gibt es auch Stimmen, die von „Universal JavaScript“ sprechen.  Universell soll den unversellen Einsatz des Codes mehr unterstreichen. Anderen wiederum ist der Begriff „universal“ zu universell und zu generisch. Diese Personen reden dann von „Isomorphic/Universal JavaScript“, was aber auch nicht besser ist. Egal, wie man es nennt und warum welcher Begriff besser geeignet sei, es beschreibt letztendlich den gleichen Ansatz, den selben Code auf Client- wie auch auf Serverseite zu nutzen und auszuführen.

Geschrieben von
Niko Köbler
Niko Köbler
Niko Köbler ist freiberuflicher Software-Architekt, Developer & Trainer für Java & JavaScript-(Enterprise-)Lösungen, Integrationen und Webdevelopment. Er ist Co-Lead der JUG Darmstadt, schreibt Artikel für Fachmagazine und ist regelmäßig als Sprecher auf internationalen Fachkonferenzen anzutreffen. Niko twittert @dasniko.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.