Kolumne: Die Angular-Abenteuer

Angular in einer Microservices-Welt
Keine Kommentare

In der Kolumne „Die Angular-Abenteuer“ stürzt sich Manfred Steyer mitten ins Getümmel der dynamischen Welt der Webentwicklung. Tipps und Tricks für Angular-Entwickler kommen dabei ebenso zur Sprache wie aktuelle Entwicklungen im JavaScript-Ökosystem.

Angular in einer Microservices-Welt

Microservices erfreuen sich derzeit großer Beliebtheit, Single Page Applications auch. Wie man diese beiden Ansätze am besten verwendet, habe ich in den letzten Monaten immer wieder mit meinen Kunden diskutiert. Das Spannende daran ist, dass es hierfür keine eindeutige Antwort gibt. Wie so häufig, hängt es stark von den Prioritäten ab und somit auch von den Gründen, warum die Entscheidung für Microservices gefallen ist. Deshalb stelle ich hier einige Möglichkeiten vor und bewerte sie im Hinblick auf ausgewählte Architekturziele, die für Microservices sprechen, hier aufgelistet in Tabelle 1.

Architekturziel Beschreibung
Isolation zu anderen SPA Inwieweit ist die Angular-Anwendung von anderen Single Page Applications, die im Browser laufen, abgeschottet? Können sich die einzelnen Anwendungen gegenseitig beeinflussen?
Seperates Deployment Lassen sich die einzelnen Single Page Applications separat deployen?
Unterschiedliche Frameworks Können unterschiedliche Singe Page Applications mit unterschiedlichen Frameworks in unterschiedlichen Versionen geschrieben werden?
Single Page Shell Präsentiert sich die Shell, die die Navigation zwischen den einzelnen Single Page Applications erlaubt, auch als SPA oder verhält sich die Lösung wie eine klassische Webanwendung, die einzelnen Seiten nachlädt und keine Zustände lokal vorhalten kann?
Tree Shaking Lassen sich Bundle-Größen durch das Abstoßen nicht benötigter Frameworkbestandteile optimieren?

Tabelle 1: Architekturziele für SPA im Microservices-Umfeld

Variante eins: Integration via Hyperlinks

Die wohl einfachste Möglichkeit, verschiedene Clients im Rahmen einer Microservices-Architektur zu integrieren, ist der Einsatz von Hyperlinks. Auf diese Weise referenzieren sich die einzelnen Clients gegenseitig. Außerdem können übergeordnete Shell-Anwendungen die Clients aufrufen. Während es sich bei diesen Clients um SPAs handelt, trifft das auf die Gesamtlösung nicht zu. Die Seitenwechsel beim Übergang zwischen den Clients hat zur Folge, dass die aktuelle SPA beendet wird und auch deren Zustände verloren gehen. Gerade die Möglichkeit, Zustände im Client vorhalten zu können, führt jedoch bei SPA zu einer erhöhten Benutzerfreundlichkeit.

Das ist ein großer Unterschied zu klassischen Webanwendungen, wo Zustände im Weblayer nicht gerne gesehen sind. Um diesen Nachteil zu kompensieren, könnte man die clientseitigen Zustände regelmäßig sichern. Hierfür bieten sich Browserdatenbanken wie IndexedDB an. Alternativ dazu lassen sich die Zustände auch zum Server senden, um sie dort in einer Datenbank zu persistieren. Dieser Ansatz hat den Vorteil, dass der Benutzer zu einem späteren Zeitpunkt genau dort weitermachen kann, wo er aufgehört hat.

Angular Kickstart: von 0 auf 100

mit Christian Liebel (Thinktecture AG) und Peter Müller (Freelancer)

JavaScript für Softwareentwickler – für Einsteiger und Umsteiger

mit Yara Mayer (evia) und Sebastian Springer (MaibornWolff)

