BLOG

Micro-Frontends mit modernem Angular

Standalone-Komponenten und esbuild

Angular verändert sich

Standalone-Komponenten machen das Framework schlanker und erleichtern die Bereitstellung von Web Components über Angular Elements. Die neue esbuild-Integration, die ab Version 17 standardmäßig aktiviert ist, bietet eine deutlich bessere Build-Performance im Vergleich zum herkömmlichen, auf webpack basierenden Build-Prozess. In unseren Tests konnten wir die Build-Zeiten um den 3 bis 4 Faktoren beschleunigen. Angular 17 bringt zudem eine überarbeitete Unterstützung für serverseitiges Rendering mit sich. Zusammen mit den aktuellen Entwicklungen rund um Hydration und dem geplanten Deferred Loading kann dies unter anderem die Laufzeitleistung erheblich verbessern.

(Siehe Branches nf-standalone-solution und nf-standalone-router-config)

Der Mehrwert unserer Expertentipps

Bei TechTalk sehen wir Software als Werkzeug, das in den Händen von Menschen und Teams das Potenzial hat, wahre Veränderungen zu bewirken.

Wir bringen nicht nur mehr als drei Jahrzehnte praktische Erfahrung in der Softwareentwicklung mit, sondern zeichnen uns auch durch unsere tiefe Verwurzelung in agilen Methoden und unsere ganzheitliche Projektumsetzung aus. Dabei verstehen wir uns nicht nur als Berater, sondern als Partner auf Augenhöhe für unsere Kund:innen, mit denen wir echte Wirkung in der Zusammenarbeit erzielen. Auf dieser Grundlage haben wir unsere Tipps zusammengestellt.

Inhaltsverzeichnis

Module Federation als Game Changer

Module Federation – seit Version 5 Bestandteil von webpack – gilt häufig als Game Changer für Micro-Frontends. Sie ermöglicht es, separat kompilierte und veröffentlichte Anwendungskomponenten bei Bedarf zu laden:

Module Federation as a Game Changer

Eine Shell-Anwendung (offiziell Host genannt) definiert URL-Segmente, die auf die Micro-Frontends (offiziell Remotes genannt) verweisen. Die Micro-Frontends stellen Programmteile wie Komponenten oder Angular-Module bereit. Die Shell kann diese Programmteile nun zur Laufzeit laden.

Darüber hinaus ermöglicht Module Federation auch das Teilen von Abhängigkeiten zur Laufzeit. Angular muss dadurch nur einmal geladen werden – selbst wenn mehrere separat kompilierte und veröffentlichte Micro-Frontends darauf basieren.

Angular CLI: esbuild ersetzt webpack –
und damit auch Module Federation!

Abgesehen von einigen Vorab-Versionen verwendet die Angular CLI seit ihren Anfängen webpack für den Build-Prozess. Das aus gutem Grund, denn webpack war lange Zeit der De-facto-Standard in diesem Bereich. Inzwischen gibt es jedoch einige Alternativen, die eine deutlich bessere Build-Performance bieten. Möglich wird das durch die Nutzung nativer Programmiersprachen statt JavaScript sowie durch die von Grund auf unterstützte Parallelisierung.

Die beliebteste dieser Alternativen ist esbuild. Seit Angular 16 liefert die CLI eine Entwickler-Vorschau eines esbuild-Builders mit. Ab Version 17 soll dieser standardmäßig aktiviert sein. Dadurch laufen sowohl ng serve als auch ng build spürbar schneller.

Der Umstieg auf esbuild bringt jedoch eine Herausforderung für Micro-Frontends mit sich: Die beliebte Module Federation basiert auf webpack und steht für esbuild nicht zur Verfügung. In den nächsten Abschnitten wird eine Lösung dafür vorgestellt.

Native Federation mit esbuild

Um das bewährte Denkmodell der Module Federation unabhängig von webpack nutzen zu können, wurde das Projekt Native Federation ins Leben gerufen. Es bietet dieselben Möglichkeiten und Konfigurationsoptionen wie Module Federation, funktioniert jedoch mit allen gängigen Build-Tools. Zudem setzt es auf browser-native Technologien wie EcmaScript-Module und Import Maps. Dadurch soll eine langfristige Unterstützung durch Browser sichergestellt und alternative Implementierungen ermöglicht werden.

Native Federation wird im Build-Prozess vor und nach dem eigentlichen Bundler aufgerufen. Daher spielt es keine Rolle, welcher Bundler tatsächlich verwendet wird:

Native Federation

