Webpack Module Federation – aber Type-Safe

13 Dez. 2024

4 Minuten Lesezeit

Webpack Module Federation ist ein Ansatz, um Module in modernen Anwendungen zu bündeln und zu teilen. Mit dieser Technik lassen sich Module erstellen, die zur Laufzeit anstelle zur Build-Zeit eingebunden werden. Dies eröffnet völlig neue Möglichkeiten für die Entwicklung von Micro-Frontends und dynamischen Anwendungen.

Problem?

Die Integration von Webpack Module Federation ist prinzipiell einfach, allerdings treten Herausforderungen beim Build der nutzenden Anwendung auf:

Die Integration erfolgt zur Laufzeit durch Laden von JavaScript-Code. Zur Build-Zeit der nutzenden Anwendung fehlen somit für den TypeScript-Transpiler jegliche Typ-Informationen, was zu Fehlern und unsicherem Code führen kann. Um dies zu vermeiden, müssen die Typ-Informationen zur Build-Zeit korrekt eingebunden werden – aber wie genau?

Lösung!

Die Lösung für dieses Problem besteht darin, Type-Definitions als eigene NPM-Bibliothek zu erstellen und als Dev-Dependency in die Projektstruktur zu integrieren.

Doch eine Dependency?

Vielleicht kommt nun direkt ein Gedanke hoch: Jetzt gibt es ja wieder eine Build-Time-Dependency!

Die kurze Antwort darauf ist: Ja – und das ist gut so!

Die etwas längere Antwort ist: Eine Abhängigkeit gibt es in jedem Fall! Die nutzende Anwendung muss mit dem Modul interagieren, was über JavaScript-Calls oder Custom Elements erfolgen kann. In beiden Fällen existiert damit aber eine Schnittstelle, auf die sich beide Teilnehmer einigen müssen. In diesem Beispiel wird die Schnittstelle nun nur explizit und vom Transpiler prüfbar.

Aber wie funktioniert die Lösung?

Generell setzt der Ansatz darauf, dass zur Build-Zeit lediglich Typ-Informationen notwendig sind – jedoch keine Implementierungen. Zur Laufzeit ist alles JavaScript und es sind nur die Implementierungen und Bezeichner relevant.

Type-Definitions als eigene NPM-Bibliothek erstellen

Wir nehmen an, dass wir @viadee/my-library für die Bibliothek verwenden wollen.

Zunächst erstellen wir eine neue NPM-Bibliothek für die Type-Definitions. Für @viadee/my-library wäre das @types/viadee__my-library. Diese Bibliothek wird sämtliche Typdefinitionen für das Modul enthalten. Der Typescript-Transpiler wird für Typ-Informationen automatisch auch in unserer Types-Bibliothek suchen.

Remote-Konfiguration für Webpack

In der Remote-Konfiguration von Webpack sollte der Name so gewählt werden, dass er zur erstellten Type-Definitions Bibliothek passt. Hier sollten wir also @viadee/my-library verwenden.

const HtmlWebpackPlugin = require('html-webpack-plugin'); 
const { ModuleFederationPlugin } = require('webpack').container; 

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'app',
      library: { type: 'var', name: 'app' }, 
      remotes: {
        '@viadee/my-library: 'my-library@http://localhost:3002/remoteEntry.js',
      },
      shared: ['lit-element'] 
    }),
    new HtmlWebpackPlugin({ template: './src/index.html' })
  ]
  // ... 
}

Pfade der Type-Definitions abstimmen

Die Exporte der Module-Federation müssen den Pfaden der Type-Definitions entsprechen. Wenn beispielsweise ein Pfad /some/stuff im Webpack-Export verwendet wird, muss es eine entsprechende Type-Definition /some/stuff.d.ts oder /some/stuff/index.d.ts geben.

const HtmlWebpackPlugin = require('html-webpack-plugin'); 
const { ModuleFederationPlugin } = require('webpack').container; 

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'my-library',
      library: { type: 'var', name: 'my-library' }, 
      filename: 'remoteEntry.js',
      exposes: {
        './some/stuff': './src/some/stuff' // <-- Hier stellen wir den Export bereit
      },
      shared: ['lit-element'] 
    }),
  ]
  // ...
}
{
  "name": "@types/viadee__my-library",
  "version": "1.0.0",
  "files": [
    "./some/stuff.d.ts"   <-- Diese Struktur muss zu dem Export oben passen
    ...
  ]
  ...
}

Type-Definitions als Dev-Dependency importieren

Schließlich wird die Type-Definitions Bibliothek als Dev-Dependency zum Projekt hinzugefügt. Dadurch stehen bei einem Import auch die erforderlichen Typ-Informationen für den Compiler zur Verfügung: 

npm install --save-dev @types/viadee__my-library

Nun können wir die Module mit den entsprechenden Typinformationen importieren:

import { myFunction } from '@viadee/my-library/some/stuff';

Zur Build-Zeit wird lediglich die Typ-Information aus unserer Dev-Dependency herangezogen. Zur Laufzeit wird Webpack für den Import das federated Module laden.

Kleine RahmenbedingunG

Wichtig ist dabei zu beachten, dass das Laden eines federated Modules aufgrund des http-Request asynchron ist. Damit muss der Import auch asynchron sein. Da das obige Beispiel jedoch kein asynchroner Import ist, müssen wir beim Initialisieren der nutzenden Anwendung sicherstellen, dass das Bootstrapping asynchron erfolgt.

D.h., dass alle Importe von Modulen in unserer Anwendung, welche von dieser Lösung Gebrauch machen, durch einen asynchronen Import entkoppelt werden müssen. Die einfachste Lösung dazu ist auch im oben verlinkten Blogpost bereits beschrieben:

Alle Importe aus unserer index.ts wandern in eine bootstrap.ts, die dann aus der index.ts asynchron geladen wird:

import('./bootstrap.ts')

Fazit

Mit diesem Ansatz können wir Webpack Module Federation nutzen und gleichzeitig von den Stärken von TypeScript profitieren. Die Type-Safety bleibt erhalten und die Code-Basis bleibt robust und wartbar.

Vorheriger Artikel

Generative KI im Prozessmanagement: Konsistente Prozessdokumentation dank LLMs

Nächster Artikel

Wie Process Mining Unternehmen zum Erfolg verhilft – Und wie die richtige Umsetzung Fehler vermeidet

Wie Process Mining zum Erfolg verhilft