Doch Wegsichern ist hier leichter gesagt als getan. Wo bekommt man die Zustände her und wie stellt man sicher, dass man alle Zustände berücksichtigt? Eine Antwort auf diese Frage liefert das in der SPA-Welt beliebte Muster Redux. Es sieht vor, dass der gesamte Anwendungszustand von einem Store verwaltet wird (Abb. 1). Diesen Store kann man sich als In-Memory-Datenbank vorstellen, die den Anwendungszustand in einem Objektbaum speichert. Die Knoten auf der ersten Ebene entsprechen bei dieser Metapher mehr oder weniger einzelnen Tabellen oder Collections, um ein Wort aus dem NoSQL-Umfeld zu bemühen. Die Anwendung kann auf sämtliche Knoten lesend zugreifen oder sich per Publish/Subscribe über Änderungen informieren lassen.

Um zu verhindern, dass jede Komponente wahllos in den Baum schreibt, ist ein direkter schreibender Zugriff hingegen nicht vorgesehen. Stattdessen senden Komponenten entsprechende Aktionen an den Store, der sie an so genannte Reducer weiterleitet. Hierbei handelt es sich um Funktionen, die den Zustandsbaum in einer wohldefinierten Weise abändern. Da hier der Anwendungszustand zentral vorliegt, lässt er sich einfach persistieren und später wiederherstellen. Wer diesen Weg mit Angular einschlagen möchte, sollte einen Blick auf die Bibliothek @ngrx/store werfen. Da ihre Entwicklung von einem Core-Teammitglied angestoßen wurde und sich die Bibliothek somit sehr gut in Angular integriert, handelt es sich um die De-facto-Implementierung in diesem Bereich.

Abb. 1: Der Store bei Redux (@ngrx/store) verwaltet einen zentralen Zustand

Abb. 1: Der Store bei Redux (@ngrx/store) verwaltet einen zentralen Zustand

Variante zwei: Integration über iFrames

„Wirklich? iFrames?“ Das ist die erste Reaktion, die ich erhalte, wenn ich diese Lösung vorstelle. Und das kann ich auch durchaus verstehen, denn iFrames werden seit jeher für Hacks verwendet. Ihre großen Geschwister, die von Netscape in den 90ern eingeführten Framesets, sind auch mittlerweile aus dem HTML-Standard verschwunden, nachdem sie jahrelang als Antipattern angesehen wurden.

Aber wenn man ein wenig über die Eigenschaften von iFrames nachdenkt, sieht man, dass sie sich für das hier skizzierte Vorhaben gut eignen.

Wie bei der Integration über Hyperlinks bieten sie beispielsweise eine perfekte Isolation zwischen einzelnen Clients. Somit lassen sich auch verschiedene Frameworks in verschiedenen Versionen nutzen, ohne dass sie sich in die Quere kommen.

Wie die Bewertung am Ende zeigt, erfüllen iFrames sogar alle hier gestellten Anforderungen. Beispielsweise können Zustände erhalten bleiben, wenn die Anwendung jeden Client in einen eigenen iFrame lädt und ihn bei Bedarf ein- oder ausblendet.

Außerdem gibt es zur Kommunikation zwischen iFrames ein standardisiertes JavaScript-API. iFrames haben natürlich auch Nachteile: Grafische Elemente können nicht über iFrames ragen. Außerdem ist es gut, wenn der iFrame genau so groß wie sein Inhalt ist, damit der Benutzer nicht innerhalb des iFrames scrollen muss. Die gute Nachricht: Letzteres lässt sich mit JavaScript lösen. Und da dieses Problem auch andere bereits hatten, findet man hierfür auch Bibliotheken.

Variante drei: Metaframeworks

Metaframeworks, die einzelne Clients via JavaScript in den Browser laden oder später wieder entladen, können die Nachteile von iFrames kompensieren. Ein Beispiel dafür ist das gern zitierte Single SPA. Da hier jeder Client in dasselbe Browserfenster geladen wird, teilen sich die Clients auch denselben globalen Namensraum. Das ist problematisch, wenn sich Frameworks auf globale Variablen stützen, was z. B. bei jQuery der Fall ist.

Das kommt heutzutage zwar selten für neue Single Page Applications zum Einsatz, schlummert aber im Hintergrund vieler Komponentenbibliotheken. Genauso problematisch gestalten sich aber auch Bibliotheken, die globale Browserobjekte verändern. Hierzu zählt unter anderem das von Angular verwendete Zone.js.

Variante vier: Plug-in-Architekturen

