Die Stärken und Vorteile von Micro Frontends

Veröffentlicht: 2022-03-11

Die Mikro-Frontend-Architektur ist ein Designansatz, bei dem eine Frontend-App in einzelne, halbunabhängige „Mikroapps“ zerlegt wird, die lose zusammenarbeiten. Das Micro-Frontend-Konzept ist vage von Microservices inspiriert und nach ihnen benannt.

Zu den Vorteilen des Mikro-Frontend-Musters gehören:

  1. Micro-Frontend-Architekturen sind möglicherweise einfacher und daher einfacher zu begründen und zu verwalten.
  2. Unabhängige Entwicklungsteams können einfacher an einer Front-End-App zusammenarbeiten.
  3. Sie können ein Mittel für die Migration von einer „alten“ App bereitstellen, indem sie eine „neue“ App parallel dazu ausführen.

Obwohl Mikro-Frontends in letzter Zeit viel Aufmerksamkeit erregt haben, gibt es bis jetzt noch keine einzelne dominante Implementierung und kein klares „bestes“ Mikro-Frontend-Framework. Vielmehr gibt es je nach Zielsetzung und Anforderung unterschiedliche Ansätze. In der Bibliographie finden Sie einige der bekannteren Implementierungen.

In diesem Artikel werden wir einen Großteil der Theorie von Mikro-Frontends überspringen. Folgendes werden wir nicht behandeln:

  • „Aufteilen“ einer App in Mikroapps
  • Bereitstellungsprobleme, einschließlich der Frage, wie Mikro-Frontends in ein CI/CD-Modell passen
  • Testen
  • Ob Mikroapps in Eins-zu-Eins-Ausrichtung mit Mikrodiensten im Back-End sein sollten
  • Kritik am Micro-Frontend-Konzept
  • Der Unterschied zwischen Mikro-Frontends und einer einfachen alten Komponentenarchitektur

Stattdessen präsentieren wir ein Micro-Frontend-Tutorial, das sich auf eine konkrete Implementierung konzentriert und die wichtigen Probleme in der Micro-Frontend-Architektur und ihre möglichen Lösungen hervorhebt.

Unsere Implementierung heißt Yumcha. Die wörtliche Bedeutung von „Yum Cha“ auf Kantonesisch ist „Tee trinken“, aber seine alltägliche Bedeutung ist „Dim Sum essen gehen“. Die Idee dabei ist, dass die einzelnen Mikroapps innerhalb einer Makroapp (wie wir die zusammengesetzte App der obersten Ebene nennen werden) analog zu den verschiedenen Körben mit mundgerechten Portionen sind, die bei einem Dim-Sum-Mittagessen herausgebracht werden.

Übersichtsdarstellung einer beispielhaften Micro-Frontend-basierten App wie oben beschrieben.

Wir werden Yumcha manchmal als „Mikro-Frontend-Framework“ bezeichnen. In der heutigen Welt wird der Begriff „Framework“ normalerweise verwendet, um Angular, React, Vue.js oder andere ähnliche Aufbauten für Web-Apps zu bezeichnen. Wir sprechen überhaupt nicht von einem Rahmen in diesem Sinne. Wir nennen Yumcha nur der Einfachheit halber ein Framework: Es ist eigentlich eher eine Reihe von Tools und ein paar dünne Schichten zum Erstellen von Micro-Frontend-basierten Apps.

Micro-Frontend-Tutorial Erste Schritte: Markup für eine zusammengesetzte App

Lassen Sie uns eintauchen, indem wir darüber nachdenken, wie wir eine Makroapp und die Mikroapps, aus denen sie besteht, definieren könnten. Markup war schon immer das Herzstück des Webs. Unsere Makroapp wird daher durch nichts Komplizierteres als dieses Markup spezifiziert:

 <html> <head> <script src="/yumcha.js"></script> </head> <body> <h1>Hello, micro-frontend app.</h1> <!-- HERE ARE THE MICROAPPS! --> <yumcha-portal name="microapp1" src="https://microapp1.example.com"></yumcha-portal> <yumcha-portal name="microapp2" src="https://microapp2.example.com"></yumcha-portal> </body> </html>

