Wie wir unsere Anwendungen auf die Micro-Frontend-Architektur bei Gcore migrieren

Wie wir unsere Anwendungen auf die Micro-Frontend-Architektur bei Gcore migrieren

Bei Gcore sind uns die Entwicklungen in der Technologiewelt nicht fremd. Wir haben vor kurzem ein neues Projekt gestartet: die Migration unserer größtenteils Angular-basierten Anwendungen auf eine Micro-Frontend-Architektur. Aufgrund unserer umfangreichen Nutzung von Angular haben wir uns für Module Federation als Strategie für diesen Übergang entschieden.

Unsere Ziele

Als wir unsere Migrationsreise begannen, legten wir klare Ziele fest. Unsere Ambitionen beschränkten sich nicht nur auf die Modernisierung unseres Technologie-Stacks, sondern zielten auch darauf ab, die Benutzererfahrung und unseren Entwicklungsprozess spürbar zu verbessern.

  • Ladezeit reduzieren: Unsere erste Priorität bestand darin, die Leistung unserer Anwendungen durch Reduzierung der Ladezeit zu verbessern. Schnellere Ladezeiten führen direkt zu einer verbesserten Benutzerzufriedenheit und -einbindung.
  • Module wiederverwenden: Unser Ziel war es, einen effizienteren Entwicklungsprozess zu etablieren, indem wir Module in verschiedenen Anwendungen wiederverwenden. Das minimiert nicht nur die Redundanz, sondern beschleunigt auch den Entwicklungszyklus und verbessert die Wartbarkeit.
  • Subdomains zugunsten von Pfadnamen aufgeben: Wir wollten auf die Verwendung von Subdomains verzichten (natürlich nicht vollständig) und uns stattdessen für Pfadnamen entscheiden. Diese Änderung wird uns eine genauere Kontrolle über das Routing ermöglichen und eine nahtlosere Benutzererfahrung bieten.
  • Widget-Skript-Initialisierung optimieren: Und schließlich verfügen wir über ein Widget-Skript, das bei jeder Anwendung initialisiert wird. Wir haben beschlossen, dass sich das ändern muss. Anstatt das Skript mit jeder App einzeln zu laden, was wertvolle Zeit verschwendet, wollten wir, dass dieser Vorgang nur einmal beim Laden unserer Shell-Anwendung durchgeführt wird.

Diese Ziele leiteten unsere Migration zur Micro-Frontend-Architektur. In unserer Geschichte geht es nicht nur um ein technologisches Upgrade, sondern auch um das Streben nach einer effizienteren, benutzerfreundlicheren digitalen Umgebung.

Module Federation

Bevor wir näher auf unsere Reise eingehen, wollen wir etwas Licht auf das entscheidende, von uns eingesetzte Tool werfen – Module Federation. Module Federation, eine in Webpack 5 eingeführte Funktion, ermöglicht separate Builds zur Bildung verschiedener „Mikro-Frontends“, die nahtlos zusammenarbeiten können.

Sie ermöglicht verschiedenen JavaScript-Anwendungen, Code aus einem anderen Build dynamisch auszuführen und dabei im Wesentlichen Bibliotheken oder Komponenten gemeinsam zu nutzen. Diese Architektur fördert die Wiederverwendung von Code, optimiert die Ladezeiten und steigert die Skalierbarkeit der Anwendung erheblich.

Mit einem besseren Verständnis für Module Federation können wir nun untersuchen, welche entscheidende Rolle es in unserem Migrationsprozess gespielt hat.

ngx-build-plus

Im Angular-Ökosystem hat ngx-build-plus die Implementierung von Module Federation grundlegend verändert. Es handelt sich um eine Erweiterung für die Angular CLI, die es uns ermöglicht, die Build-Konfiguration zu optimieren, ohne die gesamte Webpack-Konfiguration ändern zu müssen.

Wir können gemeinsame Abhängigkeiten definieren und so sicherstellen, dass sie nur einmal im endgültigen Bundle enthalten sind. Im Folgenden finden Sie ein Beispiel für eine Konfiguration, in der wir Angular-Bibliotheken, RXJS und einige benutzerdefinierte Gcore-Bibliotheken freigegeben haben:

hared: share({ 
	'@angular/core': { singleton: true, requiredVersion: '^14.0.0' }, 
	'@angular/common': { singleton: true, requiredVersion: '^14.0.0' }, 
	'@angular/router': { singleton: true, requiredVersion: '^14.0.0' }, 
	rxjs: { singleton: true, requiredVersion: '>=7.1.0' }, 
	'@gcore/my-messages': { 
    	singleton: true, 
    	strictVersion: false, 
    	packageName: '@gcore/my-messages', 
	}, 
	'@gcore/my-modules': { 
    	singleton: true, 
    	strictVersion: true, 
    	requiredVersion: '^1.0.0', 
    	packageName: '@gcore/my-modules', 
	}, 
	'@gcore/ui-kit': { singleton: true, requiredVersion: '^10.2.0' }, 
}),

