adesso Blog

Micro-Frontends

In der Regel werden Backend-Anwendungen, die auf einer Micro-Services-Architektur basieren, durch eine monolithische Anwendung im Frontend, häufig in Form einer Web-Applikation, ergänzt. Während das serverseitige System von allen Vorzügen einer verteilten Architektur profitiert, stehen der Entwicklung des Frontends altbekannte Herausforderungen im Weg. Vorteile in Bezug auf Skalierbarkeit, Flexibilität, Organisation sowie kontinuierliche Integration und Bereitstellung reichen nicht über die Grenzen des Backends hinaus, sondern enden dort, wo die Entwicklung der monolithischen Anwendung beginnt. Das Konzept der Micro-Frontends möchte dieser Problematik entgegenwirken, indem es frontendseitige Webanwendungen in einzelne, voneinander unabhängige Teilsysteme gliedert. Hierdurch entstehen vertikale Strukturen, die von der Nutzeroberfläche über die serverseitige Datenlogik bis hin zur Persistenz reichen.

Vertikale Strukturen mit Micro-Frontends, Quelle: https://micro-frontends.org/

Vertikale Strukturen mit Micro-Frontends, Quelle: https://micro-frontends.org/

Vor- und Nachteile

Hervorzuheben ist, dass Micro-Frontends weniger dazu dienen, Probleme technischer Natur zu lösen, sondern vielmehr ein Mittel zur Bewältigung organisatorischer Herausforderungen darstellen. Gerade für sehr große Projekte, die auf eine Micro-Services-Architektur im Backend aufsetzen und eventuell schon eine fachliche Aufteilung von Entwicklungsteams vorsehen, bieten Micro-Frontends viel Potenzial. Konkret ergeben sich aus ihrer Anwendung folgende Vorteile:

  • Unabhängige Entwicklung und Bereitstellung durch autonome Teams
  • Lose Kopplung und simplere Codebasen
  • Technologische Unabhängigkeit einzelner Teams

Bei der Abwägung von Micro-Frontends als anzuwendendem Architekturmuster spielen jedoch nicht nur die Vorteile eine Rolle. Wie jede Lösung in der Softwareentwicklung stellen auch sie keine „Silver Bullet“ dar, sondern bringen einige Nachteile mit sich. Diese lassen sich vor allem auf eine erhöhte technische und organisatorische Komplexität zurückführen, da folgende Aspekte berücksichtigt werden sollten:

  • Aufteilung der Anwendung in Teilsysteme
  • Zusammenführung der Teilsysteme zu einem Gesamtsystem
  • Gewährleistung einer konsistenten User Experience
  • Dependency Management
  • Erhöhter Aufwand für die Infrastruktur

Wer also eine Micro-Frontends-Architektur in Betracht zieht, sollte ausreichend Zeit in die Planung investieren, um den damit einhergehenden Aufwand korrekt einzuschätzen und etwaigen Problemen frühzeitig entgegenwirken zu können.

Technische Umsetzung

Wenn man die Popularität von Micro-Frontends mit der von Micro-Services vergleicht, stellt man fest, dass Erstere sich bei weitem noch nicht so etabliert haben wie ihr Backend-Pendant. Dies ist womöglich auch einer der Gründe dafür, dass es für ihre Umsetzung keinerlei Best-Practice- oder „Go to“-Ansätze gibt. Da das Prinzip der Micro-Frontends im Grunde nichts anderes vorsieht, als unabhängige Frontend-Anwendungen einzeln zu entwickeln, bereitzustellen und zu einer nahtlosen Gesamtanwendung zusammenzufügen, reichen die Umsetzungsmöglichkeiten von einfachen nativen Lösungen bis hin zu komplexeren, werkzeuggestützten Methoden. Ich möchte euch im Folgenden die Umsetzung mit Hilfe von webpack und Module-Federation zeigen.

Micro-Frontends mit Angular und webpacks Module-Federation