Durch die Definition unserer Makroapp mithilfe von Markup haben wir vollen Zugriff auf die Leistungsfähigkeit von HTML und CSS, um unsere Mikroapps zu gestalten und zu verwalten. Beispielsweise könnte eine Mikroapp auf einer anderen sitzen, oder neben ihr, oder in einer Ecke der Seite, oder in einem Bereich eines Akkordeons, oder verborgen bleiben, bis etwas passiert, oder dauerhaft im Hintergrund bleiben .

Wir haben das benutzerdefinierte Element, das für Mikroapps verwendet wird, <yumcha-portal> genannt, weil „Portal“ ein vielversprechender Begriff für Mikroapps ist, die im Portalvorschlag verwendet werden, einem frühen Versuch, ein Standard-HTML-Element für die Verwendung in Mikro-Frontends zu definieren.

Implementieren des benutzerdefinierten Elements <yumcha-portal>

Wie sollten wir <yumcha-portal> implementieren? Da es sich um ein benutzerdefiniertes Element handelt, natürlich als Webkomponente! Wir können aus einer Reihe starker Konkurrenten für das Schreiben und Kompilieren von Micro-Frontend-Webkomponenten wählen; Hier verwenden wir LitElement, die neueste Iteration des Polymer-Projekts. LitElement unterstützt TypeScript-basierten syntaktischen Zucker, der die meisten benutzerdefinierten Elementbausteine ​​für uns verarbeitet. Um <yumcha-portal> für unsere Seite verfügbar zu machen, müssen wir den entsprechenden Code als <script> einfügen, wie wir es oben getan haben.

Aber was macht <yumcha-portal> eigentlich? Eine erste Annäherung wäre, einfach einen iframe mit der angegebenen Quelle zu erstellen:

 render() { return html`<iframe src=${this.src}></iframe>`; }

… wobei render der standardmäßige LitElement-Render-Hook ist, der sein html -getaggtes Template-Literal verwendet. Diese minimale Funktionalität könnte für einige triviale Anwendungsfälle fast ausreichen.

Mikroapps in iframe einbetten s

iframe s sind das HTML-Element, das jeder gerne hasst, aber tatsächlich bieten sie ein äußerst nützliches, felsenfestes Sandboxing-Verhalten. Es gibt jedoch noch eine lange Liste von Problemen, die bei der Verwendung von iframe werden müssen, mit potenziellen Auswirkungen auf das Verhalten und die Funktionalität unserer App:

  • Erstens haben iframe bekannte Macken in Bezug auf Größe und Layout.
  • CSS wird natürlich vollständig vom iframe isoliert , auf Gedeih und Verderb.
  • Die „Zurück“-Schaltfläche des Browsers funktioniert einigermaßen gut, obwohl der aktuelle Navigationsstatus des iframe nicht in der URL der Seite widergespiegelt wird , sodass wir weder URLs ausschneiden und einfügen konnten, um zum gleichen Zustand der zusammengesetzten App zu gelangen, noch Deep-Links zu ihnen.
  • Die Kommunikation mit dem iframe von außen muss je nach CORS-Setup möglicherweise über das postMessage -Protokoll erfolgen .
  • Für die Authentifizierung über iframe Grenzen hinweg müssen Vorkehrungen getroffen werden.
  • Einige Screenreader stolpern möglicherweise über die iframe -Grenze oder benötigen einen Titel für den iframe , den sie dem Benutzer ankündigen können.

Einige dieser Probleme können vermieden oder gemildert werden, indem keine iframe s verwendet werden, eine Alternative, die wir später in diesem Artikel besprechen.

Auf der positiven Seite wird der iframe eine eigene, unabhängige Content-Security-Policy (CSP) haben. Auch wenn die iframe , auf die der Iframe verweist, einen Dienstmitarbeiter verwendet oder serverseitiges Rendering implementiert, funktioniert alles wie erwartet. Wir können auch verschiedene Sandboxing-Optionen für den iframe angeben, um seine Fähigkeiten einzuschränken, z. B. die Möglichkeit, zum oberen Frame zu navigieren.

Einige Browser haben ein loading=lazy Attribut für iframe ausgeliefert oder planen dies, das das Laden von Below-the-Fold- iframe verzögert, bis der Benutzer in ihre Nähe scrollt, aber dies bietet nicht die feinkörnige Kontrolle des Lazy Loadings wir wollen.

