zurück zum Artikel

Raus aus der Legacy-Falle: Single Page Applications und Micro-Apps

Architektur/Methoden
Raus aus der Legacy-Falle: Single Page Applications und Micro-Apps

(Bild: bella67, Pixabay)

Micro-Apps erlauben Teams, für ihre Anwendungsbereiche selbst zu entscheiden. Somit müssen sie bei der Entwicklung nicht an bestehenden Frameworks festhalten.

Legacy: Ein Wort, das einem den Schauer über den Rücken treibt. Viele versuchen Projekte, deren Methoden das Wort enthhält, zu meiden – schließlich möchte man nicht mühevoll Know-how aufbauen, das veraltet ist. Das Problem ist nur, dass Verfahren immer schneller in den Status "Legacy" driften. Gerade im Umfeld von Single-Page-Applikationen, bei denen ständig neue JavaScript-Frameworks aufkommen, ist das ein besonderes Problem. Das gilt vor allem für jene, die langfristig zu pflegen und erweitern sind. Eine Lebensdauer von 10 bis 15 Jahren ist im Enterprise-Umfeld keine Seltenheit. Wie geht man damit um?

Microservices

Im Backend kennt man hierfür eine Antwort: Microservices. Die Idee ist an und für sich simpel und alles andere als neu: Anstatt einer großen Anwendung, die alles kann, schafft man viele kleine. Sie tauschen zwar falls nötig untereinander Nachrichten aus, funktionieren aber auch, wenn der "Nachbar" mal nicht verfügbar ist. Unterschiedliche Teams können sich nun um die einzelnen Microservices kümmern und dafür die geeignetsten Werkzeuge einsetzen. Die Teams können autark arbeiten, ohne sich ständig mit anderen Teams abstimmen zu müssen. Außerdem können sie ihren Microservice unabhängig von anderen Teams in Produktion setzen. Diesen Vorteil erkaufen sie sich mit einem hochgradig verteilten System. Es gilt, unter anderem über Fehlertoleranz, Schnittstellen und Datenkonsistenz über Servicegrenzen hinweg nachzudenken. Überträgt man die Idee in die Welt der User Interfaces (UI), spricht man neuerdings von Micro-Frontends oder Micro-Apps.

Micro-Apps

Mehrere kleine Apps statt einer großen zu entwickeln, ist zunächst keine Hexerei. Die Grenzen zwischen den Apps müssen gut definiert sein. Dabei hilft unter anderem Domain-driven Design mit der Idee des Bounded Context [1]. Salopp gesprochen, handelt es sich um eigenständige Bereiche, die von anderen isoliert sind und ihr eigenes Datenmodell haben.

In der Regel arbeiten in unterschiedlichen Bereichen auch unterschiedliche Fachexperten. Eine Anwendung zum Buchen von Flügen könnte man zum Beispiel nach den Aspekten Passagiere, Flüge und Buchungen unterteilen. Daneben ist es häufig wünschenswert, dem Benutzer die einzelnen Micro-Apps als großes Ganzes anzubieten. Das stellt Entwickler von Single Page Applications (SPA) vor eine Herausforderung, denn die vorliegenden Frameworks unterstützen das nicht. Es gilt, selbst eine Variante zu finden. Die nachfolgenden Abschnitte zeigen ein paar Möglichkeiten auf.

Hyperlinks

Die einfachste Lösung für das Zusammenfügen unterschiedlicher SPAs ist der Einsatz von Hyperlinks. Große Konzerne machen es mit ihren Produktsuiten vor, beispielsweise Google mit Google Maps (Abbildung 1). Es handelt sich um eine Micro-App, die auf eine Aufgabe spezialisiert ist. Außerdem bietet sie über einen Sushi-Menü-Button verschiedene Hyperlinks, die auf andere Anwendungen der Produktsuite verweisen.

Abbildung 1: Google Maps als Beispiel für eine Micro-App
Abbildung 1: Google Maps als Beispiel für eine Micro-App

