ES-Module in Node.js navigieren – Ein Leitfaden für modernes JavaScript
James Reed
Infrastructure Engineer · Leapcell

Einleitung: Die Dämmerung moderner JavaScript-Module in Node.js
Lange Zeit fehlte dem JavaScript als Sprache ein natives Modulsystem. Diese Lücke wurde durch verschiedene Lösungen gefüllt, allen voran CommonJS in der Node.js-Umgebung und AMD/RequireJS im Browser. Während CommonJS Node.js jahrelang hervorragend diente, brachte die Einführung von ES-Modulen (ESM) als offiziellem, standardisiertem Modulsystem für JavaScript ein neues Paradigma. Mit ESM strebte JavaScript ein einheitliches Modulsystem an, das nahtlos sowohl in Client- als auch in Serversegmenten funktioniert.
Die Integration von ESM in Node.js war jedoch keine einfache Ablösung. Node.js verfügt über ein riesiges Ökosystem, das auf CommonJS basiert, und die Gewährleistung der Abwärtskompatibilität bei gleichzeitiger Ausrichtung auf die Zukunft erforderte sorgfältige Überlegungen. Dieser Übergang hat zu einem dualen Modulsystem geführt, das in Node.js nebeneinander existiert und den Entwicklern Entscheidungen und manchmal auch Herausforderungen bietet. Das Verständnis der grundlegenden Unterschiede zwischen CommonJS und ESM sowie der Strategien zur Migration bestehender Projekte ist für jeden Node.js-Entwickler, der moderne, zukunftssichere JavaScript-Anwendungen schreiben möchte, unerlässlich. Dieser Artikel wird diese Unterschiede untersuchen, praktische Beispiele liefern und Sie durch den Prozess der Einführung von ESM in Ihren Node.js-Projekten führen.
JavaScript-Module verstehen: CommonJS vs. ES-Module
Bevor wir uns eingehend mit den Nuancen befassen, wollen wir ein klares Verständnis der Kernkonzepte und Mechanismen hinter CommonJS und ES-Modulen schaffen.
Kernterminologie
- Modulsystem: Ein Mechanismus zur Organisation von Code in separate, wiederverwendbare Dateien (Module) und zur Definition, wie diese Module interagieren (Funktionalitäten importieren und exportieren).
- CommonJS (CJS): Das langjährige Standard-Modulsystem in Node.js. Es verwendet
require()
zum Importieren von Modulen undmodule.exports
oderexports
zum Exportieren von Funktionalitäten. - ES-Module (ESM): Das offizielle, standardisierte Modulsystem für JavaScript, spezifiziert durch ECMAScript. Es verwendet
import
- undexport
-Anweisungen. package.json
type
-Feld: Ein Feld inpackage.json
, das angibt, wie Node.js Dateien innerhalb eines Pakets interpretieren soll. Die Einstellung auf"module"
behandelt.js
-Dateien als ESM, während"commonjs"
(oder dessen Fehlen) sie als CommonJS behandelt.- Dateiendungen: Node.js verwendet Dateiendungen, um das Modulsystem abzuleiten.
.mjs
-Dateien werden als ESM behandelt, und.cjs
-Dateien werden als CommonJS behandelt, unabhängig vompackage.json
type
-Feld. - Dual Package Hazard: Eine Situation, in der ein Paket sowohl CommonJS- als auch ESM-Versionen bereitstellt, was zu Inkonsistenzen oder Fehlern führen kann, wenn beide im selben Anwendungskontext für verschiedene Modulsysteme geladen werden.
CommonJS: Der traditionelle Node.js-Ansatz
CommonJS arbeitet synchron, d. h. wenn Sie ein Modul require()
n, blockiert Node.js die Ausführung, bis dieses Modul geladen und ausgewertet wurde. Diese synchrone Natur ist generell für Serverumgebungen geeignet, in denen die Dateiein-/ausgabe nicht so kritisch ist wie in einem Browser.
Exportieren in CommonJS:
Sie exportieren Funktionalitäten mit module.exports
oder exports
.
// math.js (CommonJS) function add(a, b) { return a + b; } const subtract = (a, b) => a - b; module.exports = { add, subtract }; // Oder direkt auf exports zuweisen // exports.add = add; // exports.subtract = subtract;
Importieren in CommonJS:
Sie importieren Module mit der require()
-Funktion.
// app.js (CommonJS) const math = require('./math'); console.log(math.add(5, 3)); // Ausgabe: 8 console.log(math.subtract(10, 4)); // Ausgabe: 6
ES-Module: Der moderne Standard
ES-Module sind asynchron konzipiert, was eine bessere Optimierung und nicht-blockierendes Laden ermöglicht, was für Webbrowser entscheidend ist. In Node.js, obwohl sie synchron vom Dateisystem geladen werden, folgen ihre interne Auflösung und Auswertung einer asynchronen Graphentraversierung. ESM verfügt über statische Analyse, was bedeutet, dass Imports/Exports zur Analysezeit bestimmt werden, was Funktionen wie Tree-Shaking und bessere Tooling-Unterstützung ermöglicht.
Exportieren in ESM:
Sie exportieren Funktionalitäten mit dem Schlüsselwort export
.
// math.mjs (ESM) oder math.js mit "type": "module" in package.json export function add(a, b) { return a + b; } export const subtract = (a, b) => a - b; // Default-Export // export default function multiply(a, b) { // return a * b; // }
Importieren in ESM:
Sie importieren Module mit dem Schlüsselwort import
.
// app.mjs (ESM) oder app.js mit "type": "module" in package.json import { add, subtract } from './math.mjs'; // Dateiendung muss angegeben werden console.log(add(5, 3)); // Ausgabe: 8 console.log(subtract(10, 4)); // Ausgabe: 6 // Import eines Standardexports // import multiply from './math.mjs'; // console.log(multiply(2, 3)); // Ausgabe: 6 // Dynamischer Import (asynchron) async function doCalculations() { const { add } = await import('./math.mjs'); console.log('Dynamische Addition:', add(10, 5)); } doCalculations();
Hauptunterschiede im Überblick
Merkmal | CommonJS (CJS) | ES-Module (ESM) |
---|---|---|
Syntax | require() , module.exports , exports | import , export |
Laden | Synchron | Asynchron (statische Graphenanalyse) |
Binding | Live-Kopie der exportierten Werte | Live-Bindung (Referenzen, keine Kopien) |
this auf oberster Ebene | module.exports | undefined |
Dateiendungen | .js (Standard), .cjs | .mjs , .js (mit "type": "module" ) |
__dirname , __filename | Verfügbar | Nicht direkt verfügbar, import.meta.url für Pfade verwenden |
package.json type | Standard (oder "commonjs" ) | "module" |
Dynamische Imports | Nicht nativ, erfordert oft Bündelung/Babel | Native import() -Funktion (liefert Promise) |
Interoperabilität: Überbrückung der Lücke
Node.js bietet Mechanismen, um ein gewisses Maß an Interoperabilität zwischen CJS und ESM zu ermöglichen.
1. ESM importiert CJS:
Ein ESM-Modul kann ein CommonJS-Modul import
en. Die import
-Anweisung behandelt module.exports
des CommonJS-Moduls als seinen Standardexport. Benannte Exporte sind nicht direkt verfügbar, es sei denn, das CommonJS-Modul weist sie explizit als Eigenschaften an module.exports
zu.
// commonjs-lib.cjs module.exports = { foo: 'bar', baz: () => 'qux' }; // esm-app.mjs (oder esm-app.js mit "type": "module") import cjsLib from './commonjs-lib.cjs'; // Standard-Import console.log(cjsLib.foo); // Ausgabe: bar console.log(cjsLib.baz()); // Ausgabe: qux
2. CJS fordert ESM an:
Standardmäßig können CommonJS-Module ES-Module nicht direkt require()
n. Dies ist eine signifikante Einschränkung. Die require()
-Funktion ist synchron und kann die asynchrone Natur der ESM-Auflösung nicht verarbeiten. Wenn Sie dies versuchen, erhalten Sie wahrscheinlich eine Fehlermeldung wie ERR_REQUIRE_ESM
.
Um ein ESM von einem CJS-Modul require()
n, müssen Sie typischerweise einen dynamischen import()
-Aufruf im CJS-Kontext verwenden, der ein Promise zurückgibt. Dadurch wird der require
-Aufruf effektiv asynchron.
// esm-lib.mjs export const greeting = 'Hallo von ESM!'; // commonjs-app.cjs async function loadESM() { const esm = await import('./esm-lib.mjs'); console.log(esm.greeting); // Ausgabe: Hallo von ESM! } loadESM();
Dies wird generell für synchrone CJS-Abhängigkeiten nicht empfohlen und deutet darauf hin, dass das CJS-Modul in ESM umgeschrieben werden müsste, falls dies eine häufige Abhängigkeit ist.
Node.js Modulauflösungsstrategie
Node.js ermittelt, ob eine Datei entweder CJS oder ESM ist, basierend auf den folgenden Regeln, in Reihenfolge der Priorität:
- Dateiendungen:
.mjs
ist immer ESM..cjs
ist immer CJS. package.json
type
-Feld: Wenn einepackage.json
-Datei"type": "module"
deklariert, werden.js
-Dateien innerhalb dieses Pakets (und seiner Unterverzeichnisse, sofern nicht durch eine anderepackage.json
überschrieben) als ESM behandelt. Wenn"type": "commonjs"
(oder nicht vorhanden), sind.js
-Dateien CJS.- Elterliches Modultyp: Wenn eine Datei von einem Modul eines bestimmten Typs importiert wird (z. B. eine
.mjs
-Datei importiert eine.js
-Datei), kann Node.js den Typ der zu importierenden Datei ableiten, wenn keine anderen Regeln gelten. Sich darauf zu verlassen ist jedoch weniger explizit und kann verwirrend sein.
Der robusteste Weg zur Verwaltung von Modultypen ist die Verwendung expliziter Dateiendungen (.mjs
, .cjs
) oder des package.json
type
-Feldes.
Migrationsstrategien zu ES-Modulen
Die Migration einer CJS-Codebasis zu ESM kann ein schrittweiser Prozess sein. Hier sind gängige Strategien, um diesen Übergang zu erleichtern.
1. Inkrementelles Vorgehen mit .mjs
-Dateien
Dies ist oft der sicherste Ausgangspunkt für ein großes bestehendes CJS-Projekt.
- Strategie: Beginnen Sie damit, neue Module als ESM zu schreiben, indem Sie ihnen die
.mjs
-Endung geben. Behalten Sie bestehende CJS-Module bei. - Vorteile: Minimale Störung bestehenden Codes. Ermöglicht die schrittweise Einführung von ESM.
- Nachteile: Sie müssen zwei Modulsysteme gleichzeitig verwalten. ESM-Module können CJS-Module importieren, aber CJS-Module können
.mjs
-Module nicht direktrequire()
n (ohne dynamischeimport()
). - Wann verwenden: Wenn Sie eine stabile, große CJS-Codebasis haben und ESM für neue Funktionen oder Refactorings einführen möchten, ohne eine komplette Neubearbeitung durchzuführen.
Beispiel:
// package.json (kein "type"-Feld bedeutet CommonJS für .js-Dateien)
{
"name": "my-app",
"version": "1.0.0"
}
// old-commonjs-util.js
module.exports = {
greeting: "Hallo von altem CJS!"
};
// new-esm-feature.mjs
import { greeting } from './old-commonjs-util.js'; // ESM importiert CJS
console.log(greeting);
export function getFeatureData() {
return "Neue ESM-Daten";
}
// app.js (CommonJS-Einstiegspunkt)
const { getFeatureData } = require('./new-esm-feature.mjs'); // Dies wird fehlschlagen! CJS kann ESM nicht direkt require()n.
// Eine dynamische Importierung wäre hier erforderlich:
async function run() {
const { getFeatureData: esmFeature } = await import('./new-esm-feature.mjs');
console.log(esmFeature());
}
run();
Dieses Beispiel verdeutlicht die Herausforderung: Obwohl ESM CJS importieren kann, kann CJS keine ESM direkt mit require()
n, was eine Umstellung auf asynchrone import()
-Aufrufe oder eine vollständige Konvertierung erzwingt.
2. Übernahme von "type": "module"
für ein Paket
Dies ist ein umfassenderer Ansatz für Pakete oder Anwendungen, die ESM vollständig übernehmen möchten.
- Strategie: Setzen Sie
"type": "module"
in Ihrerpackage.json
. Dies bewirkt, dass alle.js
-Dateien in diesem Paket (und seinen Unterordnern) standardmäßig als ESM behandelt werden. - Vorteile: Vereinfacht den Code, macht
import/export
zum Standard. Fördert die vollständige Akzeptanz von ESM. - Nachteile: Erfordert entweder die vollständige Konvertierung aller
.js
-Dateien in ESM-Syntax oder die explizite Kennzeichnung von CJS-Dateien mit der.cjs
-Erweiterung. - Wann verwenden: Für neue Projekte oder aktiv gepflegte Projekte, die für eine vollständige Umstellung bereit sind.
Beispiel:
// package.json
{
"name": "my-esm-app",
"version": "1.0.0",
"type": "module" // Alle .js-Dateien sind jetzt ESM
}
// esm-math.js (wird als ESM behandelt)
export function add(a, b) {
return a + b;
}
// esm-app.js (wird als ESM behandelt)
import { add } from './esm-math.js';
console.log(add(10, 20));
// Wenn Sie noch CJS-Abhängigkeiten haben, kennzeichnen Sie diese mit .cjs:
// legacy-cjs-module.cjs
module.exports = {
getConfig: () => ({ version: '1.0' })
};
// esm-app.js
import { add } from './esm-math.js';
import cjsConfig from './legacy-cjs-module.cjs'; // ESM importiert CJS .cjs-Datei
console.log(add(10, 20));
console.log(cjsConfig.getConfig());
3. Umgang mit __dirname
und __filename
In ESM sind __dirname
und __filename
nicht direkt verfügbar. Sie können ähnliche Pfade mit import.meta.url
konstruieren.
// esm-module.mjs oder esm-module.js mit "type": "module" import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); console.log('Aktueller Dateipfad:', __filename); console.log('Aktueller Verzeichnispfad:', __dirname);
4. Migration von Drittanbieter-Abhängigkeiten
Dies ist oft der schwierigste Teil.
-
ESM-kompatible Versionen identifizieren: Prüfen Sie, ob Ihre Abhängigkeiten ESM-Versionen veröffentlicht haben. Viele beliebte Bibliotheken bieten mittlerweile sowohl CJS- als auch ESM-Builds.
-
Bedingte Exporte (
exports
-Feld inpackage.json
): Viele Bibliotheken verwenden dasexports
-Feld inpackage.json
, um bedingte Exporte zu definieren, die es ihnen ermöglichen, verschiedene Modulversionen basierend auf der importierenden Umgebung bereitzustellen (z. B."import"
für ESM,"require"
für CommonJS). So erkennt Node.js intelligent, welche Version eines Pakets geladen werden soll.// Beispiel für package.json einer Abhängigkeit { "name": "my-library", "main": "./dist/cjs/index.js", // CommonJS-Einstiegspunkt "module": "./dist/esm/index.js", // ES-Modul-Einstiegspunkt (ältere Methode) "exports": { ".": { "import": "./dist/esm/index.js", "require": "./dist/cjs/index.js" } } }
Ihre Anwendung, falls sie ESM ist, wird automatisch die in
"import"
definierten Exporte übernehmen. -
Transpilierung (mit Babel/TypeScript): Für Pakete, die noch nicht ESM-fähig sind, können Sie sie weiterhin wie CommonJS-Pakete
import
en (wie sie behandelt werden). Wenn Sie aus Ihrem Projekt eine reine ESM-Ausgabe erzeugen müssen, aber CJS-Abhängigkeiten verwenden, kann Ihr Build-Tool (wie Rollup oder Webpack mit entsprechenden Loadern) CJS-Abhängigkeiten oft in Ihre ESM-Ausgabe transpilieren/bündeln. -
Dynamischer
import()
für reine CJS-Abhängigkeiten: Wenn ein ESM-Modul eine reine CJS-Abhängigkeit verwenden muss, die keine ESM-Entsprechung anbietet, können Sie dynamischimport()
verwenden, um sie zu laden. Dies ist jedoch asynchron und hauptsächlich nützlich für Situationen, in denen die Abhängigkeit bei Bedarf geladen werden kann.// esm-app.mjs async function init() { const moment = await import('moment'); // 'moment' ist generell nur für CJS verfügbar console.log(moment().format('LLL')); } init();
Fazit: Die Zukunft der JavaScript-Module umarmen
Der Übergang zu ES-Modulen in Node.js bedeutet einen wichtigen Schritt hin zu einem einheitlicheren und moderneren JavaScript-Ökosystem. Obwohl die Koexistenz von CommonJS und ESM anfängliche Komplexitäten mit sich bringt, ermöglicht das Verständnis ihrer Unterschiede, der Auflösungsmechanismen von Node.js und praktischer Migrationsstrategien den Entwicklern, robustere und zukunftssichere Anwendungen zu erstellen. Durch die schrittweise Einführung von ESM, die Nutzung korrekter Dateiendungen und package.json
-Konfigurationen sowie die sorgfältige Verwaltung der Interoperabilität können Entwickler diese Evolution reibungslos bewältigen und letztendlich von den verbesserten Ergonomie-, statischen Analysefähigkeiten und der Standardisierung profitieren, die ES-Module für die Node.js-Entwicklung mit sich bringen. Die Reise zu einem vollständig ESM-nativen Node.js ist noch im Gange, aber der Weg ist klar und gut unterstützt.