Das eigentliche Problem mit iframe besteht darin, dass der Inhalt des iframe mehrere Netzwerkanfragen zum Abrufen benötigt. Die index.html der obersten Ebene wird empfangen, ihre Skripte werden geladen und ihr HTML-Code wird geparst – aber dann muss der Browser eine weitere Anfrage für den HTML-Code des iframe initiieren, auf den Empfang warten, seine Skripte parsen und laden und rendern Inhalt von iframe In vielen Fällen müsste das JavaScript des iframe dann noch hochfahren, seine eigenen API-Aufrufe durchführen und sinnvolle Daten erst anzeigen, nachdem diese API-Aufrufe zurückgegeben und die Daten zur Anzeige verarbeitet wurden.

Dies führt wahrscheinlich zu unerwünschten Verzögerungen und Wiedergabeartefakten, insbesondere wenn mehrere Mikroapps beteiligt sind. Wenn die App des iframe SSR implementiert, hilft dies, vermeidet aber dennoch nicht die Notwendigkeit zusätzlicher Roundtrips.

Eine der wichtigsten Herausforderungen, denen wir uns bei der Gestaltung unserer Portalimplementierung gegenübersehen, ist der Umgang mit diesem Round-Trip-Problem. Unser Ziel ist es, dass eine einzige Netzwerkanfrage die gesamte Seite mit all ihren Mikroapps herunterfährt, einschließlich aller Inhalte, die jede von ihnen vorbelegen kann. Die Lösung für dieses Problem liegt im Yumcha-Server.

Der Yumcha-Server

Ein Schlüsselelement der hier vorgestellten Mikro-Frontend-Lösung ist die Einrichtung eines dedizierten Servers zur Verarbeitung der Mikroapp-Komposition. Dieser Server leitet Anforderungen an die Server weiter, auf denen die jeweilige Mikroapp gehostet wird. Zugegeben, es erfordert einige Mühe, diesen Server einzurichten und zu verwalten. Einige Mikro-Frontend-Ansätze (z. B. Single-Spa) versuchen, im Namen der einfachen Bereitstellung und Konfiguration auf die Notwendigkeit solcher speziellen Server-Setups zu verzichten.

Die Kosten für die Einrichtung dieses Reverse-Proxys werden jedoch durch die Vorteile, die wir erzielen, mehr als ausgeglichen; Tatsächlich gibt es wichtige Verhaltensweisen von Mikro-Frontend-basierten Apps, die wir ohne sie einfach nicht erreichen können. Es gibt viele kommerzielle und kostenlose Alternativen zum Einrichten eines solchen Reverse-Proxys.

Der Reverse-Proxy leitet nicht nur Mikroapp-Anforderungen an den entsprechenden Server, sondern auch Makroapp- Anforderungen an einen Makroapp-Server. Dieser Server bereitet das HTML für die komponierte App auf besondere Weise auf. Beim Empfang einer Anfrage nach index.html vom Browser über den Proxy-Server unter einer URL wie http://macroapp.example.com ruft er die index.html ab und unterzieht sie dann einer einfachen, aber entscheidenden Transformation, bevor er zurückkehrt es.

Insbesondere wird der HTML-Code auf <yumcha-portal> -Tags analysiert, was mit einem der kompetenten HTML-Parser, die im Node.js-Ökosystem verfügbar sind, problemlos durchgeführt werden kann. Unter Verwendung des src -Attributs für <yumcha-portal> wird der Server kontaktiert, auf dem die Mikroapp ausgeführt wird, und seine index.html wird abgerufen – einschließlich serverseitig gerenderter Inhalte, falls vorhanden. Das Ergebnis wird als <script> - oder <template> -Tag in die HTML-Antwort eingefügt, um nicht vom Browser ausgeführt zu werden.

Illustration der Serverarchitektur von Yumcha. Der Browser kommuniziert mit dem Reverse-Proxy, der wiederum mit der Makroapp und jeder der Mikroapps kommuniziert. Der Makroapp-Schritt transformiert und füllt die Hauptdatei index.html der App vor.

