Kolumne

EnterpriseTales: Micro Frontend Frameworks

Sven Kölpin
Funktionale Programmierung

Spotify, Zalando, DAZN: Alle tun es. Micro Frontends sind in der Industrie etabliert. Mittlerweile gibt es verschiedene Frameworks, die bei der Umsetzung dieses architektonischen Ansatzes für Web-Frontends verwendet werden können.

Was sind Micro Frontends nochmal?

Mit Micro Frontends wird das Konzept von Microservices konsequent bis in das Frontend weitergedacht. Neben dem Backend wird auch die Benutzungsoberfläche in fachlich getriebene, unabhängige Applikationen zerlegt. Damit ist ein System vertikal in isolierte Services unterteilbar, die von der Datenbank bis zum Web-Frontend alles beinhalten. Die Vorteile liegen darin, dass die jeweiligen Entwicklerteams nun auch im Frontend mit verschiedenen Frameworks und Tools arbeiten können. Vor allem in der zuletzt häufig schnelllebigen Webwelt kann eine solche Framework-Diversifikation positive Auswirkungen haben. Zudem erlauben Micro Frontends es, dass Anwendungsteile in unterschiedlichen Geschwindigkeiten entwickelt werden und so getrennt voneinander wachsen können. Natürlich führt eine solche Unterteilung des Frontends aber auch zu neuen (technischen) Herausforderungen. Je nach Art der Implementierung können die zu übertragenden Datenmengen beträchtlich anwachsen, beispielsweise wenn Teams verschiedene JavaScript-Frameworks oder Framework-Versionen zeitgleich verwenden. Auch die technische Komplexität ist nicht zu unterschätzen. Zudem stellt sich die Frage, ob und wie Informationen zwischen den einzelnen Micro Frontends ausgetauscht werden können.

Möglichkeiten der Umsetzung

Micro Frontends sind auf verschiedenen Wegen realisierbar. Die einfachste Art der Umsetzung ist die strikte Trennung von einzelnen Anwendungsbestandteilen nach Routen. Jedes Micro Frontend ist dabei zum Beispiel unter einem eigenen URL zu erreichen. Dieses Vorgehen bietet sich vor allem bei „klassischen“, serverseitig gerenderten Anwendungen an, die eine Navigation zwischen Anwendungsteilen zumeist ohnehin über einen Seitenwechsel realisieren.

Das Konzept von Micro Frontends wird aber vor allem dann interessant, wenn die verschiedenen Anwendungen zeitgleich in einer Benutzungsoberfläche angezeigt oder als Single Page Applikation (SPA) realisiert werden sollen. Dafür müssen die verschiedenen Micro Frontends logischerweise in irgendeiner Art zusammengefügt werden. Eine solche Integration kann sowohl zur Build-Zeit als auch zur Laufzeit erfolgen.

Bei der Build-Zeit-Integration werden die eigentlich voneinander unabhängigen Micro Frontends in einem extra Build-Step zu einem monolithischen Artefakt vereint und so schlussendlich als eine einzige Anwendung ausgeliefert. Vor allem bei rein clientseitigen Weboberflächen ist dieses Vorgehen eine Option, weil sie technisch gesehen relativ einfach umsetzbar ist. Der große Nachteil ist aber, dass dabei die Unabhängigkeit zwischen den Micro Frontends verlorengeht. Schließlich muss die gesamte Anwendung auch bei der kleinsten Änderung eines einzelnen Frontends neu gebaut und ausgeliefert werden.

Eine zweite mögliche Umsetzungsart ist die Integration zur Laufzeit, bei der die Micro Frontends dynamisch während des Betriebs mithilfe einer Integrationsschicht zu einer Benutzungsoberfläche zusammengeführt werden. Die Integrationsschicht kann entweder als dedizierte Serveranwendung, als Teil eines Micro Frontends oder direkt im Browser realisiert sein. Der Vorteil der Laufzeitintegration ist, dass die vollständige Unabhängigkeit der einzelnen Frontends auch zur Deployment-Zeit gewahrt ist. Der Nachteil liegt in der zusätzlichen technischen Komplexität.

