Stefan Zörner
16.05.2018
Micro Moves, Bauteil 3. Hier gehts um die UI-Frage und Antworten von klassisch bis SPA.
Mit den ersten beiden Bauteilen der Micro Moves Blog-Serie sind zwei Services unserer Online-Schachplattform entstanden. Der games-Service für die laufenden Partien bietet eine REST-Schnittstelle (Folge 1), der chess-diagrams-Service liefert auf HTTP-Anfrage eine PNG-Bild mit der Situation auf dem Schachbrett (Folge 2).
In dieser Folge machen wir nun erste Schritte Richtung „echtes“ Frontend und kombinieren die beiden. Ziel ist es im Web-Browser eine neue Partie zu eröffnen und Züge zu machen. Ein zweiter Spieler kann sie sehen und seinerseits ziehen. Ihr könnt dann zu zweit gegeneinander Schach spielen immerhin schon. Schönes Iterationsziel also.
Diese Folge liefert sowohl ein Beispiel für ein klassisches Request-Response-basiertes Frontend, als auch eine Single Page Application (kurz SPA). Das klassische Frontend bauen wir im games-Service ein (im Bauplan unten die (1)). Der SPA spendieren wir ein neues Modul play; sie nutzt die beiden Services. Die rote (3) markiert deren Standpunkt der SPA im Gesamtbild („Sie befinden sich hier.“).
In dieser Folge kommen Spring Web MVC mit Thymeleaf als Template-Mechanismus zum Einsatz und das JavaScript-Framework Vue.js für die SPA. Wenn ein Spieler einen Zug macht, muss sein Gegner diesen sehen. Für diese Kommunikation vom Server zu den Clients nutzen wie WebSocket.
Aber der Reihe nach …
Im Zusammenhang mit Microservices finde ich die Frage, wie man mit dem User Interface umgeht, besonders spannend. Martin Fowler und James Lewis sprechen in ihrer „Definition“ des Architekturstils von einer Anwendung, die aus unabhängigen Prozessen besteht. D.h. für den Benutzer sollte es sich wie ein UI anfühlen, obwohl die Lösung aus technologisch unterschiedlich realisierten Teilen besteht. Wie unser FLEXess hier auch. Wie stellt man das an?
In diesem Zusammenhang gibt es reichlich zu entscheiden. Rich Client vs. Web, im Falle Mobiler Endgeräte Native vs. Hybrid etc. Konzentrieren wir uns auf den häufigen Fall einer Webapplikation! Auch hier gibt es unterschiedliche Spielarten (Request/Response vs. SPA sehen wir später noch, und auch hier gibt es Hybrid-Ansätze).
Im Zuge vertikaler Architekturstile entscheidet unabhängig davon eine Frage: Trägt eine einzelne Vertikale (z.B. ein Microservice) sein UI mit sich, oder sind die Vertikalen UI-los. Die folgende Abbildung illustriert die beiden Extreme (auch hier gibt es Lösungen dazwischen).
Konkret kann das so aussehen, dass in Modell 1 links im Bild jeder Service neben der Business-Logik (BL) ein Webfrontend in sich trägt. Die Integration findet im Browser etwa über Links statt. Im Modell 2 bieten die Services die Business-Logik z.B. über eine REST-Schnittstelle an, Eine übergeordnete UI (etwa native Apps oder eine SPA) nutzt diese.
Im ersten Modell haben Teams den kompletten Technologie-Stack inkl. Frontend in der Hand und können z.B. die UI-Technologie zu einem gewissen Grad frei wählen. Dafür ist es bei diesem Ansatz fordernd, dem Anwender eine Anwendung aus einem Guss zu präsentieren. Modell 2 ermöglicht tendenziell eine bessere User Experience, aber unter Verzicht einer unabhängigen Entwicklung der einzelnen UI-Teile und echt Cross-funktionaler Teams (für das übergeordnete UI ist oft ein separates Team zuständig).
Der Widerspruch User Experience vs. unabhängige Entwicklung lässt sich abschwächen, etwa durch eine Integration im Build.
Ein technologisch „klassischer“ Ansatz gemäß Modell 1 (Vertikale mit UI Huckepack) erzeugt die Oberfläche serverseitig mit einem geeigneten Framework, und liefert an den Browser HTML-Seiten. Vielleicht angehübscht mit CSS und ein bisschen JavaScript für dynamische Aspekte, damit es nicht ganz so 90er rüberkommt. Eine Interaktion mit dem Nutzer läuft über einen Request/Response-Zyklus ab. Der Browser sendet ausgelöst durch einen Link oder das Absenden eines Formulars eine Anfrage, der Server antwortet mit dem neuen Seiteninhalt.
Zur Illustration dieses Ansatzes erweitern wir hierzu unser games-Modul für die Schachpartien entsprechend (Quelltext hier). Mit Spring Web MVC (Model View Controller) ist das flink gemacht. Insbesondere da wir in Folge 1 die Geschäftslogik getrennt von der REST-API gehalten haben. Die Klassen für die Weboberfläche findet Ihr im Paket org.flexess.games.web, das analog zur REST-API auf die Services zugreift:
Annotationen an den Controller-Klassen legen das Mapping von URLs zu Verhalten fest. Das Generieren der Seiten übernimmt Thymeleaf (Verwendung im Build-File Gradle angeben). Thymeleaf versteht sich als moderne serverseitige Java-Template-Engine.
Für mehr Details: Der Beitrag „Getting Started: Serving Web Content with Spring MVC“ nutzt ebenfalls Thymeleaf und zeigt die Integration in Spring Boot. Etwas mehr zum Funktionsumfang der Template-Engine selbst zeigt Eugen Paraschiv in seinem Artikel „Introduction to Using Thymeleaf in Spring“.
Die Templates für unser games-Modul liegen im Verzeichnis src/main/resources/templates. Sie sind mit Bootstrap angehübscht. Die Startseite erreicht Ihr aktuell mit http://localhost:8080, wenn Ihr den Service wie in Folge 1 baut und startet. Weitere Seiten sind von dort verlinkt (Screenshots siehe Abbildung unten).
Später steuert games einen Beitrag zur Gesamtoberfläche von FLEXess gemäß UI-Modell 1 bei. Das diskutieren wir, wenn ein zweites Bauteil mit eigenem UI hinzu kommt, und die beiden sich aus einem Guss präsentieren wollen.
Grundsätzlich ließe sich mit dem Ansatz die ganze Oberfläche bauen. Die größte Herausforderung liegt in der Interaktion während einer Partie. Jeder Schachzug müsste mit einem Request zum Server gelangen, und der Gegner (anderer Client, anderer Browser) ihn auch sehen, um seinerseits mit einem Zug zu antworten.
Die Dynamikanforderungen an das UI nehmen wir zum Anlass, um als Alternative zur Request/Response-basierten Oberfläche eine Single Page Application (kurz SPA) zu zeigen. Ihr könnt dieses Teil als Blaupause für ein UI nach Modell 2 sehen.
Bei einer SPA liefert der Server eine einzelne (HTML-)Seite an den Client. Jede weitere Interaktion läuft mit Hilfe von JavaScript im Browser ab. Mit dem Server kommuniziert eine SPA typischerweise via REST. Passenderweise hält das games-Modul eine solche Schnittstelle bereit (vergleiche Folge 1, die Rolle von curl und Postman dort übernimmt jetzt die SPA).
Vue.js ist ein JavaScript-Framework zur Entwicklung eben solcher SPAs. Vergleichbare Lösungen (und Mitbewerber) wären React oder Angular.js. Die Vue.js-Homepage bietet einen guten Einstieg in die Entwicklung mit dem Framework.
Für unseren Fall hier haben wir lediglich das Spielen einer Partie als SPA umgesetzt. Aus der klassischen Web-Oberfläche gelangt man über einen Link zu ihr (siehe Screenshot oben, grüner Button „Play Game!“). Die zu spielende Partie wird via ID als Request-Parameter übergeben und von der play-SPA ausgelesen.
Die Quelltexte zur SPA findet Ihr hier. Sie sind überschaubar: Lediglich eine HTML-Seite (SPA halt ;-)) und dort eingebettet drei JavaScript-Dateien. Die folgende Tabelle gibt einen Überblick:
Datei | Beschreibung |
---|---|
index.html | Trägt die initiale HTML-Seite mit Layout, Navigation etc. und verweist auf die JavaScript-Dateien |
src/main.js | Die eigentliche Vue.js-Anwendung mit Daten und Verhalten |
src/games.js | Funktionen für die REST-Calls gegen die API des games-Moduls (z.B. Ziehen) sowie für die WebSocket-Kommunikation mit games. |
src/chess-diagrams.js | Funktionen für die Einbindung der Schach-Diagramme des chess-diagrams-Moduls. |
package.json | Informationen für npm. (Siehe unten) |
Neben diese Dateien sind noch einige Fremdbibliotheken erforderlich. Vor allem Vue.js natürlich, aber auch Bootstrap für das Layout und Bibliotheken für WebSocket (siehe unten). Ich habe es mir hier einfach gemacht, und die Abhängigkeiten als Links auf ein CDN (Content Delivery Network) eingepflegt. Somit lädt der Browser sie einfach aus dem Internet. Alternativ wäre auch ein Build-Prozess mit geeigneten Tools denkbar. Das hebe ich mir für einen späteren Beitrag auf, wenn wir mehr mit JavaScript machen.
Theoretisch könnte man die Anwendung lokal im Web-Browser öffnen. Sehr realistisch wäre das aber nicht — die Spieler müssen ja irgendwie an den Client kommen. Besser wäre da schon eine Verteilung über das games-Modul, das ja bereits Seiten per HTTP ausliefert (siehe oben).
Ich habe mich für einen separaten Serverprozess für play entschieden, um die Sachen voneinander entkoppelt zu halten (Angelehnt an Modell 2). Eine recht schlanke Lösung ist ein HTTP-Server, der sich via Node.js betreiben lässt (von Node.js werden wir später in dieser Serie noch mehr hören).
„Installation“ und Betrieb können bei installiertem npm (Node Package Manager, liegt Node.js bei) so erfolgen:
$ git clone https://github.com/embarced/micro-moves.git
Cloning into 'micro-moves'...
$ cd micro-moves/modules/play/
$ npm install
$ npm start
> play@0.1.0 start /Users/stefanz/Documents/GitHub/micro-moves/modules/play
> httpserver -p 7000
lo0: 127.0.0.1
utun0: null
vboxnet1: 192.168.99.1
en5: 192.168.2.124
server started: http://0.0.0.0:7000
$
Der verwendete Port 7000 ist in package.json hinterlegt. Die Spring Web MVC-Applikation oben ist schon vorbereitet. Deren „Play Game!“-Link verweist genau auf diese Adresse mit der ID der Partie als Parameter. Die SPA stellt sich dann so im Browser dar:
Die SPA setzt nach laden der index.html einen REST-Call gegen das games-Subsystem ab. Sie holt sich Partie-Informationen zur ID, die sie zum Beispiel in der Titelzeile (Namen der Spieler) anzeigt. Hingucker ist sicherlich die Darstellung des Brettes. Unter der Haube ein Bild aus dem chess-diagrams-Service (HTML-Tag img mit geeigneten src-Attribut. Die URL wird mit dem FEN-Wert aus den Spielinformationen als Parameter versorgt). Damit das Ganze funktioniert muss neben play (lokal, Port 7000) und dem games-Services (lokal, Port 8080) auch der chess-diagrams-Service (lokal, Port 5000, vgl. Folge 2) laufen.
Die Eingabe des Zuges erfolgt entweder als Text im Input-Feld (z.B. „e2e4“ für e2 nach e4), oder durch klicken in die Brettdarstellung. Eine Funktion aus chess-diagrams.js ermittelt die Koordinaten (z.B. „h4“) und fügt sie ins Input-Feld ein. Keep it simple.
Aktuell überprüft die Anwendung noch nicht, ob der Browser-Benutzer überhaupt an der Partie beteiligt ist. Den Themen Authentifizierung und Autorisierung widmen wir einen späteren Beitrag. Folgerichtig kann ein Benutzer jetzt einfach auf dem Brett gegen sich selbst spielen.
Der Screenshot oben ist in einer neuen Partie nach einem weißen und einem schwarzen Zug entstanden, gefolgt von einem weiteren Versuch den schwarzen Springer auf f6 zu bewegen. Allzu viel prüft der games-Service aktuell nicht (später übernimmt das der Spielregeln-Service). Zu Testzwecken checkt er zumindest, ob die Farbe der Figur passt. Daher die Warnmeldung im Screenshot.
Wie läuft die Kommunikation nun genau ab? Nach Eingabe des Zuges und Abschicken des Formulars (Button „Send Move“) setzt die SPA einen REST-Call (POST Request) gegen den games-Service ab (Funktion sendMove() in games.js). Der landet dann in der Methode createMove() des GameRestController von games. Im Falle eines Fehlers (wie oben, Figur falscher Farbe) liefert diese einen passenden HTTP-Fehlercode zurück.
Wenn der Zug akzeptabel ist speichert games ihn, führt ihn aus (ermittelt also die neue Spielsituation) und aktualisiert die Game-Entität (speziell: wie sieht das Brett aus, wer ist am Zug …?). Das erfolgt in der Methode createAndPerformMove() aus der Klasse GameService in games. Die SPA könnte sich jetzt den neuen Zustand der Partie via GET-Request gegen games holen — sie „weiß“ ja, dass sich die Spielsituation durch Ausführung des Zuges geändert hat. Das ist allerdings nur die halbe Miete. Denn es gibt ja in der Regel noch einen zweiten Spieler mit einem anderen Browser, der ebenfalls über die Änderung informiert werden muss, um dann seinerseits einen Zug zu machen. Die Spieler ziehen ja abwechselnd.
Die SPA verbindet sich daher stattdessen per WebSocket mit dem games-Server, und lauscht auf Ereignisse. Der andere Spieler tut es ihm mit seinem Browser gleich. Im Falle einer neuen Spielsituation sendet games eine Nachricht an ein Topic, welche die neue Spielsituation enthält. Wenn beide Spieler mit ihren SPA dieses Topic abonnieren kriegen sie es mit. Das Topic lautet bei uns /topic/game_XY, wobei XY die ID der Partie ist.
Die Rolle des WebSocket-Servers übernimmt die Spring Boot-Applikation games. Die Konfiguration steckt in der Klasse org.flexess.games.web.WebSocketConfig. Auf Client- (also SPA-)Seite sind die betreffenden JavaScript-Funktionen in games.js implementiert (webSocketConnect() etc.). Die folgende Abbildung illustriert das Zusammenspiel zwischen zwei SPAs (Weißer und Schwarzer Spieler) und dem games-Service als Sequenzdiagramm. Weiß und Schwarz spielen jeweils einen Zug.
Bei der Umsetzung habe ich mich am Beitrag „Using WebSocket to build an interactive web application“ orientiert, der ein „Hello World“ mit Spring Boot und WebSocket beschreibt.
Das Zusammenspiel der drei Einzelteile play, games und chess-diagrams ist im Moment noch „etwas“ fragil. Als Showcase mag es taugen, aber alle Teile laufen auf localhost und die Adressen und Ports sind in der Vue.js-Applikation als Konstanten fest verdrahtet (zu Beginn von games.js und chess-diagrams.js).
Idealerweise sollte die SPA nicht alle unterschiedlichen Services persönlich kennen. Und bei horizontaler Skalierung sollten auch mehrere Exemplare der Services laufen können — unsichtbar für den Client. Derartige Themen adressieren wir mit dem nächsten Bauteil. Dann ziehen wir einen Reverse-Proxy ein.
Wenn Ihr mehr zu Vue.js erfahren wollt möchte Ich Euch noch das schöne Buch „The Majesty of Vue.js 2“ ans Herz legen (erschienen bei Leanpub, Cover rechts). Es führt mit vielen kleinen Beispielen lebendig in alle Aspekte ein, die wir hier verwendet haben (z.B. Computed Values zur Aktualisierung der Schachbrett-Graphik nach einem Zug).
Ach ja: Fragen und Anregungen sind natürlich jederzeit gerne Willkommen. Per Kommentar hier im Blog oder gerne auch per Mail direkt an mich …