Die Vorteile dieses Setups bestehen in erster Linie darin, dass der Server bei der allerersten Anfrage nach der index.html für die zusammengesetzte Seite die einzelnen Seiten von den einzelnen Mikroapp-Servern vollständig abrufen kann – einschließlich SSR-gerenderter Inhalte, falls any – und liefert eine einzelne, vollständige Seite an den Browser, einschließlich des Inhalts, der verwendet werden kann, um den Iframe ohne zusätzliche Server-Roundtrips zu srcdoc iframe ). Der Proxy-Server stellt auch sicher, dass alle Details darüber, von wo aus die Mikroapps bereitgestellt werden, vor neugierigen Blicken verborgen sind. Schließlich werden CORS-Probleme vereinfacht, da alle Anwendungsanforderungen an denselben Ursprung gesendet werden.

Zurück beim Client wird das <yumcha-portal> -Tag instanziiert und findet den Inhalt dort, wo es vom Server im Antwortdokument platziert wurde, rendert zum geeigneten Zeitpunkt den iframe und weist den Inhalt seinem srcdoc Attribut zu. Wenn wir keine iframe s verwenden (siehe unten), dann wird der Inhalt, der diesem <yumcha-portal> -Tag entspricht, entweder in das Schatten-DOM des benutzerdefinierten Elements eingefügt, wenn wir dieses verwenden, oder direkt in das Dokument eingebettet.

Zu diesem Zeitpunkt haben wir bereits eine teilweise funktionierende Mikro-Frontend-basierte App.

Dies ist nur die Spitze des Eisbergs in Bezug auf interessante Funktionen für den Yumcha-Server. Beispielsweise möchten wir Funktionen hinzufügen, um zu steuern, wie HTTP-Fehlerantworten von den Mikroapp-Servern verarbeitet werden, oder wie mit Mikroapps umgegangen wird, die sehr langsam reagieren – wir wollen nicht ewig warten, um die Seite bereitzustellen, wenn eine Mikroapp dies nicht tut reagieren! Diese und andere Themen werden wir für einen anderen Beitrag aufheben.

Die Transformationslogik von Yumcha macroapp index.html könnte leicht in Form einer serverlosen Lambda-Funktion oder als Middleware für Server-Frameworks wie Express oder Koa implementiert werden.

Stub-basierte Microapp-Steuerung

Um auf die Client-Seite zurückzukommen, gibt es einen weiteren Aspekt bei der Implementierung von Mikroapps, der für Effizienz, verzögertes Laden und störungsfreies Rendering wichtig ist. Wir könnten das Iframe-Tag für jede iframe generieren, entweder mit einem src -Attribut – das eine weitere Netzwerkanfrage stellt – oder mit dem srcdoc -Attribut, das mit dem vom Server für uns ausgefüllten Inhalt ausgefüllt wird. Aber in beiden Fällen wird der Code in diesem iframe sofort gestartet, einschließlich des Ladens aller seiner Skript- und Link-Tags, des Bootstrappings und aller anfänglichen API-Aufrufe und der damit verbundenen Datenverarbeitung – selbst wenn der Benutzer nicht einmal auf die betreffende Mikroapp zugreift.

Unsere Lösung für dieses Problem besteht darin, Mikroapps zunächst als winzige inaktivierte Stubs auf der Seite darzustellen, die dann aktiviert werden können. Die Aktivierung kann entweder dadurch erfolgen, dass die Region der Mikroapp sichtbar wird, indem die zu wenig genutzte IntersectionObserver -API verwendet wird, oder häufiger durch Vorabbenachrichtigungen, die von außen gesendet werden. Natürlich können wir auch festlegen, dass die Mikroapp sofort aktiviert wird.

In jedem Fall, wenn und nur wenn die Mikroapp aktiviert wird, wird der iframe tatsächlich gerendert und sein Code geladen und ausgeführt. In Bezug auf unsere Implementierung mit LitElement und unter der Annahme, dass der Aktivierungsstatus durch eine activated Instanzvariable dargestellt wird, hätten wir so etwas wie:

 render() { if (!this.activated) return html`{this.placeholder}`; else return html` <iframe srcdoc="${this.content}" @load="${this.markLoaded}"></iframe>`; }

Kommunikation zwischen Mikroapps