So, da habt Ihr es, Leute! Holen Sie sich ngx-build-plus, richten Sie Module Federation ein, konfigurieren Sie Ihre gemeinsamen Abhängigkeiten und voilà, Sie sind ein Micro-Frontend-Meister. Herzlichen Glückwunsch!

Oh, warten Sie…

Kommunikation zwischen Anwendungen

Mit zunehmender Komplexität unserer Anwendungen wuchs auch der Bedarf an einer effizienten Kommunikation zwischen diesen. Zunächst hatten wir auf jeder Anwendungsseite ein Widget-Skript geladen und die Kommunikation zwischen der Anwendung und dem Widget wurde über das Fensterobjekt orchestriert. Es funktionierte, und uns wurde klar, dass wir es noch weiter optimieren konnten.

Geben Sie @gcore/my-messages ein, unseren ureigenen Ritter in glänzender Rüstung. Es handelt sich um einen gemeinsam nutzbaren Dienst. Es ähnelt eher einem Nachrichtenbus, außer dass es sich nicht um einen Bus handelt, sondern um einen von rxjs unterstützten Dienst.

Aber bevor wir uns zu Metaphern hinreißen lassen, wollen wir eines klarstellen: Dieser Dienst kennt Widgets und Anwendungen glücklicherweise nicht. Es handelt sich lediglich um Schnittstellen von Nachrichten und der Logik zum Senden dieser Nachrichten. Grundsätzlich handelt es sich bei diesen Schnittstellen um Konventionen. Dadurch bleiben sie schlank, effizient und unvoreingenommen, was sie zu einem perfekten Vermittler für die Kommunikation unserer Anwendungen macht.

Und es geht noch weiter.

Wo stehe ich? Mangel an Selbstwahrnehmung der Statiker

Statiker sind sich ihrer Umgebung glücklicherweise nicht bewusst, und dieser Mangel an Selbstwahrnehmung kann echte Probleme verursachen.

Um diese existenzielle Krise zu lösen, haben wir einen Mechanismus geschaffen, der jede Micro-Frontend-App über ihre eigene Herkunft informieren kann. Es hätten verschiedene Lösungen eingeführt werden können, aber wir entschieden uns für my-modules.

Stellen Sie sich @gcore/my-modules als Reiseführer vor. Dieser wird in die Shell-Anwendung eingefügt und enthält alle wesentlichen Informationen über die Micro-Frontend-Apps. Dieses Umgebung erkennende Modul wird während der Shell-CI/CD-Prozesse konfiguriert. Wodurch es dynamisch und dennoch zuverlässig ist und während der Shell-Initialisierung gefüllt wird. So kann es jederzeit nach Ihren Wünschen konfiguriert werden.

Über Module Federation kann my-modules gemeinsam genutzt werden, sodass andere Apps bei Bedarf auf diese wichtigen Informationen zugreifen können. Beachten Sie, dass Sie jedes Mal, wenn Sie eine neue Micro-Frontend-Anwendung hinzufügen, die über Ihre Shell bereitgestellt werden soll, my-modules aktualisieren und richtig konfigurieren müssen. So gehen keine Anwendungen mehr verloren, jeder weiß, wo sie sich befinden.

Lokale Entwicklung (MF-App als Standalone)

Lassen Sie uns nun über etwas sprechen, das Sie noch nicht gesehen haben – @gcore/my-panel. Sie haben es vielleicht noch nicht in der Konfiguration des Module Federation-Webpacks gesehen, aber es war die ganze Zeit da und hat unermüdlich hinter den Kulissen gearbeitet.

Die Rolle von @gcore/my-panel besteht darin, uns bei der Initialisierung des Widgets zu helfen. Es verarbeitet Widget-Nachrichten, sendet sie über Widget-Nachrichten und macht auch das Gegenteil. Und das ist noch nicht alles; @gcore/my-panel dient einer weiteren wichtigen Rolle während der lokalen Entwicklung und ermöglicht es uns, unsere Micro-Frontend-Anwendung als eigenständige Anwendung auszuführen.

Also, wie funktioniert es? Nun, in einer Micro-Frontend-Anwendung sollten Sie es, ähnlich wie in der Shell-Anwendung, während des Initialisierungsprozesses initialisieren. So gehen wir in unserem app.init.ts vor::