Leider hat der einfache Ansatz auch einen enormen Nachteil: Das Gesamtsystem ist keine SPA mehr. Das bedeutet, dass der Anwendungszustand beim Wechsel der Anwendung verloren geht, sofern die Anwendung ihn nicht explizit sichert. Außerdem muss der Benutzer immer wieder eine neue Seite laden. Genau das möchten Entwickler mit Single Page Applications zur Steigerung der Benutzerfreundlichkeit verhindern. Glücklicherweise können Entwickler die Nachteile in einigen Fällen vernachlässigen: Vor allem, wenn Benutzer nur selten die App wechseln beziehungsweise die einzelnen Apps in eigenen Tabs öffnen können, ohne dass es sich komisch anfühlt.

Shell für Micro-Apps

Kommt man mit Hyperlinks nicht zum Ziel, muss häufig eine Shell her. Damit ist eine Anwendung gemeint, die die einzelnen Micro-Apps bei Bedarf lädt. Abbildung 2 zeigt ein Beispiel einer Shell, die gerade eine Angular-Micro-App (roter Rahmen) geladen hat. Wie die Icons veranschaulichen, nutzt die Micro-App Widgets von anderen Apps, die auch mit anderen Frameworks geschrieben sind. Weitere Informationen dazu finden sich in einem Beispiel des Autors [2].

Raus aus der Legacy-Falle: Single Page Applications und Micro-Apps
Abb. 2: Schematische Darstellung einer Shell

Die Shell lädt die Micro-Apps nicht nur, sondern zeigt sie auch zum richtigen Zeitpunkt an. Beispielsweise könnte eine Micro-App, die sich um die Verwaltung von Passagieren kümmert, den gerade ausgewählten Passagier an andere Micro-Apps weiterreichen. Um Abhängigkeiten zu vermeiden, kommunizieren Micro-Apps nur selten miteinander. Zur Sicherstellung einer losen Kopplung bietet sich der Einsatz von Messaging an. Das bedeutet, dass jede App bei bestimmten Ereignissen eine Nachricht veröffentlicht. Ob sich andere dafür interessieren oder nicht, sollte ihr egal sein. Schon gar nicht sollte sie eine bestimmte Antwort erwarten, denn das führt zu Abhängigkeiten und verringert die Stabilität des Gesamtsystems. Für die Implementierung der Shell stehen mehrere Ansätze zur Verfügung, wobei jeder davon seine ganz eigenen Vor- und Nachteile hat.

Integration über iframes

Eine einfache Strategie zur Umsetzung einer Shell sind iframes. Auch wenn es sich dabei wohl um eine der unbeliebtesten Web-Techniken handelt, haben sie Vorteile: Sie erlauben die Integration von Legacy-Anwendungen, egal ob sie dem Single-Page- oder dem klassischen Multi-Page-Ansatz folgen, und sie bieten die beste Isolation, die man in einer Webanwendung haben kann. Isolation bedeutet, dass eine Micro-App einer anderen Micro-App nicht in die Quere kommt. Sie können sich nicht gegenseitig ausspionieren oder manipulieren. Außerdem wirken sich Fehler sowie Styles einer Micro-App nicht auf die anderen aus. Gerade das kann bei Applikationen, deren Bestandteile verschiedene Dienstleister entwickeln, interessant sein.

Eine wohldefinierte Kommunikation zwischen iframes lässt sich über eine standardisierte API postMessage [3] durchführen. Sie erlaubt das Versenden von Nachrichten, wobei es dem Nachrichtenempfänger offen steht, Nachrichten nach deren Herkunft oder Inhalt zu filtern. Um zu verhindern, dass der iframe eine eigene Scrollbar bekommt, muss die Shell ihn regelmäßig an seinen Inhalt anpassen. Das lässt sich über den Austausch von Nachrichten bewerkstelligen: Die Micro-App sendet ihre aktuelle Höhe an die Shell und sie vergrößert beziehungsweise verkleinert den iframe. Das ist keine schöne Umsetzung, aber wenn die aufgezeigten Vorteile überwiegen, kann man sich damit arrangieren. Klar ist aber auch, dass sich iframes nicht für öffentliche Auftritte eignen und auch bei internen Anwendungen möchte man sie vermeiden, sofern man weder deren Isolation braucht noch Legacy-Anwendungen integrieren muss.

Web Components