Obwohl die Mikroapps, aus denen eine Makroapp besteht, per Definition lose gekoppelt sind, müssen sie dennoch in der Lage sein, miteinander zu kommunizieren. Beispielsweise müsste eine Navigationsmikroapp eine Benachrichtigung aussenden, dass eine andere gerade vom Benutzer ausgewählte Mikroapp aktiviert werden sollte, und die zu aktivierende App muss solche Benachrichtigungen empfangen.

Im Einklang mit unserer minimalistischen Denkweise möchten wir vermeiden, viele Nachrichtenübermittlungsmaschinen einzuführen. Stattdessen verwenden wir im Geiste von Webkomponenten DOM-Ereignisse. Wir bieten eine triviale Broadcast-API, die alle Stubs vorab über ein bevorstehendes Ereignis benachrichtigt, auf alle wartet, die eine Aktivierung für diesen Ereignistyp angefordert haben, um aktiviert zu werden, und dann das Ereignis gegen das Dokument sendet, auf dem jede Mikroapp lauschen kann es. Da alle unsere iframe denselben Ursprung haben, können wir vom iframe zur Seite und umgekehrt greifen, um Elemente zu finden, gegen die Ereignisse ausgelöst werden können.

Routing

Heutzutage erwarten wir alle, dass die URL-Leiste in SPAs den Anzeigestatus der Anwendung darstellt, sodass wir sie ausschneiden, einfügen, per E-Mail versenden, Text und einen Link darauf verlinken können, um direkt zu einer Seite innerhalb der App zu springen. In einer Mikro-Frontend-App ist der Anwendungszustand jedoch tatsächlich eine Kombination von Zuständen, einer für jede Mikroapp. Wie sollen wir das darstellen und kontrollieren?

Die Lösung besteht darin, den Zustand jeder Mikroapp in eine einzige zusammengesetzte URL zu codieren und einen kleinen Makroapp-Router zu verwenden, der weiß, wie man diese zusammengesetzte URL zusammenfügt und auseinandernimmt. Leider erfordert dies eine Yumcha-spezifische Logik in jeder Mikroapp: um Nachrichten vom Makroapp-Router zu empfangen und den Zustand der Mikroapp zu aktualisieren und umgekehrt den Makroapp-Router über Änderungen in diesem Zustand zu informieren, damit die zusammengesetzte URL aktualisiert werden kann. Man könnte sich zum Beispiel eine YumchaLocationStrategy für Angular oder ein <YumchaRouter> -Element für React vorstellen.

Eine zusammengesetzte URL, die einen Makroapp-Zustand darstellt. Seine Abfragezeichenfolge wird in zwei separate (doppelt codierte) Abfragezeichenfolgen dekodiert, die dann an die Mikroapps übergeben werden, deren IDs als ihre Schlüssel angegeben sind.

Der Non- iframe Fall

Wie oben erwähnt, hat das Hosten von Mikroapps in iframe einige Nachteile. Es gibt zwei Alternativen: Fügen Sie sie direkt inline in den HTML-Code der Seite ein oder platzieren Sie sie im Schatten-DOM. Beide Alternativen spiegeln die Vor- und Nachteile von iframe etwas wider, aber manchmal auf unterschiedliche Weise.

Beispielsweise müssten einzelne Mikroapp-CSP-Richtlinien irgendwie zusammengeführt werden. Hilfstechnologien wie Screenreader sollten besser funktionieren als mit iframe s, vorausgesetzt, sie unterstützen das Schatten-DOM (was noch nicht alle tun). Es sollte einfach sein, die Service Worker einer Mikroapp mit dem Service Worker-Konzept „Bereich“ zu registrieren, obwohl die App sicherstellen müsste, dass ihr Service Worker unter dem Namen der App und nicht unter "/" registriert ist. Keines der mit iframe verbundenen Layoutprobleme gilt für die Inline- oder Shadow-DOM-Methoden.

Anwendungen, die mit Frameworks wie Angular und React erstellt wurden, sind jedoch wahrscheinlich unzufrieden damit, inline oder im Schatten-DOM zu leben. Für diese werden wir wahrscheinlich iframe s verwenden wollen.