Die meisten Micro Frontend Frameworks sind darauf spezialisiert, eine Integration zur Laufzeit zu ermöglichen. Grundsätzlich ist das über zwei verschiedene Wege möglich: serverseitige Integration und clientseitige Integration.

Serverseitige Integration

Bei dieser Integrationsart werden die Micro Frontends bei einer Anfrage serverseitig aggregiert und als eine einzige HTML-Seite an den Browser gesendet. Für den Benutzer ist so gar nicht ersichtlich, dass die Anwendung eigentlich aus mehreren Applikationen zusammengesetzt ist (Abb. 1). Vor allem für serverseitig rendernde Micro Frontends ist dieses Vorgehen geeignet, weil die Integrationsschicht in diesem Fall lediglich fertige HTML-Fragmente zusammenführen muss. Es ist aber ebenfalls möglich, clientseitig rendernde Micro Frontends (SPAs) über diesen Weg zu integrieren. Dazu sammelt die Integrationsschicht zwar die JavaScript-Ressourcen der einzelnen Frontends serverseitig auf, gerendert werden sie aber erst im Browser.

Wichtig ist bei einem serverseitigen Integrationslayer vor allem die Widerstandsfähigkeit (Resilience). Beispielsweise muss auf potenzielle Ausfälle oder langsame Antwortzeiten der zu integrierenden Micro Frontends reagiert werden können.

Eine serverseitige Integration kann ganz ohne Framework, zum Beispiel mit Hilfe von Server-side Includes (SSI), erreicht werden. Über die letzten Jahre haben sich am Markt aber auch spezialisierte Frameworks hierfür etabliert. Eines der bekanntesten ist das von Zalando entwickelte Projekt Tailor.

Tailor

Tailor ist ein auf Node.js basierendes Layout-Service-Framework für die serverseitige Komposition von Micro Frontends. Die Bibliothek ist Teil des Project Mosaics, das verschiedene Tools beinhaltet, die Entwicklern beim Erstellen von Micro Frontends helfen sollen.

Es ist möglich, Tailor mit unterschiedlichen, auf Node.js basierenden Serverbibliotheken (z. B. express) zu integrieren (Listing 1).

const express = require('express');
const path = require('path');
const Tailor = require('node-tailor');

const {
  PORT,
} = process.env;

const integrationServer = express();
const tailor = new Tailor({
  templatesPath: path.resolve(__dirname, 'templates') //set templates directory
});
integrationServer.get('/index', tailor.requestHandler); //add tailor to /index route
integrationServer.listen(PORT, () => console.log('Server up'));

Das Herzstück des Frameworks bilden dabei die Fragments. Mit ihnen lässt sich deklarativ beschreiben, von welchem Server ein bestimmtes Micro Frontend geladen und wo es auf der finalen Seite platziert werden soll (Listing 2).

<!-- File: templates/index.html -->
…
<body>
  <section>
  <fragment primary
    id="browse"
      src="http://browse.localhost/fragments"/>
</section>
<section>
  <fragment id="order"
    timeout="1000"
    fallback-src="http://order2.localhost/fragments"
    src="http://order.localhost/fragments"/>
</section>
</body>
…

Bei einem Request sorgt Tailor dann dafür, dass die entsprechenden Fragmente serverseitig abgerufen und an den Browser gestreamt werden. Es gibt dazu verschiedene Parameter für die Konfiguration. Vor allem die Einstellungsmöglichkeiten für Resilience Patterns wie Timeouts und Fallbacks sind hier hervorzuheben. Inspiriert von Facebooks Big-Pipe-Ansatz legt Tailor zudem einen besonderen Fokus auf Performance. Etwaige Fragmente werden parallel von den jeweiligen Micro Frontend Services angefragt und so früh wie möglich an den Browser gestreamt, ohne dabei den Rest der Seite zu blockieren.

Neben der Komposition von serverseitig gerenderten Micro Frontends können auch Ressourcen wie JavaScript und CSS ausgeliefert werden. Das Framework eignet sich also grundsätzlich auch für die Aggregation von clientseitig gerenderten Single-Page-Applikationen. Für die Auslieferung von Ressourcen müssen allerdings explizit entsprechende Link-Header vom jeweiligen Micro Frontend übermittelt werden (Listing 3).

//…
const {
  PORT,
} = process.env;