Angular erlaubt das dynamische Laden von Modulen. Am einfachsten lässt sich das über den Router realisieren, der diese Möglichkeit für Lazy Loading nutzt. Die darunterliegenden Low-Level-APIs lassen sich jedoch auch direkt zum Laden von Modulen und zum dynamischen Erzeugen der Komponenten einsetzen. Spannend wird es, wenn man die einzelnen Module separat kompilieren und deployen möchte, denn dies wird nicht von webpack und dem darauf basierenden Angular CLI unterstützt. Auch wenn diese beiden Technologien das Referenzieren anderer Dateien mit require und import erlauben, gehen sie davon aus, dass alles gemeinsam kompiliert wird. Somit muss man für diesen Ansatz auf eine andere Toolchain setzen.

Hierfür bietet sich die Kombination von SystemJS und Gulp an. Ein weit verbreitetes Seed-Projekt befindet sich hier. Ein Proof of Concept für die hier skizzierte Idee ist hier zu finden. Während Plug-in-Architekturen sowohl ein separates Deployment erlauben als auch alle geladenen Plug-ins nahtlos auf einer Seite darstellen, kommen sie mit einem Nachteil: Man geht davon aus, dass für alle Module zueinander kompatible Versionen von Angular zum Einsatz kommen.

Variante fünf: npm-Pakete

Mein letzter Vorschlag ist, verschiedene npm-Pakete zu nutzen. Jedes npm-Paket könnte einen der einzelnen Clients anbieten. Somit lässt sich jeder Client separat implementieren und testen. Um die npm-Pakete anderen Teams bereitzustellen, können sie über eine npm-Registry verteilt werden. Dabei kann es sich auch um eine Lösung wie Nexus oder Artifactory handeln. Falls solch eine Lösung in Ihrer Umgebung noch nicht existiert, bietet sich die äußerst leichtgewichtige Implementierung verdaccio für das schnelle Glück an. Das Generieren von npm-Paketen wird vom Angular CLI noch nicht unterstützt. Allerdings existieren Alternativen, die im Handumdrehen das Grundgerüst für solch ein Vorhaben herbeizaubern.

Ich habe gute Erfahrungen mit der Lösung generator-angular2-library gemacht. Der Nachteil dieser Lösung ist, dass die npm-Pakete vor dem Ausliefern in eine Shell-Anwendung integriert werden müssen. Diese ist gemeinsam mit allen Paketen zu kompilieren, und danach wird alles gemeinsam ausgeliefert. Ein separates Deployment ist somit nicht möglich. Allerdings macht das gemeinsame Kompilieren Optimierungen wie Tree Shaking möglich.

Bewertung: Es kommt darauf an

Wie Tabelle 2 zeigt, erfüllt der iFrame-basierte Ansatz alle eingangs festgelegten Architekturziele. Aber auch die Integration mit Hyperlinks erscheint aufgrund ihrer Einfachheit verlockend. Das soll jedoch nicht heißen, dass diese beiden Lösungen die besten sind. Tatsächlich hängt es davon ab, wie wichtig die hier aufgelisteten Architekturziele sind. Beispielsweise habe ich viele erfolgreiche Projekte gesehen, die auf npm-Pakete setzen. Gerade wenn das separate Deployment nicht wichtig ist und auf breiter Basis Angular zum Einsatz kommt, ist dieser geradlinige Ansatz gut geeignet, um einzelne Clients separat zu entwickeln.

Isolation zu anderen SPA Seperates Deployment Unterschiedliche Frameworks Single Page Shell Tree Shaking
Links x x x x
iFrames x x x x x
Metaframework
Plug-in-Architektur x x
npm-Pakete x x

Tabelle 2: Bewertung der vorgestellten Ansätze anhand ausgewählter Architekturziele

Fazit

Für die Umsetzung von Clients im Rahmen von Microservices-Architekturen gibt es keine perfekte Lösung. Was ist gibt, sind jedoch einige Lösungen, die ihre eigenen Vor- und Nachteile bringen. Deswegen gilt es, diese vor dem Hintergrund der vorherrschenden Architekturziele zu bewerten und Strategien zum Kompensieren negativer Konsequenzen zu finden.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu:
X
- Gib Deinen Standort ein -
- or -