Die Inline- und Shadow-DOM-Methoden unterscheiden sich in Bezug auf CSS. CSS wird sauber im Schatten-DOM gekapselt. Wenn wir aus irgendeinem Grund externes CSS mit dem Schatten-DOM teilen wollten, müssten wir konstruierbare Stylesheets oder ähnliches verwenden. Bei eingebetteten Mikroapps würde das gesamte CSS auf der gesamten Seite geteilt werden.


Letztendlich ist die Implementierung der Logik für Inline- und Shadow-DOM-Mikroapps in <yumcha-portal> unkompliziert. Wir rufen den Inhalt für eine bestimmte Mikroapp von dort ab, wo er von der Serverlogik in die Seite eingefügt wurde, als HTML-Element <template> , klonen ihn und hängen ihn dann an das an, was LitElement renderRoot nennt, was normalerweise das Schatten-DOM des Elements ist, aber kann auch auf das Element selbst ( this ) für den Inline-Fall (Nicht-Schatten-DOM) gesetzt werden.

Aber warte! Der vom Mikroapp-Server bereitgestellte Inhalt ist eine ganze HTML-Seite. Wir können die HTML-Seite für die Mikroapp nicht komplett mit den Tags html , head und body in die Mitte der Seite für die Makroapp einfügen, oder?

Wir lösen dieses Problem, indem wir uns eine Eigenart des template -Tags zunutze machen, in das der vom Mikroapp-Server abgerufene Mikroapp-Inhalt eingebettet ist. Es stellt sich heraus, dass moderne Browser, wenn sie auf ein template -Tag stoßen, es zwar nicht „ausführen“, aber parsen und dabei ungültige Inhalte wie die Tags <html> , <head> und <body> entfernen , während ihr innerer Inhalt erhalten bleibt. Die Tags <script> und <link> im <head> sowie der Inhalt des <body> bleiben also erhalten. Genau das wollen wir, um Mikroapp-Inhalte in unsere Seite einzufügen.

Micro-Frontend-Architektur: Der Teufel steckt im Detail

Mikro-Frontends werden im Webapp-Ökosystem Fuß fassen, wenn (a) sie sich als besserer Architekturansatz erweisen und (b) wir herausfinden können, wie sie so implementiert werden können, dass sie den unzähligen praktischen Anforderungen des heutigen Webs gerecht werden.

Bezüglich der ersten Frage behauptet niemand, dass Micro Frontends die richtige Architektur für alle Anwendungsfälle sind. Insbesondere gäbe es wenig Grund für eine Greenfield-Entwicklung durch ein einzelnes Team, um Mikro-Frontends einzuführen. Die Frage, welche Arten von Apps in welchen Kontexten am meisten von einem Mikro-Frontend-Muster profitieren könnten, überlasse ich anderen Kommentatoren.

In Bezug auf Implementierung und Machbarkeit haben wir gesehen, dass es viele Details zu beachten gibt, darunter einige, die in diesem Artikel nicht einmal erwähnt werden – insbesondere Authentifizierung und Sicherheit, Code-Duplizierung und SEO. Nichtsdestotrotz hoffe ich, dass dieser Artikel einen grundlegenden Implementierungsansatz für Mikro-Frontends darlegt, der mit weiterer Verfeinerung den Anforderungen der realen Welt standhalten kann.

Literaturverzeichnis

  • Mikro-Frontends – Angular Style – Teil 1
  • Mikro-Frontends – Wie man es im Angular-Stil macht – Teil 2
  • Entwicklung einer AngularJS-Anwendung mit Microfrontends
  • Mikro-Frontends
  • UI-Microservices – Umkehrung des Anti-Musters (Micro-Frontends)
  • UI-Microservices – ein Anti-Pattern?
  • Die Seitenerstellung mit Micro-Frontends verfolgt einen Yumcha-ähnlichen Ansatz mit Reverse-Proxys und SSIs, den ich sehr empfehle.
  • Ressourcen für Mikro-Frontends
  • Podium
  • Ich verstehe Micro-Frontends nicht. Dies ist ein ziemlich guter Überblick über Arten von Mikro-Frontend-Architekturen und Anwendungsfällen.
  • Serverlose Micro-Frontends mit Vue.js, AWS Lambda und Hypernova
  • Micro Frontends: Ein toller, umfassender Überblick.