Der Module-Bundler webpack bietet ab Version 5 und aufwärts eine Funktion namens „Module-Federation“, mit Hilfe derer es möglich ist, separat gebündelte und bereitgestellte Anwendungen während der Laufzeit in eine andere Applikation einzubinden. Der Bundler findet unter anderem Verwendung innerhalb des Angular-CLI, des Standard-Tools für die Entwicklung und Auslieferung von Angular-Anwendungen. Daher ist Module-Federation ein interessanter Ansatz für die Umsetzung einer Micro-Frontends-Architektur innerhalb einer Angular-Anwendungslandschaft. Wie genau man auf Angular basierende Micro-Frontends mit Hilfe von Module-Federation entwerfen und sie in einer gemeinsamen Anwendung zusammenführen kann, möchte ich euch im nächsten Abschnitt zeigen.

Wir wollen eine prototypische Anwendung, basierend auf Angular und Module-Federation, umsetzen, die aus folgenden Teilsystemen besteht:

Prototypische Anwendung, Quelle: eigene Darstellung

Prototypische Anwendung, Quelle: eigene Darstellung

  • Rot: App-Shell: Angular-Komponente als Teil der Rahmenanwendung
  • Gelb: „Buy“-Komponente: dynamisch geladene Angular-Komponente
  • Orange: „Banner“-Komponente: dynamisch geladene Web-Component
  • Grün: „Cart“- und „Menu“-Komponente: Teil eines dynamisch geladenen Angular-Moduls