const ordersFrontend = express();
//…
ordersFrontend.get('/fragments', (req, res) => {
  //inform tailor about css resources
  res.set({ Link: `<http://localhost:${PORT}/css/style.css>; rel="stylesheet"` });
  //render fragment
  res.send(`<h1>Hello Order Micro Frontend</h1>`)
});

ordersFrontend.listen(PORT, () => console.log('Order app running'));

Tailor ist auf die performante, serverseitige Komposition von Fragmenten ausgelegt und liefert von Haus aus keine Lösungsansätze für die Isolation von Styling-Informationen. Das kann aber zum Beispiel zur Laufzeit mithilfe von Web Components oder zur Build-Zeit mit CSS Modules erreicht werden. Zudem ist nicht festgelegt, wie die einzelnen Micro Frontends untereinander kommunizieren können.

Clientseitige Integration

Bei einem clientseitig agierenden Integrationslayer findet die Komposition von Micro Frontends im Browser statt. Dieser Ansatz wird zumeist für die Integration verschiedener Single-Page-Applikationen verwendet, theoretisch ist er aber auch mit serverseitigen Ansätzen vereinbar. Die Herausforderungen liegen hier vornehmlich in der Umsetzung von Routing zwischen den einzelnen SPAs, in der Codeisolation und in der Reduktion der zu übertragenden Datenmengen (vor allem dann, wenn mehrere Frameworks parallel eingesetzt werden).

Die einfachste Art der Umsetzung einer clientseitigen Integrationsschicht ist die Verwendung von iFrames. Damit lassen sich HTML-Dokumente von (verschiedenen) Servern in einem einzigen Browserfenster kombinieren. Die einzelnen Teilapplikationen laufen dabei vollkommen abgegrenzt voneinander. Der Vorteil ist dabei, dass sowohl die Style-Isolation als auch die Isolation des JavaScript-Codes naturgemäß gegeben ist. Zudem können auch serverseitig gerenderte Applikationen integriert werden. Durch die iFrame-Isolation der einzelnen Anwendungen ist es allerdings schwierig, erwartungsgemäßes Routing, Deep Linking oder Code-Sharing umzusetzen.

Die clientseitige Integration verschiedener Single-Page-Applikationen ohne die Verwendung von iFrames erfordert ein Meta-Framework, das sich zum Beispiel um die Registrierung und das Routing der einzelnen Micro Frontends kümmert. Das bekannteste Meta-Framework dieser Art ist single-spa, das von canopytax entwickelt wurde und bereits bei zahlreichen Unternehmen zum Einsatz kommt.

single-spa

Mithilfe von single-spa lassen sich unterschiedliche Single-Page-Anwendungen zur Laufzeit in einer einzigen clientseitigen Anwendung integrieren. Dabei können sogar unterschiedliche Frameworks parallel zum Einsatz kommen (z. B. React und Angular). Die Navigation zwischen den einzelnen Anwendungsteilen erfolgt, ganz SPA-typisch, ohne einen echten Seitenwechsel. Zudem werden die einzelnen Micro Frontends nur bei Bedarf geladen, um so einen möglichst schnellen Seitenaufbau zu ermöglichen und die Netzwerklast zu reduzieren.

Damit ein jeweiliges Micro Frontend von single-spa integriert werden kann, muss es ein bestimmtes Interface implementieren. Darin ist definiert, wie und wo eine SPA im DOM hinzugefügt oder entfernt wird. Glücklicherweise gibt es bereits für alle großen SPA-Frameworks (Angular, React, Vue.js, …) fertige Integrationslayer, sodass der Implementierungsaufwand überschaubar ist (Listing 4 und Listing 5). Jedes Micro Frontend hat außerdem die Möglichkeit, eigene Routen zu definieren und so eine Navigation innerhalb des Micro Frontends im Kontext der Gesamtapplikation umzusetzen.

import React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import OrderApp from './OrderApp.js';

const reactLifecycles = singleSpaReact({
  React,
  ReactDOM,
  rootComponent: OrderApp,
  domElementGetter,
});

export const bootstrap = [
  reactLifecycles.bootstrap,
];

export const mount = [
  reactLifecycles.mount,
];

export const unmount = [
  reactLifecycles.unmount,
];