Eine Alternative um Micro-Apps in eine Shell zu laden stellt der Einsatz von Web Components dar [4]. Einer der Standards hinter Web Components ist für solch ein Vorhaben besonders wichtig: Custom Elements. Damit lassen sich eigene HTML-Elemente erstellen. Aus Sicht von JavaScript verhalten sie sich genau wie ihre eingebauten Gegenstücke, zum Beispiel input oder table. Tags beschreiben sie und sie nehmen Daten über Eigenschaften auf JavaScript-Ebene oder Attribute auf HTML-Ebene entgegen und veröffentlichen mit Ereignissen Informationen. Das nachfolgende Beispiel zeigt, wie sich Custom Elements einbinden und nutzen lassen:

<script src="micro-app.bundle.js"></script>

<custom-slider min="-273" max="unbound" onchange="adjust(event)"></custom-slider>
<create-passenger-dialog ondone="callback(event)"></create-passenger-dialog>
<micro-app appState="…" onmessage="handle(event)"></micro-app>

Zunächst importiert das Beispiel ein Bundle mit den Custom Elements und ruft es durch Nutzung ihrer Tags auf. Über Attribute gibt der Aufrufer Daten weiter und lässt sich über Ereignisse informieren. Das Beispiel zeigt, dass Custom Elements auf verschiedenen Granularitätsebenen vorliegen können: Anwendungsfall-unabhängige Widgets (custom-slider), Anwendungsfall-bezogene Elemente (create-passenger) und ganze Anwendungen (micro-app). Letzteres mag auf den ersten Blick ein wenig verwundern, technisch gesehen handelt es sich bei Anwendungen aber nur um Komponenten, die aus weiteren Komponenten bestehen. Eine weitere Eigenschaft von Custom Elements ist, dass sie sich extrem einfach bei Bedarf laden lassen. Das ist gerade bei Micro-Apps wichtig, da jedes Team seine Bestandteile unabhängig bereitstellt. Die Shell kann sich somit bei Bedarf das neue Bundle schnappen und nutzen. Anstatt die oben gezeigten Tags direkt in der Seite zu platzieren, erzeugt sie die Shell mit traditionellem DOM-Code zur Laufzeit:

const configItem = this.config[name];
const content = document.getElementById('content');
const script = document.createElement('script');
script.src = configItem.path;
content.appendChild(script);
const element = document.createElement(configItem.element);
element.setAttribute('state', 'init');
element.addEventListener('message', msg => this.handle(msg));
content.appendChild(element);

Das Beispiel erzeugt mit createElement zwei Elemente, setzt ihre Attribute, definiert Ereignisbehandlungsroutinen und fügt sie zur Seite hinzu. Genauso flexibel lassen sich wiederverwendbare Widgets (siehe Abb. 2) einbinden, die auf Custom Elements basieren.

Zur Kommunikation zwischen Shell und Micro-App kommen das Attribut state sowie das Ereignis message zum Einsatz. Die Shell nutzt die Standardmechanismen, um die einzelnen Apps über einen Client-internen Message-Bus zu verknüpfen. Auch im Punkt Isolation haben Web Components etwas zu bieten: Dank des Standards Shadow DOM kann die Komponente sicherstellen, dass sich ihr Styling nicht auf andere Komponenten oder die Shell auswirkt. Die Isolation ist zwar nicht so stark wie bei iframes, aber für viele Fälle ist sie ausreichend. Vor allem dann, wenn die einzelnen Teams dem selben Unternehmen angehören, sollten selten Probleme aufkommen.

Außerdem können mittlerweile die einzelnen Browser sehr gut mit Web Components umgehen. Die meisten implementieren beispielsweise Custom Elements und Shadow DOM nativ. Für ältere Browser wie Internet Explorer liegen Polyfills vor [5]. Die erlauben zwar nicht alle Optionen des Standards, allerdings kann man sich damit arrangieren – vor allem wenn die Tests den Umstand von Anfang an berücksichtigen.

Jetzt stellt sich nur noch die Frage, wo man die benötigten Web Components herbekommt. Gute Nachrichten: Viele Frameworks, wie Angular oder Vue unterstützen die darunterliegenden Standards bereits ab Werk. Die richtigen Einstellungen zur Laufzeit oder beim Kompilieren überführen in solchen Fällen die Framework-Komponenten in Web Components. Aber auch die nativen Browser-APIs für Web Components sind einfach zu nutzen, sodass sich ohne viel Aufwand jeder bestehende Teil einer SPA unabhängig von der genutzten Technik in eine Web Component überführen lässt [6].