Um Module-Federation nutzen zu können, muss die von dem Angular-CLI verwendete webpack-Konfiguration entsprechend angepasst werden. Dies lässt sich am einfachsten mit Hilfe eines benutzerdefinierten Angular-CLI-Builders realisieren. Hierfür nutzen wir den Builder, der Teil des npm-Paketes @angular-architects/module-federation ist. Installieren wir das Paket wie folgt innerhalb unserer bereits existierenden Angular-Anwendung: ng add @angular-architects/module-federation --project X -- port X, erhalten wir eine webpack-Konfigurationsdatei, die wir anpassen können und die beim Build-Prozess von dem Angular-CLI berücksichtigt wird. Über die Optionen project und -- port werden der Name des Projektes sowie der Port, auf dem der Angular- Entwicklungsserver dieses bereitstellt, an das Projekt übergeben. Mit Hilfe der webpack- Konfiguration können wir unsere Micro-Frontends definieren. Jede Anwendung, die Teil der Micro-Frontends-Architektur ist, wird mit einer eigenen webpack-Konfiguration versehen. Innerhalb dieser wird definiert, welche anderen Anwendungen eingebunden und welche Anwendungsteile – etwa Module oder Komponenten - nach außen hin freigegeben werden sollen. Letztere können dann wiederum von anderen Micro-Frontends genutzt werden. Folgende webpack-Konfiguration ist Teil der „Menu“-Anwendung:

	
	module.exports = {
		  output: {
		    uniqueName: "mf1",
		  },
		  plugins: [
		    new ModuleFederationPlugin({
		      name: "menu",
		      filename: "remoteEntry.js",
		      exposes: {
		        "./MenuModule": "./src/app//menu/menu.module.ts",
		      },
		      remotes: {
		        cart: "cart@http://localhost:3001/remoteEntry.js",
		      },
		    }),
		  ],
	

In dem „exposes“-Objekt wird definiert, welche Teile der Anwendung nach außen hin freigegeben und an anderer Stelle eingebunden werden können. In diesem Fall stellt das „Menu“-Micro-Frontend ein Modul innerhalb der Datei „menu.module.ts“ über den Schlüssel „./MenuModule“ bereit. In dem „remotes“-Objekt wird definiert, welche anderen Micro-Frontends konsumiert werden und über welche URL diese eingebunden werden können. In diesem Fall wird der Einstiegspunkt des „cart“-Micro-Frontends über die entsprechende URL referenziert. Die Eigenschaft „filename“ beschreibt den Namen der JavaScript-Datei, die unter anderem für die Auflösung der Micro-Frontends und ihrer Pfade sorgt. Sie wird von webpack generiert und dient als Einstiegspunkt in die einzelnen Micro-Frontends.

Folgendes Diagramm zeigt den Zusammenhang zwischen den webpack-Konfigurationen aller Teilanwendungen:

Zusammenhand einzelner Micro-Frontends, Quelle: eigene Darstellung

Zusammenhand einzelner Micro-Frontends, Quelle: eigene Darstellung

Eine Möglichkeit, ein Micro-Frontend zu integrieren, ist die Nutzung von Angulars Lazy-Loading-Funktionalität. Hierfür wird in der App Shell folgende Routen-Konfiguration definiert:

	
	{
		    path: 'menu',
		    loadChildren: () => import('menu/MenuModule').then((m) => m.MenuModule),
		  },
	

Der Import-Pfad zeigt auf das über die „Menu“-webpack-Konfiguration bereitgestellte „MenuModule“ und ergibt sich wie folgt (Beispiel aus einer anderen Anwendung):

Zusammensetzung dynamischer Import-Pfade, Quelle: eigene Darstellung

Zusammensetzung dynamischer Import-Pfad, Quelle: eigene Darstellung

Neben der Einbindung ganzer Module ist es ebenfalls möglich, einzelne Angular-Komponenten gezielt einzubinden. Dieser Ansatz ist hilfreich, wenn ein Micro-Frontend nicht eine ganze Seite einer Webanwendung darstellen, sondern als Teil einer übergeordneten Seite fungieren soll. Ein solches Beispiel stellt das „Buy“-Micro-Frontend unseres Systems dar, das eine Button-Komponente rendert.

	
	export class BuyTemplateComponent implements OnInit, AfterViewInit {
		  @ViewChild('container', { read: ViewContainerRef })
		  container?: ViewContainerRef;
		  constructor(private cfr: ComponentFactoryResolver) {}
		  ngOnInit(): void {}
		  ngAfterViewInit(): void {
		    this.lazyLoadComponent();
		  }
		  async lazyLoadComponent(): Promise<void> {
		    const { BuyButtonComponent } = await import('buy/BuyComponent');
		    const cFactory = this.cfr.resolveComponentFactory(BuyButtonComponent);
		    this.container?.clear();
		    const buyButtonInstance = this.container?.createComponent(cFactory)
		      .instance;
		    (buyButtonInstance as any).label = 'Buy!';
		  }
		}
	

Für das Rendern eines dynamisch geladenen Micro-Frontends innerhalb einer Angular-Komponente kann der vom Framework bereitgestellte ComponentFactoryResolver genutzt werden. Hierfür wird mit Hilfe von ViewChild() eine Referenz auf einemContainer im entsprechenden HTML-Template geschaffen und anschließend als Outlet für die zu rendernde Komponente genutzt. Wie in Zeile 28 zu sehen ist, entspricht der dynamische Import-Pfad demselben Schema wie bei dem Laden des „MenuModule“ via Lazy-Loading. Das Laden von Modulen und Komponenten stellt jeweils einen Angular-spezifischen Ansatz zur Zusammenführung mehrerer Micro-Frontends dar. Wer sich Framework-unabhängiger aufstellen möchte, der kann einzelne Teilsysteme auch mit Hilfe von Web-Components definieren und diese anschließend in jede beliebige Anwendung einbinden. Da Web- Components einen Web-Standard darstellen, können sie sowohl alleinstehend als auch in Verbindung mit jeglichem modernen JavaScript-Framework genutzt werden.

Fazit

Wir haben gesehen, wie man mit etwas Vorbereitung und Konfigurationsaufwand eine Angular-Anwendung aus mehreren Teilanwendungen, seien es Module oder Komponenten, zusammensetzen kann. Der hier präsentierte Prototyp ist weitaus weniger komplex als eine vollfunktionale Anwendung, die viel mehr organisatorische Weitsicht und Aufwand bedarf. Nichtsdestotrotz zeigt sie, wie man eine verteilte Frontend-Architektur auf Basis moderner Web-Frameworks realisieren kann. Ob Micro-Frontends sich als Alternative zu dem klassischen Frontend-Monolithen etablieren können, wird die Zukunft zeigen. Es gibt sicherlich einige Projekte, deren Codebasis und deren Entwicklungsprozesse von der Umstellung auf Micro-Frontends profitieren würden, jedoch ist die Einstiegshürde in Form der organisatorischen Komplexität nicht zu unterschätzen.

Bild Dario Braun

Autor Dario Braun

Dario Braun ist Software Engineer bei adesso und beschäftigt sich hauptsächlich mit der Entwicklung von Webanwendungen. Mit Hilfe moderner Frameworks wie Angular, React und NestJS entwickelt er individuelle Softwarelösungen für kundenspezifische Anforderungen.

Diese Seite speichern. Diese Seite entfernen.