Da Native Federation ebenfalls einige Bundles erzeugen muss, übergibt es diese Aufgabe an den gewählten Bundler. Die jeweiligen Bundler sind über austauschbare Adapter angebunden.

Das folgende Bild zeigt ein Beispiel, das mit Angular, esbuild und Native Federation umgesetzt wurde:

Beispiel Aufbau mit Angular, esbuild und Native Federation

Die hier dargestellte Shell hat mit Hilfe von Native Federation ein separat entwickeltes und bereitgestelltes Micro-Frontend in ihren Workspace geladen.

Obwohl sowohl die Shell als auch das Micro-Frontend auf Angular basieren, wurde Angular durch Native Federation nur einmal geladen. Um das zu ermöglichen, platziert Native Federation – ganz im Sinne der Module Federation – die Remotes und die zu teilenden Bibliotheken in eigenen Bundles. Hierfür werden standardkonforme EcmaScript-Bundles verwendet, die theoretisch auch von anderen Tools erstellt werden könnten. Die Informationen zu diesen Bundles werden in Metadaten-Dateien hinterlegt:

Native Federation Laufdauer

Diese Metadaten-Dateien bilden die Grundlage für eine standardkonforme Import Map, die dem Browser mitteilt, von wo welche Bundles zu laden sind.

Erste Schritte mit Angular oder
direkt abtauchen ins Detail?

Ob Sie neu einsteigen oder Ihr Know-how auf das nächste Level bringen wollen –
unsere zwei Trainings liefern genau das, was Sie gerade brauchen.

Jetzt das passende Training finden

Native Federation:
Ein Micro-Frontend einrichten

Für die Verwendung mit Angular und der CLI bietet Native Federation ein ng-add-Schematic. Mit folgendem Befehl wird Native Federation zum Angular-Projekt mfe1 hinzugefügt und als Remote konfiguriert, das als Micro-Frontend fungiert:
				
					ng add @angular-architects/native-federation --project mfe1 --port 4201 --type remote

				
			
Das ng-add-Schematic erstellt außerdem eine federation.config.js, die das Verhalten von Native Federation steuert:
				
					const { withNativeFederation, shareAll } = require('@angular-architects/native-federation/config');

module.exports = withNativeFederation({

  name: 'mfe1',

  exposes: {
    './Component': './projects/mfe1/src/app/app.component.ts',
  },

  shared: {
    ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
  },

  skip: [
    'rxjs/ajax',
    'rxjs/fetch',
    'rxjs/testing',
    'rxjs/webSocket',
    // Add further packages you don't need at runtime
  ]

});
				
			

Die Eigenschaft name definiert einen eindeutigen und unverwechselbaren Namen für das Remote. Im Bereich exposes wird festgelegt, welche Dateien das Remote dem Host zur Verfügung stellen soll. Diese Dateien werden zwar gemeinsam mit dem Remote gebaut und deployed, können aber zur Laufzeit vom Host geladen werden. Da der Host keine vollständigen Dateipfade benötigt, werden die exponierten Dateien über exposes auf kürzere Namen gemappt.

Im gezeigten Fall stellt das Remote zur Vereinfachung nur seine AppComponent bereit. Es könnten jedoch auch beliebige andere Komponenten veröffentlicht werden – zum Beispiel Lazy-Routing-Konfigurationen, die mehrere Komponenten eines Features referenzieren.

Im Abschnitt shared listet die Konfiguration alle Abhängigkeiten auf, die das Remote mit anderen Remotes und dem Host teilen möchte. Um nicht jede benötigte npm-Abhängigkeit manuell anzugeben, wird die Hilfsfunktion shareAll verwendet. Sie schließt alle Pakete ein, die in der package.json unter dependencies aufgeführt sind.

Pakete, die nicht durch shareAll geteilt werden sollen, werden unter skip eingetragen. Dies kann die Build- und Startzeit der Anwendung leicht verbessern. Außerdem müssen Pakete, die für die Nutzung mit NodeJS gedacht sind, unter skip angegeben werden, da sie nicht für den Einsatz im Browser kompiliert werden können.

Native Federation:
Eine Shell einrichten

Auch der Host, der als Micro-Frontend-Shell fungiert, kann mit ng add eingerichtet werden:
				
					ng add @angular-architects/native-federation --project shell --port 4200 --type dynamic-host

				
			
Der Typ dynamic-host gibt an, dass die zu ladenden Remotes in einer Konfigurationsdatei definiert werden:
				
					{
    "mfe1" : "http://localhost:4201/remoteEntry.json"
}

				
			