Entscheidung treffen

Nachdem nun mehrere Ansätze für die Umsetzung von Micro-Apps zur Auswahl stehen, drängt sich die Frage auf, welcher für die eigene Applikation am besten geeignet ist. Das ist nicht einfach zu beantworten, da jeder Ansatz seine Vor- und Nachteile hat, die nun gegen die Architekturanforderungen zu validieren sind. Zur Unterstützung der Entscheidung hat der Autor auf Basis praktischer Projekterfahrung den nachfolgenden Entscheidungsbaum (Abbildung 3) erstellt. Damit lassen sich zwar ebenfalls keine allgemeingültigen Antworten liefern, nichtsdestotrotz hat er sich als nützlich erwiesen, um einen ersten guten Ansatz zu identifizieren.

Raus aus der Legacy-Falle: Single Page Applications und Micro-Apps
Abbildung 3: Entscheidungsbaum für die Technikauswahl

Immer wenn es möglich ist, empfiehlt der Entscheidungsbaum den Einsatz von Hyperlinks. Das ist die einfachste Variante und das Team hält sich damit viele Optionen offen. Hyperlinks sind nur sinnvoll, wenn die Apps wenig Zustand teilen müssen und der Benutzer nur selten zwischen den Micro-Apps navigieren muss. Erstrecken sich viele zentrale Anwendungsfälle über mehrere Micro-Apps, ist der Ansatz für die Benutzerfreundlichkeit kontraproduktiv. Im Idealfall beschränken sich zwar alle Anwendungsfälle auf eine Micro-App, doch das vorgestellte Beispiel zeigt, dass das häufig nicht der Fall ist. Beispielsweise tangiert das Buchen von Flügen die drei Bereiche Flüge, Passagiere und Bezahlung.

Scheiden Hyperlinks aus, stellt der Baum die Frage, ob die Shell Legacy-Anwendungen integrieren soll oder aus den diskutierten Gründen eine starke Isolation benötigt. In solchen Fällen schlägt der Entscheidungsbaum iframes vor, obwohl sie nicht für öffentliche Portale geeignet sind.

Als nächstes wirft der Entscheidungsbaum die Frage auf, ob man autarke Teams benötigt. Das beinhaltet die Möglichkeit, eigenständig zu deployen und eigenständig Technikentscheidungen treffen zu können. Ist das nicht gewünscht, zahlt sich der Mehraufwand für Micro-Apps wahrscheinlich nicht aus. Dann empfiehlt der Baum einen Client-Monolithen. Ansonsten sind Web Components das Mittel der Wahl.

Fazit

Micro-Apps lösen das Legacy-Problem und erlauben autarke Teams. Das alles erkauft man sich mit erhötem Aufwand, der durch die Integration mehrerer Clients entsteht. Gerade bei großen Unternehmensanwendungen, die verschiedene Teams über eine lange Zeit entwickeln, warten und erweitern, dürfte sich die Arbeit meist auszahlen. Das setzt aber voraus, dass sich ein Client in sinnvolle Micro-Apps teilen lässt. (bbo [7])

Manfred Steyer [8]
ist Trainer und Berater mit Fokus auf Angular sowie Google Developer Expert. Er schreibt für O'Reilly, das deutsche Java-Magazin und heise Developer. In seinem aktuellen Buch zu Angular behandelt er die vielen Seiten des populären JavaScript-Frameworks aus der Feder von Google.


URL dieses Artikels:
http://www.heise.de/-4165029

Links in diesem Artikel:
[1] https://martinfowler.com/bliki/BoundedContext.html
[2] https://www.softwarearchitekt.at/post/2018/05/04/microservice-clients-with-web-components-using-angular-elements-dreams-of-the-near-future.aspx
[3] https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
[4] https://www.webcomponents.org/
[5] https://www.webcomponents.org/polyfills
[6] https://www.softwarearchitekt.at/post/2018/08/19/angular-react-vue-js-and-co-peacefully-united-thanks-to-micro-apps-and-web-components.aspx
[7] mailto:bbo@ix.de
[8] https://www.softwarearchitekt.at/