Funktionalitäten für die Integration von Micro Frontends werden heute von verschieden Bibliotheken und Frameworks bereitgestellt, darunter auch von Webpack. Mit Webpack steht Entwicklern eine Open-Source JavaScript-Bibliothek zur Verfügung, die sowohl als Build-Tool aber auch Module-Bundler verwendet werden kann. Module können so zu produktionsfertigen Bundles verarbeitet werden. Webpack zeichnet sich dabei besonders durch die einfache Anwendbarkeit sowie die Unterstützung vieler unterschiedlicher Plugins sowie Loader aus und kann bereits eine sehr große Community aufweisen.
Micro Frontends mit Webpack
Die JavaScript-Bibliothek ermöglicht es zum einen, Module in einen Abhängigkeitsgraphen einzuordnen. Des Weiteren können durch Verwendung diverser Loader bestehende Assets bzw. Dateien transformiert werden, wie z.B. durch die Nutzung eines Style-/CSS-Loaders für das Styling oder Url-Loader für die Transformation von Bildern. Auch Lazy-Loading wird unterstützt, sodass Funktionalitäten bzw. Assets in Realtime nachgeladen werden können, sobald sie tatsächlich benötigt werden. Weiterhin ist die Anwendung von Code-Splitting möglich, d.h. das Aufspalten von Bundles.
In Bezug auf Micro Frontends existieren heute viele Implementierungen die eine hohe Komplexität sowie inkonsistente Performance aufweisen. Es bestehen somit diverse Problemstellungen, die es in diesem Zusammenhang zu lösen gilt. Zum einen muss die Entwicklung ein oder mehrerer Applikationen durch verschiedene Teams ermöglicht werden. Des Weiteren müssen Probleme bei der Aufspaltung von mittleren bis großen Applikationen behandelt werden, sodass jeder resultierende Teil der Anwendung einem separaten Team gehört und unabhängig entwickelt sowie deployed werden kann. Auch das Teilen von Komponenten und Bibliotheken zwischen Komponenten bzw. Applikationen stellt nach wie vor eine Herausforderung dar.
Bereits in der Vergangenheit gab es Ansätze, um mit Webpack eine Micro Frontends-Architektur aufzubauen. Bisher konnten Nutzer Native ECMA-Script-Module, einen Single Build, ein DllPlugin sowie Externals als mögliche Integrationsansätze verwenden. Diese weisen jedoch Schwächen bei der Skalierung von Applikationen bezüglich der Build- und Web-Performance sowie für das Teilen von Abhängigkeiten auf.
Mit Webpack 5 wurden den Entwicklern einige Verbesserungen zugänglich gemacht, so wurde versucht die Developer Experience sowie die Build-Time zu verbessern. Zusätzlich wird seit Ende 2020 ein neuer Integrationsansatz für Micro Frontends zur Verfügung gestellt – das sogenannte Module Federation. Damit liefert Webpack für die Integration von Micro Frontends eine skalierbare Lösung und einen Kompromiss aus guter Build-Performance, guter Web-Performance sowie einer Lösung für das Teilen von Abhängigkeiten.
Module Federation
Unter Module Federation ist eine JavaScript-Architektur von Zack Jackson zu verstehen, die durch die Bereitstellung eines Webpack-Plugins in Webpack 5 verwendet werden kann. Das Plugin ermöglicht das dynamische Importieren von Code anderer Applikationen zur Laufzeit. Hierfür wird für die Module jeweils eine eindeutige Entry-Datei generiert, um von außen auf Komponenten zugreifen zu können. Des Weiteren behandelt dieser Ansatz bisherige Probleme, indem das Teilen von Abhängigkeiten bezüglich des Codes sowie der Bundles ermöglicht wird. Um das Plugin zu verwenden ist es lediglich erforderlich Webpack 5 als Bundler zu verwenden sowie das ModuleFederationPlugin in der entsprechenden webpack.config-Datei einzubinden.
Konzepte von Module Federation
Module Federation basiert auf verschiedenen Low-Level- sowie übergeordneten Konzepten.
Zu den Low-Level-Konzepten zählen die lokalen Module und die Remote-Module. Als lokal werden die normalen Module bezeichnet, die Teil des aktuellen Builds sind. Im Gegensatz dazu sind Remote-Module nicht Teil des aktuellen Builds, sondern werden zur Laufzeit aus Containern geladen. Der Ladevorgang erfolgt dabei asynchron z.B. durch einen import()-Aufruf. Ein Container wird immer über einen Containereintrag erstellt, der einen asynchronen Zugriff ermöglicht. Verschiedene Container können außerdem zirkuläre Abhängigkeiten sowie Verschachtelungen aufweisen. Auch das Überschreiben lokaler Module durch Module eines anderen Containers ist möglich.
Als übergeordnetes Konzept ist zu verstehen, dass jeder Build einem Container entspricht und andere Builds konsumieren kann. Dabei kann auf alle verfügbaren exponierten Module zugegriffen werden.
Die folgenden Ziele werden durch die Verwendung der Konzepte verfolgt:
- Verfügbarkeit und Verwendung jedes von Webpack unterstützen Modultyps
- Paralleles Laden von Chunks zur Verringerung der Kommunikation mit den Servern
- Die Kontrolle wird vom Konsumenten zum Container übergeben, um Überschreibungen nur in eine Richtung zu ermöglichen.
- Umgebungsunabhängigkeit – Die Verwendung soll sowohl im Web als auch mit z.B. Node.js möglich sein.
- Relative und absolute Anfragen in Shared – Der Shared-Scope ist immer verfügbar, auch wenn er nicht verwendet wird.
- Modulanfragen in Shared – Module werden nur zur Verfügung gestellt, wenn sie auch benutzt werden. Die Module werden dann in der benötigten Version bereitgestellt.
ModuleFederationPlugin
Abbildung 2: Einordnung des ModuleFederationPlugins.
Das ModuleFederationPlugin ist ein High-Level-Plugin, das die automatisierte Verwendung verschiedener Low-Level-Plugins, wie z.B. des ContainerPlugins oder ContainerReferencePlugins, ermöglicht. Die Low-Level-Plugins können auch manuell genutzt werden, was jedoch mit einem erheblichen Konfigurationsaufwand verbunden ist.
Für die Konfiguration des ModuleFederationPlugins können verschiedene Properties genutzt werden, die sich auf unterschiedliche Aspekte beziehen. Die folgenden Properties stehen für die Konfiguration bereit:
- Name - Name der Entry-Datei, wenn der Filename nicht gesetzt wird.
- Library – Variable, der das Build-Ergebnis zugewiesen wird.
- Filename – Name der Entry-Datei.
- Shared – Teilen von Modulen zwischen allen beteiligten Containern. Die Aktuell installierte Version des jeweiligen Moduls wird bereitgestellt.
- Exposes – Eine Liste von Modulen/Komponenten, die von Konsumenten des Containers genutzt werden können. Jedes der angegebenen Module erhält einen öffentlichen Namen, dem ein Zeiger auf das Modul der eigenen Codebase zugeordnet wird. Es wird jeder Modultyp, der auch mit Webpack verwendet werden kann, unterstützt.
- Remotes – Eine Liste mit federated Modules, von denen die aufrufende Applikation abhängt, d.h. es handelt sich um ein Objekt mit allen für den aktuellen Build benötigten Containern. Über diese Property ist der Zugriff auf Komponenten anderer Applikationen möglich. Für den Zugriff auf die Module der Container ist jeweils ein Schlüssel festzulegen, dem die Location des Containers als Wert zugewiesen wird. Per Default können auch Script-Externals als Container-Location verwendet werden.
Alle zuvor genannten Properties unterstützen außerdem Advanced Configuration Options wie z.B. Singleton: true für Shared, um nur eine Instanz zur Laufzeit zu erstellen. Zusätzlich können Überschreibungen erlaubt sowie die automatische Generierung von Werten deaktiviert werden. Auch eine andere Verwendung von Bibliotheken und Externals kann man über diese Properties definieren.
Grundlegende Prinzipien von Module Federation
Abbildung 3: Grundlegende Prinzipien von Module Federation.
Grundlegend wird bei Module Federation jeder Teil als separater Build erstellt und als Container kompiliert. Dabei können durch Applikationen oder Container andere Container referenziert werden. Ein Container, der nur Komponenten nach außen freigibt, wird als Remote bezeichnet. Wird dagegen nur auf Komponenten anderer Container zugegriffen, ist von einem Host die Rede. Container die sowohl als Host sowie als Remote auftreten, werden bidirektionale Hosts genannt.
Überblick über Module Federation
In der folgenden Abbildung wird die Systematik von Module Federation, besonders in Bezug auf die Aspekte der Exposed Modules sowie der Shared Modules, im Überblick dargestellt.
Abbildung 4: Überblick über Module Federation.
Die Exposed Modules werden asynchron geladen. Hierbei erfolgt die Anfrage an den bereitstellenden Container bevor die Module im konsumierenden Container genutzt werden. Die Module werden immer erst dann geladen, wenn sie auch tatsächlich verwendet werden. In diesem Kontext kommen außerdem Splitting-Techniken zum Einsatz, um Anfragen sowie Downloads auf ein Minimum zur reduzieren. Ziel ist es dadurch eine gute Web-Performance leisten zu können. Nach dem Ladevorgang können die Exposed Modules vom konsumierenden Container verarbeitet werden.
Die Shared Modules werden zusammen mit den dazugehörigen Versionsinformationen durch alle Container bzw. Apps dem Share Scope hinzugefügt. Dementsprechend ist es anschließend möglich, alle verfügbaren Module, unter Verwendung eines Versionsanforderungs-Checks, aus dem Share Scope, der ein Laden von Duplikaten verhindert, zu konsumieren. Das Hinzufügen von Modulen sowie das Konsumieren erfolgt jeweils asynchron. Durch die Existenz von Instanzen innerhalb des Share Scope ist ein erneuter Download nicht notwendig, dieser erfolgt lediglich beim ersten Verwenden bzw. Konsumieren des jeweiligen Shared Modules.
Konfigurationsbeispiele
Beispielkonfiguration eines Remote-Containers:
Beispielkonfiguration eines Host-Containers:
Import und Nutzung von Federated Modules
Um eine Verwendung der federated Modules zu ermöglichen wird eine Entry-Datei (z.B. remoteEntry.js) durch das ModuleFederationPlugin erstellt. Über diese Datei kann die konsumierende Applikation freigegebene (also exposed) Komponenten nutzen. Insgesamt müssen jedoch mehrere Voraussetzungen geschaffen werden, um die Komponenten verwenden zu können:
Die Inhalte aus der index.ts müssen in eine neue Datei, hier bootstrap.ts, ausgelagert werden.
Die bootstrap.ts muss innerhalb der index.ts importiert werden. Dieses Vorgehen ermöglicht ein asynchrones Laden der bootstrap.ts durch Webpack.
Die Einbindung der Entry-Datei muss im HTML-Code (index.html) der aufrufenden Applikation erfolgen.
Die genutzten Komponenten müssen in einer neuen Datei, hier app.d.ts, als Module deklariert werden.
Durch das Importieren der deklarierten Module in einer beliebigen Datei, kann anschließend auf die freigegebenen Komponenten zugegriffen werden.
Zusammenfassung und Ausblick
Mit Module Federation hat Webpack eine skalierbare Lösung für das Teilen von Code zwischen unabhängigen Applikationen geschaffen. Die separaten Builds verhalten sich durch die Verwendung von Module Federation wie ein Monolith zur Laufzeit und können sich die Module, die für alle Webpack-Ziele verfügbar sind, teilen. Es sind alle von Webpack supporteten Modultypen nutzbar. Die einzelnen Module werden nur dann geladen, wenn sie auch tatsächlich benötigt werden. Das Laden der federated Modules erfolgt dabei asynchron über einen import()-Aufruf.
Zukünftig können bezüglich Module Federation noch einige Veränderungen erwartet werden. Auf der Roadmap ist zum einen ein Dashboard-Service wiederzufinden, der als UI-basiertes Tool umgesetzt werden könnte, um Transparenz zu schaffen sowie eine Visualisierung des föderierten Systems zu ermöglichen. Außerdem soll der Ansatz um Testing-Tools zur Durchführung von Federated Unit Tests erweitert werden. Aber auch noch weitere Ergänzungen des Ansatzes, wie z.B. automatisiertes Error-Handling, Remote-Rollback oder fließende NodeModules, stehen auf der Agenda.
Sie möchten sich mit uns zu dem Thema Microfrontends austauschen? Gerne stellen wir Ihnen auch weiteres Material zum Thema zur Verfügung. Kommentieren Sie einfach unter diesem Beitrag oder kontaktieren uns per Mail.
Dieser Blogbeitrag ist in einem studentischen Projekt in Zusammenarbeit mit der Fachhochschule Münster entstanden.
Du interessierst dich für ein Praktikum, Werksstudententätigkeit oder eine Abschlussarbeit bei uns? Dann schau hier rein und erfahre mehr über die Tätigkeit bei der viadee und entdecke unsere Themen für Abschlussarbeiten.
zurück zur Blogübersicht