Nahtloser Code-Austausch in Node.js Microservices mit Module Federation
Daniel Hayes
Full-Stack Engineer · Leapcell

Einleitung
In der sich ständig weiterentwickelnden Landschaft der modernen Softwareentwicklung hat sich die Microservices-Architektur als ein vorherrschendes Muster etabliert und bietet zahlreiche Vorteile wie unabhängige Bereitstellbarkeit, Skalierbarkeit und technologische Vielfalt. Mit der Verbreitung verschiedener Dienste entsteht jedoch häufig eine neue Herausforderung: Wie kann gemeinsamer Code, Dienstprogramme oder Komponenten effektiv über diese Dienste hinweg geteilt werden, ohne auf traditionelle Paketmanagementstrategien zurückzugreifen, die oft zu Problemen mit der Versionsverwaltung oder überflüssigen Abhängigkeiten führen. Dies ist besonders in Node.js-Umgebungen relevant, wo eine gemeinsame Codebasis den Boilerplate-Code erheblich reduzieren, die Konsistenz verbessern und die Entwicklungszyklen beschleunigen kann. Module Federation, das ursprünglich in der Frontend-Welt populär wurde, präsentiert eine innovative und überzeugende Lösung für die Ermöglichung dynamischen Code-Austauschs, selbst im Backend.
Dieser Artikel befasst sich mit der praktischen Anwendung von Module Federation in Node.js Microservices und zeigt, wie es die Zusammenarbeit beim Codeaustausch zwischen Diensten neu definieren kann.
Understanding Module Federation für Node.js Microservices
Bevor wir uns mit den Implementierungsdetails befassen, klären wir einige Kernkonzepte rund um Module Federation und seine Relevanz im Node.js-Kontext.
Kernkonzepte
-
Module Federation: Eine webpack-Funktion, die es einer JavaScript-Anwendung ermöglicht, Code dynamisch aus einer anderen Anwendung zu laden und ihn als Abhängigkeit zu behandeln. Im Gegensatz zum herkömmlichen Abhängigkeitsmanagement werden Federated Modules nicht im Voraus in verbrauchenden Anwendungen gebündelt. Stattdessen werden sie zur Laufzeit verfügbar gemacht und verbraucht. Diese dynamische Verknüpfungsfähigkeit ist der Schlüssel zu seiner Leistungsfähigkeit.
-
Host (Container): Eine Anwendung, die von anderen Anwendungen bereitgestellte Module verbraucht. In einem Microservice-Setup fungiert ein Dienst, der Funktionalitäten eines anderen Dienstes nutzen muss, als Host.
-
Remote (Exposed Module): Eine Anwendung, die einige ihrer Module für andere Anwendungen bereitstellt. Ein Microservice, der gemeinsame Dienstprogramme oder Logik anbietet, wäre ein Remote.
-
Shared Modules: Abhängigkeiten (wie
lodash
,express
,uuid
), die mehreren föderierten Anwendungen gemeinsam sind. Dieshared
-Konfiguration von Module Federation stellt sicher, dass diese Abhängigkeiten nur einmal geladen werden, was die Leistung und Konsistenz verbessert, indem mehrere Instanzen derselben Bibliothek vermieden werden. Dies ist entscheidend für Node.js-Dienste zur effektiven Verwaltung von Abhängigkeiten.
Funktionsprinzip
Im Grunde genommen generiert Module Federation eine spezielle Bundle-Datei namens remoteEntry.js
für jede Remote-Anwendung. Diese Datei fungiert als Manifest, enthält Metadaten über die bereitgestellten Module und ein Bootstrap-Skript zum Laden. Wenn ein Host ein Remote-Modul verbrauchen muss, ruft er diese remoteEntry.js
-Datei dynamisch vom Remote-Dienst ab (in Node.js-Setups oft über HTTP) und führt dann seine Bootstrap-Logik aus, um das angeforderte Modul zu laden. Der Webpack-Runtime-Glue integriert dieses Modul dann in den Modulgraphen der Host-Anwendung und macht es verfügbar, als wäre es eine lokal installierte Abhängigkeit.
Für Node.js Microservices bedeutet dies, dass ein Dienst eine Reihe von Hilfsfunktionen, API-Clients oder sogar ganze Middleware-Stacks bereitstellen kann, und andere Dienste können diese direkt zur Laufzeit verbrauchen. Kein Bedarf an der Veröffentlichung privater npm-Pakete, keine komplexen Monorepo-Setups nur für gemeinsamen Code – nur eine direkte Laufzeitbeziehung.
Implementierung und Anwendung
Lassen Sie uns dies anhand eines praktischen Beispiels veranschaulichen. Stellen wir uns zwei Node.js Microservices vor: einen User Service
und einen Auth Service
. Der Auth Service
kümmert sich um die Benutzerauthentifizierung und generiert JWT-Token. Der User Service
muss diese Token für authentifizierte Anfragen validieren. Anstatt die Token-Validierungslogik zu duplizieren oder sie in eine interne npm-Bibliothek zu verpacken, können wir Module Federation verwenden.
Auth Service (Remote)
Der Auth Service
wird eine Hilfsfunktion validateJwtToken
bereitstellen.
Stellen Sie zunächst sicher, dass Sie webpack 5 installiert und für Node.js konfiguriert haben.
npm install webpack webpack-cli webpack-node-externals @module-federation/node@next
webpack.config.js
für Auth Service:
const { ModuleFederationPlugin } = require('@module-federation/node'); const path = require('path'); const nodeExternals = require('webpack-node-externals'); module.exports = { entry: './src/index.js', target: 'node', mode: 'development', // oder 'production' output: { path: path.resolve(__dirname, 'dist'), filename: 'main.js', libraryTarget: 'commonjs-static', // Wichtig für Node.js }, externals: [nodeExternals()], // node_modules vom Bündel ausschließen plugins: [ new ModuleFederationPlugin({ name: 'authServiceRemote', filename: 'remoteEntry.js', // Diese Datei wird von Hosts abgerufen exposes: { './tokenValidator': './src/utils/tokenValidator.js', // Bereitstellen eines Dienstprogramms }, shared: { // Gemeinsame Abhängigkeiten, um Duplizierung zu vermeiden. // Webpack stellt sicher, dass nur eine Instanz geladen wird. jsonwebtoken: { singleton: true, requiredVersion: '^8.5.1', }, dotenv: { singleton: true, requiredVersion: '^16.0.0', }, }, }), ], };
src/utils/tokenValidator.js
im Auth Service:
const jwt = require('jsonwebtoken'); require('dotenv').config(); const JWT_SECRET = process.env.JWT_SECRET || 'supersecretkey'; function validateJwtToken(token) { try { const decoded = jwt.verify(token, JWT_SECRET); return { isValid: true, user: decoded }; } catch (error) { return { isValid: false, error: error.message }; } } module.exports = { validateJwtToken };
Der Auth Service
würde dann starten und typischerweise seine remoteEntry.js
-Datei unter einem bekannten Endpunkt (z. B. http://localhost:3001/remoteEntry.js
) bereitstellen. Dies bedeutet normalerweise, dass der dist
-Ordner direkt bedient wird oder in eine vorhandene Express-App integriert wird.
User Service (Host)
Der User Service
wird die Funktion validateJwtToken
vom Auth Service
verbrauchen.
webpack.config.js
für User Service:
const { ModuleFederationPlugin } = require('@module-federation/node'); const path = require('path'); const nodeExternals = require('webpack-node-externals'); module.exports = { entry: './src/index.js', target: 'node', mode: 'development', output: { path: path.resolve(__dirname, 'dist'), filename: 'main.js', libraryTarget: 'commonjs-static', }, externals: [nodeExternals()], plugins: [ new ModuleFederationPlugin({ name: 'userServiceHost', remotes: { // Zeigt auf die remoteEntry.js des Auth Service authServiceRemote: 'authServiceRemote@http://localhost:3001/remoteEntry.js', }, shared: { jsonwebtoken: { singleton: true, requiredVersion: '^8.5.1', }, dotenv: { singleton: true, requiredVersion: '^16.0.0', }, express: { singleton: true, requiredVersion: '^4.17.1', }, }, }), ], };
src/index.js
im User Service:
const express = require('express'); const { validateJwtToken } = require('authServiceRemote/tokenValidator'); // Verbrauch des Remote-Moduls const app = express(); app.use(express.json()); // Middleware zum Schutz von Routen mit dem föderierten Token-Validator app.use(async (req, res, next) => { const authHeader = req.headers.authorization; if (!authHeader) { return res.status(401).send('Kein Autorisierungs-Header'); } const token = authHeader.split(' ')[1]; if (!token) { return res.status(401).send('Token nicht bereitgestellt'); } // Verwenden Sie die föderierte Funktion! const validationResult = await validateJwtToken(token); if (validationResult.isValid) { req.user = validationResult.user; // Benutzerinformationen an die Anfrage anhängen next(); } else { res.status(403).send(`Ungültiges Token: ${validationResult.error}`); } }); app.get('/profile', (req, res) => { res.json({ message: `Willkommen ${req.user.username}! Dies ist Ihr Profil.`, user: req.user }); }); const PORT = 3000; app.listen(PORT, () => { console.log(`User Service läuft auf Port ${PORT}`); });
Wenn der User Service
startet, versucht er, authServiceRemote/tokenValidator
dynamisch von http://localhost:3001/remoteEntry.js
zu laden. Dies ermöglicht es dem User Service
, die Logik des Auth Service
direkt zu nutzen, ohne jsonwebtoken
oder dotenv
zweimal zu bündeln oder ein vorab veröffentlichtes token-validator
-Paket zu benötigen.
Vorteile und Anwendungsfälle
- Reduzierte Duplizierung: Zentralisieren Sie gemeinsame Logik (z. B. Validierungsschemata, spezifische Algorithmen, API-Clients) und teilen Sie sie über Dienste hinweg.
- Verbesserte Konsistenz: Stellen Sie sicher, dass alle Dienste dieselbe Version gemeinsamer Komponenten oder Dienstprogramme verwenden, wodurch "Drift" und potenzielle Fehler aufgrund unterschiedlicher Implementierungen reduziert werden.
- Schnellere Entwicklung: Entwickler erstellen ein Dienstprogramm einmal und stellen es bereit; andere Teams können es sofort nutzen, ohne auf die Paketveröffentlichung oder komplexe Synchronisationen warten zu müssen.
- Unabhängige Bereitstellung: Dienste können ihre bereitgestellten Module unabhängig voneinander aktualisieren. Solange die API-Schnittstelle stabil bleibt, müssen die Verbraucher nicht erneut bereitgestellt werden.
- Dynamische Aktualisierung: Theoretisch könnte ein Host seine Remote-Konfiguration aktualisieren, um auf eine neue Version eines Remote-Dienstes zu verweisen, ohne dass eine vollständige Neuentwicklung des Hosts selbst erforderlich ist, obwohl dies eine sorgfältige Verwaltung von Versionierung und nicht abwärtskompatiblen Änderungen erfordert.
- Alternative zu Monorepos: Bietet eine Möglichkeit, Code über Dienste hinweg zu teilen, ohne an ein Monorepo-Setup gebunden zu sein, und ermöglicht es Diensten, in separaten Repositories zu residieren.
Überlegungen und Herausforderungen
- Laufzeitabhängigkeit: Führt eine Laufzeitabhängigkeit von der Verfügbarkeit und Reaktionsfähigkeit des Remote-Servers ein. Wenn der Remote-Dienst (oder sein
remoteEntry.js
) nicht verfügbar ist, kann der Host möglicherweise seine föderierten Module nicht laden. - Versionierung und Kompatibilität: Obwohl
shared
Module helfen, erfordert die Verwaltung nicht abwärtskompatibler Änderungen in bereitgestellten Remote-Modulen weiterhin Disziplin. Semantisches Versioning von Remote-Modulen ist entscheidend. - Build-Komplexität: Erhöht den Konfigurationsaufwand für webpack für jeden Dienst.
- Fehlersuche: Die Fehlersuche bei Problemen zwischen föderierten Modulen kann komplexer sein als bei herkömmlichen lokalen Abhängigkeiten.
- Sicherheit: Stellen Sie sicher, dass die
remoteEntry.js
-Dateien sicher bereitgestellt und aus vertrauenswürdigen Quellen bezogen werden, um bösartige Codeinjektionen zu verhindern.
Fazit
Module Federation bietet eine leistungsstarke und elegante Lösung für den effizienten Code-Austausch in Node.js Microservices. Durch die Ermöglichung des dynamischen Ladens von Modulen zur Laufzeit zwischen verschiedenen Diensten verbessert es die Wartbarkeit erheblich, reduziert redundanten Code und beschleunigt die Entwicklung. Obwohl es neue Überlegungen zu Laufzeitabhängigkeiten und Versionierung mit sich bringt, machen die Vorteile in Bezug auf Flexibilität und abteilungsübergreifende Zusammenarbeit es zu einem überzeugenden Architektursystem, das es für moderne Node.js-Backends zu erkunden lohnt. Im Wesentlichen ermöglicht Module Federation Microservices, wirklich als ein zusammenhängendes, aber unabhängig bereitstellbares Ökosystem zu agieren.