Während sich im Backend eine fachliche Trennung von Monolithen in unterschiedliche Module (Modulithen) oder gar Microservices etabliert hat, ist die Frontend-Welt noch immer stark monolithisch geprägt. In diesem Artikel will ich dir zwei Ansätze zeigen, um mehr Flexibilität in deine Frontend-Architektur zu bringen: Module Federation und Web Components.
Monolithen in der Frontend-Welt
In der Angular- und React-Welt ist es üblich, Frontends als Single-Page-Applications aufzubauen. Dabei wird ein inhaltlich leeres HTML-Dokument mithilfe von JavaScript, das im Browser läuft, mit Inhalt befüllt (Client-seitiges Rendering). Außerdem werden alle Komponenten der Webseite in ein großes Bundle transpiliert. Wir haben es also mit einem Monolithen zu tun.
Das HTML-Dokument referenziert einen Einstiegspunkt, von dem aus das Framework und die benötigten Komponenten importiert werden. Wie bei monolithischen Backends hat dies einerseits den Vorteil einer Single Source-of-Truth, und somit auch den Vorteil eines zentralen Release-Prozesses. Andererseits gibt es den Nachteil, dass die ganze Anwendung an einer zentralen Stelle definiert wird. Bei größeren Frontends wären die verschiedenen Teams also stark aneinander stark gekoppelt.
Zwar ist es möglich, Komponenten in Bibliotheken auszulagern, die von der Anwendung importiert werden. Auch können die einzelnen Komponenten durch ein Refactoring besser voneinander getrennt und entkoppelt werden; der Build wird dadurch dennoch nicht dezentralisiert. Dies ist für kleine Frontends in Ordnung. Bei größeren, von mehreren Teams entwickelten Frontends führt dies – wie im Backend – jedoch schnell zu Konflikten.
Microfrontends? Was genau darf man sich darunter vorstellen?
In der Backend-Welt gibt es das Konzept der Microservices. Das heißt, Funktionalitäten werden in kleine Backend-Services aufgeteilt, die sich auf eine bestimmte Funktionalität beschränken. Diese Backend-Services können untereinander zum Beispiel mittels REST oder gRPC kommunizieren, können aber unabhängig voneinander entwickelt und deployt werden.
Der Microservices-Ansatz ermöglicht dem Team eine höhere Autonomität. Wird der Ansatz richtig umgesetzt, können Teams selbständig arbeiten und deployen, ohne sich gegenseitig in die Quere zu kommen. Es stellt sich die Frage, wie dies in der Frontend-Welt umgesetzt werden kann.
Eine Idee dafür nennt sich Microfrontends. Allerdings kann die Realisierung dieser Idee sehr unterschiedlich aussehen. Der Browser als feste Ablaufumgebung setzt hier klare Grenzen, die sich an Web-Standards wie JavaScript, HTTP oder Web-APIs orientieren müssen.
Wir schauen uns im Folgenden zwei Optionen näher an:
- Module Federation
- Web Components
Basis für die Umsetzung ist die Modulauflösung in JavaScript.
Exkurs: Modulauflösung mit JavaScript
Historisch gibt es in JavaScript verschiedene Arten von Modulsystemen. Mit ES6 wurde ein neues Modulsystem eingeführt, das in den meisten modernen Browsern unterstützt wird: ECMAScript Modules (ESM). Daher ist ESM seit einigen Jahren das bevorzugte Modulsystem im JavaScript-Ökosystem.
Mit ESM können Module weitere Module über zwei Arten einbinden; über einen top-level Import, sowie über den Aufruf der import(...)
Funktion:
import { something } from '../some/module.js';
// ^- sofort verfügbar
const { somethingElse } = await import('../another/module.js');
// ^- erst nachdem await fertig ist verfügbar
Der Aufruf ist eine Promise. Es wird gewartet bis das Modul sowie alle Top-Level-Abhängigkeiten des Moduls geladen wurden. Top-Level-Imports werden sofort mit dem Einbinden des eigenen Moduls mitgeladen und sind dadurch ohne Promise verwendbar. Imports über import(...)
werden erst dann ausgeführt, wenn die Funktion tatsächlich ausgeführt wird. Da es sein kann, dass das importierte Modul noch geladen werden muss, wird hier eine Promise zurückgegeben.
Ansatz 1: Framework-spezifische UI-Komponenten
Wie oben erwähnt, habe ich für diesen Blogartikel zwei Strategien genauer unter die Lupe genommen. Erstens: das Einbinden Framework-spezifischer UI-Komponenten in eine Host-Anwendung. Zweitens: das Einbinden Framework-unabhängiger Web Components in eine Host-Anwendung.
Für den Framework-spezifischen Ansatz habe ich mir zwei Frameworks angeschaut: Angular und React.
Da beim Framework-spezifischen Ansatz Host und die eingebundenen Remote-Frontends mit demselben Framework implementiert werden müssen, wurden zwei separate Anwendungen, eine mit React und eine mit Angular, implementiert. Die Anforderung an die Applikationen war es, eine Komponente, bzw. ein Modul von einem anderen Server zu laden und erfolgreich in die Applikation einzubinden, und Daten in der Anwendung zwischen den lokal und remote zur Verfügung stehenden Komponenten zu übertragen.
Module Federation
Für den Framework-spezifischen Ansatz wurde Module Federation mithilfe von @angular-architects/module-federation
(Angular) und @originjs/vite-plugin-federation
(Vite/React) realisiert.
Da alle modernen Browser ESM direkt unterstützen, ist Module Federation auch ohne diese Plugins möglich. Dafür werden die Remote Frontends im Library-Modus gebaut und als ES-Modul direkt konsumiert. Die Plugins automatisieren diesen Schritt für uns und bauen zudem Polyfills für ältere Browser ein.
Module Federation verwendet standardmäßig die import()
Funktion, um das Remote-Kompilat zu laden. Denn import()
kann neben relativen Pfaden (../module.js
) auch URLs (http://localhost:3001/module.js
) aufrufen. Diese URLs können auch auf Server mit einer anderen Root URL zeigen, solange der Remote Server CORS-Header setzt.
Wurde das Frontend nicht im Library-Modus gebaut, können die Remote Module über import()
nicht direkt aufgerufen werden. Das liegt daran, dass Bundler wie Webpack und Rollup (Vite) die JavaScript-Module umverpacken und für Browser optimieren. Statt import("../util/myModule.js")
findet sich bei Webpack im Bundle ein minifizierter Aufruf von __webpack_require__(module_id)
. Die ID ist intern und kann daher nicht „öffentlich“ konsumiert werden.
Die remoteEntry.js
ist hier die Lösung. Sie stellt eine Art Nachschlagewerk bereit. Externe Module importieren die remoteEntry.js
und rufen das interne Modul über den Namen auf.
Hier ein Ausschnitt der nicht minifizierten remoteEntry.js
aus dem Angular-Projekt:
var moduleMap = {
'./RemoteRoute': () => {
return __webpack_require__
.e(44)
.then(() => () => __webpack_require__(2044));
},
};
/// ... der Rest der remoteEntry.js
React
Das React-Frontend verwendet als Build-Tool Vite, ein relativ neues Entwicklungswerkzeug, das viel Wert auf Entwicklererfahrung setzt und zum Beispiel mittels ESM und Überspringen eines Bundle-Steps während der Entwicklung erheblich schnellere Bauzeiten ermöglicht als es beispielweise bei Webpack der Fall ist.
Für Vite gibt es das vite-plugin-federation
-Modul, welches als Plugin in die Vite-Konfiguration eingebunden werden kann. Über das Plugin lassen sich von diesem Kompilat bereitgestellte, sowie von externen Kompilaten konsumierte JavaScript-Module deklarieren:
// vite.config.ts
import federation from '@originjs/vite-plugin-federation';
return defineConfig({
...,
plugins: [
...,
federation({
name: 'my-component-name',
// Der Remote Entrypoint. Ein externes Modul kann diese Datei aufrufen
// und über einen Lookup eine der unter exposes definierten Komponenten
// laden.
filename: 'remoteEntry.js',
// Exposes, wenn dieses Modul Komponenten bereitstellt
exposes: {
'./SomeComponent': './src/components/SomeComponent',
'./AnotherComponent': './src/components/AnotherComponent',
},
// Remotes, wenn dieses Modul externe Komponenten konsumiert,
// mit einem Link zum Remote-Entrypoint.
remotes: {
someRemoteEntry: 'http://localhost:5001/assets/remoteEntry.js',
},
// Hier definierte Dependencies werden im Browser effektiv
// nur einmal geladen. Es muss also nicht pro Remote Frontend
// eine eigene React-Version geladen werden.
shared: ['react', 'react-dom'],
}),
],
});
Bei einem Build sorgt das Plugin dann dafür, dass die remoteEntry.js
-Datei erzeugt wird. Die Komponenten können dann über den im remotes
-Objekt definierten Namen eingebunden werden:
const RemoteComponent = React.lazy(() => import('someRemoteEntry/SomeRemoteComponent'));
return (
<Suspense fallback={<Loading />}>
<RemoteComponent />
</Suspense>
);
Das Plugin schreibt den Import während des Builds so um, dass die remoteEntry.js
vom Remote Server importiert, und darüber die gewünschte Komponente gefunden wird.
Hier ist es wichtig, anzumerken, dass ein Top-Level-Import, also
import RemoteComponent from 'someRemoteEntry/SomeRemoteComponent';
nicht möglich ist, da es sich hier um eine synchrone Operation handeln würde. Das heißt, das Remote-Modul muss zuerst von einem anderen Server geladen werden.
Die import(...)
-Funktion gibt das Modul als Promise zurück, welche gelöst wird, wenn das Modul, sowie alle Top-Level-Importe dieses Moduls aufgelöst wurden.
Angular
Da Angular-Projekte standardmäßig Webpack als Build-Tool verwenden, habe ich dieses Tool mit dem Plugin @angular-architects/module-federation-plugin
für das Aufrufen und Bereitstellen von Remote-Modulen ausprobiert.
Ähnlich wie beim vite-plugin-federation
kann ein Modul über die webpack.config.js
über remotes
und exposes
Remote-Module konsumieren, bzw. Module für andere Remote-Module bereitstellen.
module.exports = withModuleFederationPlugin({
// Wird von diesem Kompilat konsumiert
remotes: {
'angular-remote': 'http://localhost:3001/remoteEntry.js',
},
name: 'module-name',
// Liste der JS-Module, welche von diesem Kompilat bereitgestellt werden
exposes: {
'./ExposedRoute': './packages/angular-remote/src/app/remote-page.module.ts',
},
shared: {
...shareAll({
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
}),
},
});
Auch ein Einbinden eines Remote-Modules ist unter Angular sehr ähnlich zum React+Vite- Beispiel:
const routes: Routes = [
{
path: '',
component: IndexRoute,
pathMatch: 'full',
},
{
path: 'remoteRoute',
// Das Remote Modul wird über eine Promise geladen. Dabei handelt
// es sich um ein Angular-Modul, welches ein
// RouterModule.forChildren(...) definiert.
loadChildren: () =>
loadRemoteModule({
type: 'module',
remoteEntry: 'http://localhost:3001/remoteEntry.js',
exposedModule: './RemoteRoute',
}).then((m) => m.RemotePageModule),
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
Auch wenn für Angular unüblich, können hier Remote-Komponenten über das asynchrone Austauschen einer ViewContainerRef
ohne einen Router eingebunden werden (vgl. ViewContainerRef Dokumentation).
Ansatz 2: Framework-unabhängiger Ansatz mit Web Components
Für diesen Ansatz wurde eine Host-Anwendung mit Angular implementiert. In diese Anwendung wurden mehrere Web Components eingebunden, implementiert mit Vue, React und „Vanilla TS“. Diese Web Components werden dabei von einem anderen Server bereitgestellt.
Web Components
Web Components sind eine in allen gängigen Browsern unterstützte API, mit der neue HTML-Elemente definiert werden können. Diese Element können, ähnlich wie bei Vue und Angular, über Props Daten annehmen und über Events zurückgeben.
Eine Web Component wird erstellt, indem wir die Klasse HTMLElement
erweitern.
Hier ein Beispiel einer sehr primitiven Web Component. Diese nimmt einen Namen an und rendert den Namen in einen Gruß:
class ExampleGreeter extends HTMLElement {
#name: string;
#root!: ShadowRoot;
#htmlElement: HTMLDivElement;
get name() {
return this.#name;
}
// Setter wird aufgerufen wenn das name-Prop modifiziert wird.
// Der Setter ruft die Render-Funktion auf
set name(name: string) {
this.#name = name;
this.render();
}
render() {
if (!this.#htmlElement) {
return;
}
this.#htmlElement.innerHTML = `Hello, <b>${this.#name}</b>!`;
}
// Code wird aufgerufen wenn das Element aufgebaut wird
connectedCallback() {
this.#root = this.attachShadow({ mode: 'open' });
this.#htmlElement = document.createElement('div');
this.render();
}
// Cleanup Code. Wird aufgerufen wenn das Element entfernt wird
disconnectedCallback() {}
// Damit eine Web Component im HTML verwendet werden darf muss sie
// zuerst angemeldet werden.
// Danach kann die Web Component wie gefolgt verwendet werden:
// <example-greeter name="Lukas"></example-greeter>
static register() {
window.customElements.define('example-greeter', ExampleGreeter);
}
}
Eine wichtige Anmerkung zu Web Components ist, dass diese isoliert zum Rest der Webseite gestylt werden. Der Inhalt der Web Component läuft unter einer Shadow DOM. Die Shadow DOM stellt ein eigenes Dokument bereit. Styles können auf dieses separate Dokument über einen in der Web Component erstellten <link rel="stylesheet" ...>
-Knoten oder durch Inline Styles angewandt werden. Diese Styles werden nur auf das eigene Dokument angewandt, die Styles werden also nicht an darunterliegende Web Components oder den Eltern-Knoten übergeben.
Definition der Remote Frontends
Die Remote Frontends wurden so definiert, dass im Kompilat eine /webcomponent.js
generiert wird. Die Datei definiert ein ES-Modul, welches als default
-Export eine Funktion bereitstellt, über welche die Web Component im Host angemeldet werden kann.
Diese Struktur ist für alle Remote Frontends identisch, sodass ein Remote Frontend aus dem Host über
const module = await import(`http://localhost:${PORT}/webcomponent.js`);
module.default('component-name-to-use');
geladen und im Host registriert wird.
Danach kann im Host eine Komponente mit dem angegebenen Namen (hier component-name-to-use
) erstellt werden.
Für die Remote Frontends wurde Vite als Build-Tool verwendet. Die Standard-Konfiguration bei Vite baut eine Single Page Application. Diese Konfiguration ist so aufgebaut, dass das Resultat auf einem Server gehostet werden kann und eine Webseite bereitstellt.
Es gibt aber auch den Library Mode. Hier wird eine Code-Datei als Einstiegspunkt referenziert.
Statt einer ganzen Webseite wird im Libary Mode dieser Einstiegspunkt, sowie alle Abhängigkeiten, in ein Bundle gepackt. Dieses Bundle lässt sich dann als ES-Modul im Host einbinden.
// Die vite Config, welche für die Remote Frontends verwendet wird.
// Hier als Beispiel die vite-webcomponent.config.ts des Vue-Microfrontend
import { resolve } from 'node:path';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
define: {
'process.env': {},
},
base: 'http://localhost:5002/',
build: {
lib: {
entry: resolve(__dirname, 'src/webcomponent.ts'),
formats: ['es'],
fileName: 'webcomponent',
},
target: 'modules',
sourcemap: true,
},
});
Die resultierende webcomponent.js
beinhaltet den ganzen Code, der für diese Web Component benötigt wird. Das beinhaltet die Laufzeit (bpsw. Vue), sämtliche Komponenten, die in der Anwendung definiert und eingebunden wurden, sowie die Logik zum Verpacken und Registrieren der Anwendung als Web Component.
Das Anbinden der Frontend-Frameworks an die Web Component hängt vom jeweiligen Framework ab. Vue 3 hat beispielsweise die defineCustomElement
-Funktion, die eine Vue Singe File Component in eine Web Component umwandelt.
Die genauen Implementationen können dem Github Repository aus den webcomponent.ts(x)
-Dateien entnommen werden.
Einbindung und Kommunikation zwischen Host und Microfrontend-Komponenten
In der Host-Anwendung wurde eine Angular-Komponente definiert, die das Laden und Einbinden einer remote Web Component ermöglicht. Kurz gesagt lädt die Komponente über import(...)
das Remote-Modul, ruft dort den Default Export auf und registriert somit die Web Component. Daraufhin wird eine Instanz der Web Component erstellt und der Zustand zwischen der Host-Anwendung und der Web Component verknüpft.
Die Web Components sind so aufgebaut, dass die Host-Anwendung einen Zustand (hier Testweise count
) übergeben und über Updates auf diesen Zustand informiert werden kann. Für die Information ist die Anmeldung an ein von der Web Component bereitgestelltes CustomEvent
notwendig.
Für das Beispiel wurde ein sehr einfacher Zustand, ein einzelner Counter, verwendet. Es ist jedoch genauso möglich, den Zustand komplexer aufzubauen oder sogar eine Message Queue zu implementieren.
Beide Ansätze im Vergleich
Im Grunde können beide Herangehensweisen verwendet werden, um Microfrontends zu realisieren. Der Framework-spezifische genauso wie die Framework-unabhängige. Doch es gibt Unterschiede:
Der Framework-spezifische Ansatz hat den Vorteil, dass Frameworks wie React, Vue oder Angular nur einmal eingebunden werden. Somit bleibt das generierte Bundle vergleichsweise klein. Auch bleibt man dadurch im Daten-Konzept des jeweiligen Frameworks (bspw. :props
und @events
mit Vue) und kann Komplexität sparen, die durch das Verpacken der Anwendung in eine Web Component entstünde.
Der Framework-unabhängige Ansatz mit Web Components hat den Vorteil, dass man sich nicht auf ein bestimmtes Framework beschränkt. Für die Kommunikation zwischen Host und Microfrontends wird eine API verwendet, die im Living Standard für HTML definiert ist.
Man darf davon ausgehen, dass Web Components als Standard stabil sind. Falls verwendete Frameworks in der Zukunft nicht mehr unterstützt werden, wäre der Austausch vergleichsweise einfach.
Ein Nachteil dieses Ansatzes ist, dass jedes Microfrontend das verwendete Framework separat einbindet. Dies hat jedoch auch den Vorteil, dass es nicht zu Versionskonflikten kommen kann.
Bei der Implementation des Framework-spezifischen Ansatzes bekam ich kryptische Fehler beim Anbinden eines Microfrontends. Nach längerem Debugging entdeckte ich die Ursache: Der Fehler kam aufgrund unterschiedlicher Versionen zwischen Host und Microfrontend zustande.
Eine alternative Lösung ist es, in den Web Components und im Host die Frameworks als externe Abhängigkeiten einzubinden.
Das verhindert einerseits, dass die verwendeten Funktionen des react
-Moduls in die webcomponent.js
kopiert werden und sorgt gleichzeitig dafür, dass stattdessen das react
-Modul auch weiterhin als Top-Level-Import im Kompilat eingebunden wird. Gekoppelt mit einer Import-Map können so mehrere react
-Imports auf denselben Import abgebildet werden, wodurch das zu ladende JS-Bundle kleiner wird.
Beide Ansätze teilen die grundlegenden Probleme mit Microfrontends:
- Builds sind schwerer reproduzierbar, weil es keine Single Source-of-Truth gibt. Es müssten also alle Komponenten in einer bestimmten Version deployt werden, um einen Build reproduzierbar zu testen.
- Weil es zum Übersetzungzeitpunkt keine Informationen über die Microfrontends gibt, gibt es keine Typsicherheit auf der Schnittstelle
Welche Alternativen gibt es?
Zwei alternative Ansätze sollten meines Erachtens nicht unerwähnt bleiben. Mit ihnen lassen sich einige der genannten Probleme lösen:
1. Komponenten-Bibliothek
Statt die einzelnen Microfrontends dezentral zu deployen und von einem zentralen Host zur Laufzeit die einzelnen Microfrontends einzubinden, könnten die Microfrontends weiterhin dezentral gebaut, aber zentral zur „Kompilierzeit“ eingebunden werden – oder anders gesagt: Die Microfrontends stellen jeweils Komponenten über ein NPM-Modul bereit, das vom Host konsumiert wird.
Dadurch gewinnen wir Typ-Sicherheit und haben Dank der Single Source-of-Truth einen reproduzierbaren und somit deutlich testbareren Build. Die „Microfrontends“ können weiterhin isoliert voneinander entwickelt und getestet werden. Die einzige Konfliktstelle zwischen den verschiedenen Implementationsteams stellt die Host-Anwendung dar. Hier müssen die einzelnen Module in der package.json
eingebunden und bei Bedarf aktualisiert werden.
Der Nachteil: Die einzelnen Microfrontends verlieren an Unabhängigkeit: Sie sind nicht mehr unabhängig deploybar, die Deploymentzeitpunkte sind zeitlich aneinander gekoppelt.
2. Multipage-Application (MPA) mit nginx
Sind die einzelnen Microfrontends vollständig voneinander isoliert, also nicht verschachtelt, und bilden nur Routen ab, könnte die Verwendung einer Multi Page Application (MPA) mit nginx in Erwägung gezogen werden.
Die ganzen Frontends werden separat als vollständige Frontends deployt. Die Router der jeweiligen Microfrontends sind so konfiguriert, dass die Wurzel nicht /
ist, sondern ein dem Frontend zugeordneter Pfad wie /account
oder /checkout
. Somit ist beispielsweise ein Microfrontend zuständig für die /account/**
-Routen, ein anderes für die /checkout/**
-Routen.
Die Frontends werden auf verschiedenen Server unabhängig deployed. Es wird als „Host“ ein Nginx-Deployment erstellt, das die einzelnen Pfade auf die dazugehörigen Frontend-Deployments abbildet. Bleibt der Nutzer auf demselben Microfrontend, so wird der Frontend-interne Router verwendet. Wechselt man zwischen den Microfrontend, dann wird die ganze Seite neu geladen. Dies führt zu etwas längeren Ladezeiten, da das HTML-Dokument, sowie die ganzen Styles und Skripte für das neue Microfrontend geladen und ausgeführt werden müssen. Dieses Problem kann jedoch durch das Setzen eines <link rel="preload" .../>
in der aufrufenden Seite eingedämmt werden.
Ein weiteres Problem ist geteilter Zustand. Möchte man Daten zwischen den Frontends austauschen, kann nicht der Frontend-interne Store verwendet werden.
Ein Lösungsansatz ist es, den geteilten Zustand in die sessionStorage
zu schreiben. Hierfür muss der Zustand als String abbildbar sein, bspw. als JSON String. Während der localStorage
einen permanenten Key-Value-Store für eine Webseite darstellt, ist der sessionStorage
nur für die aktuelle Session aktiv. Wenn eine Webseite auf mehreren Tabs geöffnet wird, wird für jeden Tab eine neue Session erstellt. Wird ein Tab, bzw. der Browser, geschlossen, dann erlischt die Session (vgl. MDN Docs).
Dank der nginx Proxy haben alle Frontends für den Browser denselben Origin. Dadurch verfällt die sessionStorage
nicht bei einem Seitenwechsel auf ein anderes Microfrontend.
Mein persönliches Fazit
Microfrontends bieten die Möglichkeit, riesige und hochkomplexe Frontends sinnvoll aufzuteilen und voneinander unabhängig zu machen und somit auch unabhängig daran zu entwickeln.
Du solltest dir jedoch gut überlegen, ob die Einbußen in der Entwicklererfahrung, Testbarkeit, Typsicherheit und Reproduzierbarkeit von Builds sich für die dadurch gewonnene Unabhängigkeit der einzelnen Microfrontends lohnen.
Für riesige Anwendungen kann dies tatsächlich der Fall sein. Doch für die meisten Anwendungen sind Investitionen in bessere Architektur, einen besseren Schnitt der Komponenten und klare Coding-Konventionen eine effizientere Art, um Konflikte zwischen Entwicklungsteams an einem Frontend zu reduzieren.
Wenn ich zwischen einer der beiden gezeigten Strategien für Microfrontends wählen müsste, würde ich die framework-unabhängige Herangehensweise empfehlen.
Aufgrund von drei Gründen:
- Höhere Flexibilität (weil unabhängig von Framework und Version)
- Einfachere Austauschbarkeit durch die Verwendung von Web Components
- Unterstützung und Unabhängigkeit: Wie erwähnt sind Web Components Teil der HTML-Spezifikation, werden von allen gängigen Browsern unterstützt und bieten somit eine Framework-unabhängige Möglichkeit für die Kommunikation zwischen Microfrontends.
Weiterführende Links
- Git-Repo framework-abhängiger Implementation mit Angular: https://github.com/opitzconsulting/modulefederation-microfrontends-angular
- Git-Repo framework-abhängiger Implementation mit React: https://github.com/opitzconsulting/modulefederation-microfrontends-react
- Git-Repo framework-unabhängiger Implementationen: https://github.com/opitzconsulting/webcomponent-microfrontends
- MDN Docs zu Web Components: https://developer.mozilla.org/en-US/docs/Web/API/Web_components
- HTML Spezifikation zu Custom Elements: https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements
- Bibliothek Microfrontends Angular: https://github.com/angular-architects/module-federation-plugin
- Bibliothek Microfrontends React / Vite: https://github.com/originjs/vite-plugin-federation