export async function initApp( 
appConfigService: AppConfigService, 
myModulesService: MyModulesService, 
myPanelService: MyPanelService, 
): Promise<unknown> { 
await appConfigService.loadConfig(); 
fillMyModulesService(myModulesService, appConfigService); 
return myPanelService.init(appConfigService.config.widgetUrl); 
}

Auf diese Weise ist es uns gelungen, @gcore/my-panel zu integrieren und effektiv in unseren Anwendungen zu nutzen, was es zu einem unverzichtbaren Bestandteil unserer Migration zu einer Micro-Frontend-Architektur macht.

Wenn Sie genau hinschauen, werden Sie in der Tat einen weiteren Schlüsselvorgang sehen, der in unserer Funktion initApp stattfindet. Wir füllen unser myModulesService mit Einstellungen aus unserem appConfigService. Mit diesem wichtigen Schritt wird sichergestellt, dass unsere Widgets ordnungsgemäß mit der erforderlichen Konfiguration ausgestattet sind, um in unseren Anwendungen optimal zu funktionieren. Sie können also in Ihrer MF-Anwendung auf der Ebene app.module APP_INITIALIZER bereitstellen:

export function initApp(myPanelService: myPanelService): any { 
	return async (): Promise<void> => { 
    	await myPanelService.init(); 
	}; 
}

Sie fragen sich vielleicht: „Wo ist die umfangreiche Konfiguration, die wir normalerweise in initApp von init.app.ts sehen können?“ Da sind Sie nicht allein! Der Ansatz hat sich tatsächlich geändert. Lassen Sie uns das analysieren.

Initialisierung der Micro-Frontend-Anwendung

Wenn wir unsere Anwendung auf einer Domäne wie localhost:4001 bereitstellen, verhält sie sich genau wie eine Standard-Angular-Anwendung – dank der Magie von myPanelService.init(). Diese Funktion ermöglicht es Entwicklern, in einer vertrauten Umgebung mit ihrer Anwendung zu arbeiten. Betrachten wir diese Anwendung als Mfe1App, die auf localhost:4001 gehostet wird.

Interessant wird es jedoch, wenn wir versuchen, unsere Micro-Frontend-Anwendung in unsere Shell-Anwendung zu laden. Webpack besucht localhost:4001/remoteEntry.js, um das Micro-Frontend-Modul abzurufen. Dies ist in app-routing.module.ts unserer Shell definiert:

{ 
	path: 'mfe1App', 
	loadChildren: () => 
    	loadRemoteModule({ 
        	type: 'manifest', 
        	remoteName: 'mfe1App', 
        	exposedModule: './Module', 
    	}).then((m) => m.Mfe1AppModule), 
},

Und in unserer mfe.manifest.json:

{ 
  "mfe1App": "<http://localhost:4001/remoteEntry.js>" 
}

Mit unserer Webpack-Konfiguration von Mfe1App steht nur ein Modul zur Verfügung:

new webpack.container.ModuleFederationPlugin({ 
	name: 'mfe1App', 
	filename: 'remoteEntry.js', 
	exposes: { 
    	'./Module': './src/app/mfe1App/mfe1App.module.ts', 
	}, 
	library: { 
    	type: 'module', 
	}, 
}),

Diese Konfiguration stellt das Modul mfe1App mit der Datei mfe1App.module.ts als Einstiegspunkt und der Datei remoteEntry.js als Datei zum Laden des Moduls bereit. Die Eigenschaft Type wird auf Modul gesetzt, um anzuzeigen, dass das Modul ES-Module verwendet. Und deshalb ist initApp unserer Mfe1App so prägnant – wir initialisieren alles in diesem Modul. Dafür verwenden wir Guards.

Betrachten Sie initMfe1App.guard.ts:

// imports 
@Injectable() 
export class InitMfe1AppGuard implements CanActivate { 
constructor( 
private myMessagesService: myMessagesService, 
private configService: AppConfigService, 
private authService: AuthService, 
private widgetServive: WidgetService, 
private mfe1AppService: mfe1AppService, 
//... 
@Optional() private myModulesService: myModulesService, 
) {} 
public canActivate( 
	route: ActivatedRouteSnapshot, 
	state: RouterStateSnapshot, 
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { 
	this.mfe1AppService.createPrimaryUrlFromRouteSnapshot(route); 
	if (this.widgetServive.loaded) { 
    	return true; 
	} 
	return this.myMessagesService.messages$.pipe( 
    	filter((message: WMessage): message is WMessageWidgetLoaded | WGlobalConfigChanged => { 
        	if ( 
            	checkMyMessageType(message, W_MESSAGE_TYPE.GLOBAL_CONFIG_CHANGED) || 
            	checkMyMessageType(message, W_MESSAGE_TYPE.WIDGET_LOADED) 
        	) { 
            	return true; 
        	} else { 
            	this.myMessagesService.sendMessage({ type: W_MESSAGE_TYPE.GLOBAL_CONFIG_REQUEST }); 
            	return false; 
        	} 
    	}), 
    	take(1), 
    	tap((message) => this.widgetService.load(message.data)), 
    	//... 
    	mapTo(true), 
    	timeout(1000 * 10), 
	); 
}

Dieser Guard ersetzt das gewohnte APP_INITIALIZER Token und bietet ein neues Zuhause für die gesamte Initialisierungslogik.

Sie haben es getan! Sie können Ihre Micro-Frontend-Anwendungen starten. Für Ihre MF-App ist also APP_INITIALIZERfür die eigenständige Initialisierung und init.guard für das MF-Module. Dieser neue Ansatz rationalisiert den Initialisierungsprozess und bietet Entwicklern ein vertrauteres, Angular-ähnliches Erlebnis. Je mehr sich die Dinge ändern, desto mehr bleiben sie wie sie sind, oder?

Aber wie steht es, wenn etwas schief geht?

providedIn: ‚root‘ ist nicht mehr so freundlich

Wenn Sie Ihre Micro-Frontend-Reise beginnen, kann es zu Turbulenzen kommen, insbesondere wenn Ihre Anwendung aufgrund von Injection-Konflikten nicht so reibungslos startet. Dies kann passieren, weil die meisten Ihrer Anbieter in ‚root‘ bereitgestellt wurden, einer beliebten Technik, die im Angular-Bereich weit verbreitet ist.

Auch wenn dieser Ansatz im Allgemeinen immer noch eine gute Praxis ist, kann er in vielen Fällen für Ihre Micro-Frontend-Apps weniger geeignet sein. Insbesondere wenn einige Ihrer Dienste von anderen Diensten und Konfigurationen abhängig sind, die jetzt im App-Init-Guard initialisiert werden, sollten Sie sie auf der Ebene der Micro-Frontend-Anwendung bereitstellen.

Davon abgesehen kann ProvideIn: ‚root‘ immer noch eine praktikable Wahl für globale, nicht konfigurierbare oder wirklich globale Dienste sein. Sie sollten jedoch Ihre analytischen Fähigkeiten nutzen, um Dienstleistungen dort bereitzustellen, wo sie wirklich benötigt werden.

Vielleicht ist es an der Zeit für eine kleine Umstrukturierung – erwägen Sie, einige dieser globalen Hilfsdienste lokal zu organisieren und sie direkt dort in Komponenten einzubinden, wo sie benötigt werden. Dieser Wandel kann die Modularität und Wartbarkeit Ihrer Anwendung verbessern und sie robuster und einfacher navigierbar machen.

Fazit

Die Reise zu einer Mikro-Frontend-Architektur bei Gcore war voller einzigartiger Herausforderungen. Durch diesen Prozess haben wir eine stärkere und flexiblere Grundlage geschaffen, die es den Teams ermöglicht, sich auf die Entwicklung der bestmöglichen Anwendungen zu konzentrieren.

In einer Welt der Mikro-Frontends müssen Teams Änderungen aus gemeinsam genutzten Bibliotheken nur dann übernehmen, wenn sie ihren Anwendungen direkt zugute kommen. Das bedeutet weniger Unterbrechungen und mehr Zeit für Innovation. Diese Freiheit erfordert jedoch eine klare und vereinbarte Integrationsstrategie, um die Kohärenz zwischen verschiedenen Anwendungen aufrechtzuerhalten und die Häufigkeit von Aktualisierungen zu reduzieren.

Unsere Erfahrung zeigt, dass es beim Übergang zu einer Micro-Frontend-Architektur nicht nur um die Überwindung technischer Hürden geht. Es ist ein Sprung hin zu einer modulareren, effizienteren und skalierbareren Art, Frontends zu erstellen.

Es ist wichtig zu beachten, dass die Micro-Frontend-Architektur zwar immer beliebter wird, es sich jedoch nicht um eine Einheitslösung handelt. Genau wie wir sollten Sie die spezifischen Anforderungen Ihrer Situation berücksichtigen und die Vor- und Nachteile abwägen, bevor Sie den Schritt wagen. Nur weil es der neue Trend ist, bedeutet das nicht unbedingt, dass es auch für Ihr Projekt oder Ihre Organisation geeignet ist.

Viel Glück!

Subscribe to our newsletter

Stay informed about the latest updates, news, and insights.