Diese federation.manifest.json wird standardmäßig im assets-Ordner des Hosts erzeugt. Da sie als Asset behandelt wird, kann die Manifestdatei beim Deployment ausgetauscht werden. So lässt sich die Anwendung an die jeweilige Umgebung anpassen.

Das Manifest ordnet den Namen der Remotes deren Metadaten zu, die Native Federation beim Build in der Datei remoteEntry.json ablegt. Auch wenn ng add das Manifest generiert, sollte es überprüft werden – beispielsweise, um Ports bei Bedarf anzupassen oder nicht benötigte Remotes zu entfernen.

Der ng add-Befehl erstellt außerdem eine federation.config.js für den Host:

				
					const { withNativeFederation, shareAll } = require('@angular-architects/native-federation/config');

module.exports = withNativeFederation({

  shared: {
    ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
  },

  skip: [
    'rxjs/ajax',
    'rxjs/fetch',
    'rxjs/testing',
    'rxjs/webSocket',
    // Add further packages you don't need at runtime
  ]

});
				
			

Der aus der Remote-Konfiguration bekannte exposes-Eintrag wird für Hosts nicht generiert, da diese üblicherweise keine Dateien für andere Hosts veröffentlichen. Soll jedoch ein Host auch als Remote für andere Hosts fungieren, spricht nichts dagegen, diesen Eintrag manuell hinzuzufügen.

Die von ng add angepasste Datei main.ts initialisiert Native Federation über das Manifest:

				
					import { initFederation } from '@angular-architects/native-federation';

initFederation('/assets/federation.manifest.json')
  .catch(err => console.error(err))
  .then(_ => import('./bootstrap'))
  .catch(err => console.error(err));
				
			

Die Funktion initFederation liest die Metadaten der einzelnen Remotes und erstellt eine Import Map, die der Browser verwendet, um gemeinsam genutzte Pakete und veröffentlichte Module zu laden. Danach übergibt der Programmfluss an die bootstrap.ts, die die Angular-Anwendung wie gewohnt mit bootstrapApplication oder bootstrapModule startet.

Alle bisher betrachteten Dateien wurden durch ng add eingerichtet. Um nun ein vom Remote bereitgestelltes Programmmodul zu laden, muss der Host um Lazy Loading erweitert werden:

				
					[…]
import { loadRemoteModule } from '@angular-architects/native-federation';

export const APP_ROUTES: Routes = [
  […],
  {
    path: 'flights',
    loadComponent: () =>
      loadRemoteModule('mfe1', './Component').then((m) => m.AppComponent),
  },
  […]
];
				
			

Die Lazy Route verwendet die Hilfsfunktion loadRemoteModule, um die AppComponent vom Remote zu laden. Sie nutzt dabei den Namen des Remotes aus dem Manifest (mfe1) sowie den Namen, unter dem das Remote die gewünschte Datei veröffentlicht (./Component).

Eine Router-Konfiguration bereitstellen

Nur eine einzelne Komponente via Native Federation bereitzustellen, ist etwas zu fein granular. Häufig möchte man ein komplettes Feature veröffentlichen, das aus mehreren Komponenten besteht. Glücklicherweise können wir mit Native Federation alle möglichen TypeScript- bzw. EcmaScript-Konstrukte bereitstellen. Im Fall von gröber geschnürten Features könnten wir ein NgModule mit Subrouten freigeben – oder, wenn wir auf Standalone Components setzen, einfach eine Routing-Konfiguration. Letzteres ist hier der Fall:
				
					import { Routes } from "@angular/router";
import { FlightComponent } from "./flight/flight.component";
import { HolidayPackagesComponent } from "./holiday-packages/holiday-packages.component";

export const APP_ROUTES: Routes = [
    {
        path: '',
        redirectTo: 'flights',
        pathMatch: 'full'
    },
    {
        path: 'flight-search',
        component: FlightComponent
    },
    {
        path: 'holiday-packages',
        component: HolidayPackagesComponent
    }
];
				
			
Diese Routing-Konfiguration muss im Abschnitt exposes der federation.config.js des Micro Frontends eingetragen werden:
				
					const { withNativeFederation, shareAll } = require('@angular-architects/native-federation/config');

module.exports = withNativeFederation({

  name: 'mfe1',

  exposes: {
    './Component': './projects/mfe1/src/app/app.component.ts',

     // Add this line:
    './routes': '././projects/mfe1/src/app/app.routes.ts',
  },

  shared: {
    ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
  },

  skip: [
    'rxjs/ajax',
    'rxjs/fetch',
    'rxjs/testing',
    'rxjs/webSocket',
    // Add further packages you don't need at runtime
  ]

});
				
			
In der Shell kann direkt auf diese Routing-Konfiguration verwiesen werden:
				
					[...]
