Microfrontend-POC mit Angular, Webpack Module Federation und Angular Elements (Web Components). Drei vollständig unabhängige Angular-Anwendungen mit verschiedenen Angular-Versionen.
poc-angular-microfrontend/
├── shell/ AppShell – Angular 19, Port 4200
├── remote/ Remote "Produkte" – Angular 18, Port 4201
├── orders/ Remote "Bestellungen" – Angular 17, Port 4202
└── docs/
└── architecture.md Vollständige Architekturdokumentation
Jede App hat ihr eigenes node_modules/. Befehle immer im jeweiligen App-Verzeichnis ausführen.
cd remote && npm start # http://localhost:4201
cd orders && npm start # http://localhost:4202
cd shell && npm start # http://localhost:4200Shell startet auch ohne laufende Remotes (APP_INITIALIZER fängt Fehler ab).
cd <app> && npx ng build --configuration development| App | Angular | TypeScript |
|---|---|---|
| shell | 19 | 5.7 |
| remote | 18 | 5.5 |
| orders | 17 | 5.2 |
Angular ist nicht als shared konfiguriert – jede App bundelt ihre eigene Version. Nur zone.js ist Singleton. Bei Versionsänderungen muss TypeScript mitgezogen werden (Angular 17 → TS <5.3, Angular 18 → TS <5.6, Angular 19 → TS <5.8).
Neue Remotes ohne Shell-Codeänderungen hinzufügen:
- Remote-App erstellen (Angular-App + Module Federation +
register.ts+AppModule) metadata.tsanlegen:{ elementName, routePath, navLabel }– kein Import, ~640 Bytewebpack.config.js:./metadataund./registerexposen- Eintrag in
shell/public/mfe-manifest.json
AppModule.ngDoBootstrap()
→ createCustomElement(FeatureComponent, { injector })
→ customElements.define('mfe-<name>', ...)
@Input() → HTML-Attribute, @Output() → CustomEvents.
@Output() mfeAction.emit({ type, payload })
→ CustomEvent 'mfeAction'
→ DynamicMfeWrapperComponent.addEventListener + NgZone.run()
→ EventBusService.emit()
→ Subscriber (z. B. HeaderComponent)
HTML-Attribute auf dem Custom Element: el.setAttribute('currentUser', 'Max').
Lädt mfe-manifest.json per APP_INITIALIZER, importiert ./metadata von jedem Remote (ohne Angular-Bundle zu laden), hält Einträge als BehaviorSubject<MfeEntry[]>.
Generischer Wrapper für alle Remotes. Liest routePath aus ActivatedRoute.paramMap (Observable, nicht snapshot – sonst kein Reload bei Navigation), erstellt Custom Element via document.createElement(), mountet es in Container-div.
type ist string (kein Union-Typ) – Remotes können beliebige Event-Typen emittieren ohne Shell-Änderung.
platformBrowserDynamic().bootstrapModule() darf nicht innerhalb einer Zone aufgerufen werden. Lösung: NgZone.runOutsideAngular(() => m.registerRemote()) in MfeLoaderService.
Listener läuft in Remote-Zone → Shell-Change-Detection wird nicht getriggert. Lösung: NgZone.run(() => this.eventBus.emit(event)).
Bei /mfe/products → /mfe/orders wird DynamicMfeWrapperComponent wiederverwendet, ngOnInit nicht erneut aufgerufen. Lösung: route.paramMap als Observable abonnieren, bei Wechsel altes Element entfernen und neues mounten.
remoteEntry.js ist ein ES-Modul. Dynamischer import() ist Cross-Origin → CORS-Header nötig. Konfiguriert in angular.json unter serve.options.headers.
register.ts und metadata.ts liegen nicht im Import-Baum von main.ts → explizit in files[] aufnehmen, sonst vom TS-Compiler ignoriert.
Nur in tsconfig.app.json und tsconfig.spec.json, nicht in Root-tsconfig.json. Sonst VS-Code-Fehler TS6059.
Angular 17+ verwendet public/ als Asset-Ordner. mfe-manifest.json liegt in shell/public/ und ist unter /mfe-manifest.json erreichbar (nicht /assets/mfe-manifest.json).
Die Shell kommuniziert nur über W3C-Standards (Custom Elements, HTML-Attribute, CustomEvents). React-, Vue- und Svelte-Remotes sind möglich – metadata.ts und das mfeAction-CustomEvent-Format sind Framework-agnostisch. zone.js wird von Nicht-Angular-Remotes ignoriert.
Web Components (@company/ui-kit, z. B. mit Lit gebaut) können über Module Federation als Singleton geteilt werden – im Gegensatz zu Angular selbst, das nicht geteilt wird. customElements.define wird global registriert und ist für alle Apps verfügbar. In webpack.config.js aller Apps: '@company/ui-kit': { singleton: true, strictVersion: false }.
- App anlegen (eigenen Port wählen)
metadata.tsanlegen –{ elementName, routePath, navLabel }, keine Importsregister.ts– exportiertregisterRemote()AppModule–DoBootstrap+createCustomElementwebpack.config.js–./registerund./metadataexposen, nurzone.jssharentsconfig.app.json– beide Dateien infiles[]angular.json– CORS-Header,tsconfig.json– keinoutDirshell/public/mfe-manifest.json– neuen Eintrag ergänzen
Vollständige Dokumentation: docs/architecture.md