Bei der Umsetzung von Microfrontends musste man bis dato ein wenig in die Trickkiste greifen. Ein Grund dafür war bisher, dass aktuelle Build-Tools und Frameworks dieses Konzept nicht kennen. Das sich derzeit in der Betaphase befindliche webpack 5 wird hier jedoch einen Kurswechsel einleiten.
Es erlaubt einen Ansatz namens Module Federation zum Referenzieren von Programmteilen, die zum Kompilierungszeitpunkt noch nicht bekannt sind. Dabei kann es sich auch um eigenständig kompilierte Microfrontends handeln. Außerdem können die einzelnen Programmteile untereinander Bibliotheken teilen, sodass die einzelnen Bundles keine Duplikate beinhalten.
In diesem Artikel zeige ich anhand eines einfachen Beispiels, wie sich Module Federation [2] nutzen lässt. Der Quellcode befindet sich hier.
Beispiel
Das hier verwendete Beispiel besteht aus einer Shell, die in der Lage ist, einzelne separat bereitgestellte Microfrontends bei Bedarf zu laden (Abb. 1).
Die Shell wird hier durch die schwarze Navigationsleiste repräsentiert. Das Microfrontend durch den darunter dargestellten eingerahmten Bereich. Außerdem lässt sich das Microfrontend auch ohne Shell starten (Abb. 2).
Das ist notwendig, um ein separates Entwickeln und Testen zu ermöglichen. Außerdem kann es für schwächere Clients wie mobile Endgeräte von Vorteil sein, nur den benötigten Programmteil laden zu müssen.
Funktionsweise
In der Vergangenheit war die Umsetzung von Szenarien wie das hier gezeigte schwierig, zumal Werkzeuge wie webpack davon ausgehen, dass der gesamte Programmcode beim Kompilieren vorliegt. Lazy Loading ist zwar möglich, aber nur von Bereichen, die beim Kompilieren abgespaltet wurden.
Gerade bei Microfrontend-Architekturen möchte man die einzelnen Programmteile jedoch separat kompilieren und bereitstellen. Daneben ist ein gegenseitiges Referenzieren über den jeweiligen URL notwendig. Dazu wären Konstrukte wie dieses hier wünschenswert:
import('http://other-microfrontend');
Da das aus den genannten Gründen nicht möglich ist, musste man auf Ansätze, wie Externals [4] und manuellem Skript-Loading ausweichen. Mit der Module Federation in webpack 5 wird sich das zum Glück ändern.
Die Idee dahinter ist einfach: Ein sogenannter Host referenziert einen Remote über einen konfigurierten Namen (Abb. 3). Auf was dieser Name verweist, ist zum Kompilierungszeitpunkt nicht bekannt.
&nsbp;
Dieser Verweis wird erst zur Laufzeit aufgelöst, indem ein sogenannter Remote Entrypoint geladen wird. Dabei handelt es sich um ein minimales Skript, das den tatsächlichen externen URL für solch einen konfigurierten Namen bereitstellt.
Implementierung des Hosts
Beim Host handelt es sich um eine JavaScript-Anwendung, die einen Remote bei Bedarf lädt. Dazu kommt ein dynamischer Import zum Einsatz. Der Host in Listing 1 lädt auf diese Weise die Komponente hinter mfe1/component.
const rxjs = await import('rxjs'); const container = document.getElementById('container'); const flightsLink = document.getElementById('flights'); rxjs.fromEvent(flightsLink, 'click').subscribe(async _ => { const module = await import('mfe1/component'); const elm = document.createElement(module.elementName); [...] container.appendChild(elm); });
Normalerweise würde webpack diesen Verweis beim Kompilieren berücksichtigen und ein eigenes Bundle dafür abspalten. Um das zu verhindern, kommt das ModuleFederationPlugin zum Einsatz (Listing 2).
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); [...] plugins: [ new ModuleFederationPlugin({ name: "shell", library: { type: "var", name: "shell" }, remotes: { mfe1: "mfe1" }, shared: ["rxjs"] }) ]
Mit seiner Hilfe wird der Remote mfe1 (Microfrontend 1) definiert. Dazu stellt die gezeigte Konfiguration ein Mapping zur Verfügung, das den anwendungsinternen Namen mfe1 auf denselben offiziellen Namen abbildet. Jeder Import, der sich nun auf mfe1 bezieht, wird von webpack nicht in die zur Compile Time generierten Bundles aufgenommen.
Bibliotheken, die sich der Host mit den Remotes teilen soll, sind unter shared einzutragen. Im gezeigten Fall handelt es sich dabei um rxjs. Das bedeutet, dass die gesamte Anwendung diese Bibliothek nur ein einziges Mal laden muss. Ohne diese Angabe würde rxjs sowohl in den Bundles des Hosts als auch in jenen aller Remotes landen.
Damit das problemlos funktioniert, müssen sich Host und Remote auf eine gemeinsame Version einigen. Außerdem muss der Host solche Bibliotheken, wie in Listing 1 gezeigt, über einen dynamischen Import laden. Statische Importe wie import * as rxjs from ‘rxjs’; unterstützte das Plug-in nicht, als der vorliegende Text verfasst wurde.
Implementierung des Remotes
Der Remote ist ebenfalls eine eigenständige Anwendung. Im hier betrachteten Fall basiert er auf Web Components (Listing 3).
class Microfrontend1 extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); } async connectedCallback() { this.shadowRoot.innerHTML = `[…]`; } } const elementName = 'microfrontend-one'; customElements.define(elementName, Microfrontend1); export { elementName };
Anstatt Web Components können aber auch beliebige JavaScript-Konstrukte oder Komponenten, die auf Frameworks basieren, zum Einsatz kommen. Die Frameworks lassen sich in diesem Fall auf die gezeigte Weise zwischen den Remotes und dem Host teilen.
Die webpack-Konfiguration des Remotes, die ebenfalls das ModuleFederationPlugin nutzt, exportiert diese Komponente mit der Eigenschaft exposes unter dem Namen component (Listing 4).
output: { publicPath: "http://localhost:3000/", [...] }, [...] plugins: [ new ModuleFederationPlugin({ name: "mfe1", library: { type: "var", name: "mfe1" }, filename: "remoteEntry.js", exposes: { component: "./mfe1/component" }, shared: ["rxjs"] }) ]
Dazu verweist der Name component auf die entsprechende Datei. Außerdem legt diese Konfiguration für den vorliegenden Remote den Namen mfe1 fest. Zum Zugriff auf den Remote nutzt der Host einen Pfad, der sich aus den beiden konfigurierten Namen, mfe1 und component, zusammensetzt. Somit ergibt sich die oben gezeigte Anweisung import(‘mfe1/component’).
Allerdings muss der Host dazu wissen, unter welchem URL er mfe1 findet. Der nächste Abschnitt gibt darüber Aufschluss.
Host mit Remote verbinden
Um dem Host die Möglichkeit zu geben, den Namen mfe1 aufzulösen, muss dieser einen Remote Entrypoint laden. Dabei handelt es sich um ein Skript, dass das ModuleFederationPlugin beim Kompilieren des Remote generiert.
Der Name dieses Skripts findet sich in der Eigenschaft filename (Listing 4). Der URL des Microfrontend wird aus dem unter output festgelegten publicPath entnommen. Das bedeutet, dass der URL des Remote bei dessen Kompilierung bereits bekannt sein muss. Das ist zwar nicht schön – in vielen Fällen kann man damit aber leben.
Nun ist dieses Skript nur noch in den Host einzubinden:
<script src="http://localhost:3000/remoteEntry.js"></script>
Zur Laufzeit lässt sich nun beobachten, dass die Anweisung import(‘mfe1/component’); im Host dazu führt, dass er den Remote von seiner eigenen URL (hier localhost:3000) lädt (Abb. 4).
Fazit und Ausblick
Die in webpack 5 integrierte Module Federation füllt eine große Lücke für Microfrentends. Endlich lassen sich separat kompilierte und bereitgestellte Programmteile nachladen und bereits geladene Bibliotheken wiederverwenden.
Die an der Entwicklung solcher Anwendungen beteiligten Teams müssen jedoch manuell sicherstellen, dass die einzelnen Teile auch zusammenspielen. Das bedeutet auch, dass man Verträge zwischen den einzelnen Microfrontends definieren und einhalten, aber auch, dass man sich bei den geteilten Bibliotheken auf jeweils eine Version einigen muss.
Die Notwendigkeit für dynamische Imports führt zu ungewohnten Codestrecken, zumal das Framework der Wahl in der Regel statisch importiert wird. Bei Angular, das den Programmcode mit einem Compiler transformiert, erweist sich dieser Umstand als Showstopper. Bis zur finalen Version ist jedoch noch ein wenig Zeit, um das zu ändern.
Als der vorliegende Artikel geschrieben wurde, war webpack 5 noch in der Betaversion und somit nicht für den Produktionseinsatz reif. Das bedeutet, dass man vorerst noch auf die eingangs erwähnten Tricks setzen muss. Mittel- bis langfristig dürfte die Module Federation jedoch die Standardlösung für Microfrontends im JavaScript-Umfeld werden.
Microfrondends auf der BASTA!
● Die Microfrontend-Revolution: Webpack 5 Module Federation mit Angular nutzen