import { loadRemoteModule } from '@angular-architects/native-federation';

export const APP_ROUTES: Routes = [
  [...]

  {
    path: 'flights',
    // loadChildreas instead of loadComponent !!!
    loadChildren: () =>
      loadRemoteModule('mfe1', './routes').then((m) => m.APP_ROUTES),
  },

  [...]
];
				
			
Außerdem müssen die Routen in der Navigation der Shell angepasst werden:
				
					<ul>
    <li><img decoding="async" src="../assets/angular.png" width="50" title="Micro-Frontends mit modernem Angular – Standalone-Komponenten und esbuild 1"></li>
    <li><a routerLink="/">Home</a></li>
    <li><a routerLink="/flights/flight-search">Flights</a></li>
    <li><a routerLink="/flights/holiday-packages">Holidays</a></li>
</ul>

<router-outlet></router-outlet>
				
			

Kommunikation zwischen Micro Frontends

Die Kommunikation zwischen Micro Frontends kann ebenfalls über gemeinsam genutzte Libraries ermöglicht werden. Vorweg möchte ich aber sagen: Diese Möglichkeit sollte nur mit Bedacht genutzt werden. Micro-Frontend-Architekturen zielen darauf ab, einzelne Frontends voneinander zu entkoppeln. Wenn jedoch ein Frontend Informationen von einem anderen erwartet, wird genau das Gegenteil erreicht. Die meisten Lösungen, die ich gesehen habe, teilen nur einige wenige kontextbezogene Informationen – etwa den aktuellen Benutzernamen, den aktiven Kunden oder ein paar globale Filter.

Um Informationen zu teilen, benötigen wir zunächst eine gemeinsame Library. Diese kann entweder ein separat entwickeltes npm-Paket oder eine Library innerhalb des aktuellen Angular-Projekts sein. Letztere lässt sich mit folgendem Befehl erzeugen:

				
					ng g lib auth

				
			

Der Name der Library ist in diesem Fall auth. Um Daten zu teilen, erhält diese Library einen State-Service. Der Einfachheit halber verwende ich hier den simpelsten zustandsbehafteten Service, der mir einfällt:

				
					@Injectable({
  providedIn: 'root'
})
export class AuthService {
  userName = '';
}
				
			

In diesem sehr einfachen Szenario wird der Service als Schwarzes Brett genutzt: Ein Micro Frontend schreibt Informationen hinein, ein anderes liest sie aus. Etwas komfortabler wäre jedoch ein Publish/Subscribe-Mechanismus, bei dem interessierte Stellen über Änderungen benachrichtigt werden. Diese Idee lässt sich z. B. mithilfe von RxJS Subjects umsetzen.

Wenn Libraries innerhalb eines Monorepos verwendet werden, sollten sie über ein Path Mapping in der tsconfig.json verfügbar gemacht werden:

				
					"compilerOptions": {
    "paths": {
      "@demo/auth": [
        "projects/auth/src/public-api.ts"
      ]
     },
     […]
}
				
			

Wichtig: Ich verweise hier auf die public-api.ts im Quellcode der Library. Diese Strategie wird auch von Nx verwendet. Die Angular CLI verweist standardmäßig jedoch auf den dist-Ordner. In diesem Fall muss der Eintrag manuell angepasst werden.

Zudem muss sichergestellt sein, dass alle Kommunikationspartner das gleiche Path Mapping verwenden.

Fazit

Der neue esbuild-Builder bringt enorme Verbesserungen in der Build-Performance. Die beliebte Module Federation ist derzeit jedoch an webpack gebunden. Native Federation greift dasselbe bewährte Konzept auf, ist jedoch unabhängig vom verwendeten Tooling umgesetzt – und funktioniert somit mit allen gängigen Bundlern. Zudem setzt sie auf Webstandards wie EcmaScript-Module und Import Maps. Dadurch sind alternative Implementierungen möglich, was Native Federation langfristig zu einer zuverlässigen Lösung macht.

Erste Schritte mit Angular oder
direkt abtauchen ins Detail?

Ob Sie neu einsteigen oder Ihr Know-how auf das nächste Level bringen wollen –
unsere zwei Trainings liefern genau das, was Sie gerade brauchen.

Jetzt das passende Training finden

Manfred Streyer

Autor

Manfred Steyrer

Newsletter

Melden Sie sich jetzt für unseren Newsletter an und erhalten Sie einen 100€-Gutschein auf alle Trainings!

*“ zeigt erforderliche Felder an

Name*