const domElementGetter = ()=> {
  // This is where single-spa will mount our application
  return document.querySelector("#orders");
}
import {NgZone} from '@angular/core';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {AppModule} from './app/app.module';
import singleSpaAngular from 'single-spa-angular';
import {singleSpaPropsSubject} from './single-spa/single-spa-props';

const lifecycles = singleSpaAngular({
  bootstrapFunction: singleSpaProps => {
    singleSpaPropsSubject.next(singleSpaProps);
    return platformBrowserDynamic().bootstrapModule(AppModule);
  },
  template: '<app-root />',
  NgZone: NgZone,
});

export const bootstrap = lifecycles.bootstrap;
export const mount = lifecycles.mount;
export const unmount = lifecycles.unmount;

Im eigentlichen Integrationslayer (die „single-spa-Anwendung“) muss definiert werden, welches Micro Frontend unter welcher Subroute aktiviert werden soll (Listing 6).

import { registerApplication, start } from 'single-spa';
//…
registerApplication(
  'orders-app',
    () => System.import('orders').then(m => m.default),
  location => location.pathname.startsWith('/orders')//load app when path is orders
);

registerApplication(
  'browse-app',
  () => System.import('browse').then(m => m.default),
  location => location.pathname === '/'//load app when path is '/'
);

start();

Das Framework kümmert sich dann darum, dass bei einer zutreffenden Route der entsprechende Code geladen und das Frontend initialisiert wird. Zudem definiert single-spa Methoden zur Fehlerbehandlung (z. B., wenn der Server eines Micro Frontends nicht erreichbar ist), und auch das Übergeben von Parametern an die jeweiligen Subapplikationen (z. B. Tokens oder Benutzerinformationen) ist möglich. Für etwaige Framework-Unverträglichkeiten gibt es zudem einen Lösungsansatz, mit dem das Problem des Überschreibens von global definierten JavaScript-Variablen gelöst werden kann.

Mit single-spa lassen sich die einzelnen Micro Frontends sowohl zur Build-Zeit als auch zur Laufzeit integrieren. Eine Build-Zeit-Integration ist vergleichsweise einfach umzusetzen, bringt aber die zuvor bereits erwähnten Nachteile mit sich. Eine Integration zur Laufzeit erfordert einige Anpassungen im Build-Prozess der einzubindenden Single-Page-Applikationen, weil zum dynamischen Laden der Anwendungsteile systemjs als Modulsystem empfohlen wird – und das ist leider bei den wenigsten Frameworks standardmäßig integriert. Glücklicherweise stehen aber verschiedene Beispiele und sogar ein Playground für diesen Schritt zur Verfügung. Für Angular gibt es außerdem ein Schematics-Skript, das die erforderliche Anpassung weitgehend automatisch vollzieht.

single-spa adressiert zwar das Problem der Style-Isolation nicht direkt, es ist aber durch die Verwendung von CSS-Modulen in allen gängigen SPA-Frameworks zumeist ohnehin bereits implizit gelöst. Auch eine Vorgabe für die Umsetzung von Kommunikation zwischen den einzelnen Micro Frontends gibt es nicht, abgesehen von der Übergabe von initialen Parametern.

Fazit

Micro Frontends können grundsätzlich serverseitig und clientseitig integriert werden. Für beide Ansätze gibt es mit Tailor und single-spa mittlerweile ausgereifte Frameworks, die viele der typischen technischen Herausforderungen lösen. Unbeantwortet lassen beide Frameworks die Frage, wie zwischen den einzelnen Micro Frontends kommuniziert werden sollte.

Natürlich sind die hier gezeigten Ansätze nicht die einzigen Möglichkeiten. Als clientseitige Alternative zu single-spa darf beispielsweise frint.js nicht unerwähnt bleiben. Auch der Registry-basierende Ansatz von OpenComponents ist durchaus interessant.

In diesem Sinne: Stay tuned.

Verwandte Themen:

Geschrieben von
Sven Kölpin
Sven Kölpin
Sven Kölpin ist Enterprise Developer bei der open knowledge GmbH in Oldenburg. Sein Schwerpunkt liegt auf der Entwicklung webbasierter Enterprise-Lösungen mittels Java EE.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
4000
  Subscribe  
Benachrichtige mich zu: