Verständnis von Modulsystemen in Node.js
Lukas Schneider
DevOps Engineer · Leapcell

Einleitung
Module sind ein grundlegender Bestandteil der modernen JavaScript-Entwicklung. Sie ermöglichen es Entwicklern, Code in wiederverwendbare Einheiten zu organisieren und Abhängigkeiten effizient zu verwalten. Lange Zeit diente CommonJS (CJS) als De-facto-Standard für serverseitiges JavaScript in Node.js und erwies sich für zahlreiche Projekte als robust und effektiv. Mit der Standardisierung von ECMAScript Modules (ESM) im Browser und schließlich in Node.js hat sich die Landschaft der Modulverwaltung jedoch erheblich verschoben. Diese Entwicklung bringt leistungsstarke neue Funktionen und ein einheitlicheres Modulsystem im gesamten JavaScript-Ökosystem mit sich, birgt aber auch Komplexitäten und Entscheidungspunkte für Entwickler. Das Verständnis der Nuancen zwischen CJS und ESM sowie der Strategien zur Navigation ihrer Interoperabilität ist entscheidend für die Erstellung wartbarer und leistungsstarker Node.js-Anwendungen heute. Dieser Artikel befasst sich mit diesen Unterschieden, untersucht Strategien für die Arbeit mit beiden Systemen und bietet praktische Einblicke für Ihre Projekte.
Kernkonzepte
Bevor wir uns mit den Einzelheiten befassen, definieren wir die Kernbegriffe im Zusammenhang mit Modulsystemen in Node.js.
CommonJS (CJS)
CommonJS ist eine Modulspezifikation, die hauptsächlich in Node.js verwendet wird. Es handelt sich um ein synchrones Modulladesystem, was bedeutet, dass die Laufzeitumgebung wartet, bis das Modul geladen und analysiert wurde, bevor die Ausführung fortgesetzt wird, wenn ein Modul angefordert wird.
require()
: Die Funktion zum Importieren von Modulen. Sie nimmt den Modulpfad als Argument und gibt dasexports
-Objekt dieses Moduls zurück.module.exports
: Ein Objekt, das zum Exportieren von Werten aus einem Modul verwendet wird. Standardmäßig ist es ein leeres Objekt{}
. Wenn Siemodule.exports
zuweisen, legen Sie den gesamten Export des Moduls fest.exports
: Eine Referenz aufmodule.exports
. Sie könnenexports
Eigenschaften hinzufügen, um mehrere Werte verfügbar zu machen.
ES Modules (ESM)
ES Modules (auch bekannt als JavaScript Modules oder ES6 Modules) sind der offizielle Standard für Module in ECMAScript. Sie verfügen über einen statischen, asynchronen Lademechanismus und sind für die Arbeit sowohl in Browsern als auch in Node.js konzipiert.
import
: Die Anweisung zum Importieren von Modulen. Sie kann für benannte Importe (import { name } from './module'
) oder Standardimporte (import defaultExport from './module'
) verwendet werden.export
: Die Anweisung zum Exportieren von Werten aus einem Modul. Sie kann für benannte Exporte (export const name = 'value'
) oder Standardexporte (export default value
) verwendet werden.- Statische Analyse: ESM-Importe und -Exporte können zur Analysezeit statisch analysiert werden, was Tree-Shaking und eine bessere Tool-Unterstützung ermöglicht.
- Asynchrones Laden: ESM-Module sind für das asynchrone Laden konzipiert, was für die Leistung in Webbrowsern entscheidend ist.
package.json
type
-Feld
Node.js verwendet das type
-Feld in package.json
, um zu bestimmen, ob Dateien in einem Paket als CJS oder ESM interpretiert werden sollen.
"type": "commonjs"
: Alle.js
-Dateien (und Dateien ohne Erweiterung) werden als CJS behandelt."type": "module"
: Alle.js
-Dateien (und Dateien ohne Erweiterung) werden als ESM behandelt.
Unabhängig vom type
-Feld werden .mjs
-Dateien immer als ESM und .cjs
-Dateien immer als CJS behandelt. Dies bietet eine Möglichkeit, Modultypen explizit abzugrenzen.
Unterschiede und Interoperabilität
Die Kernunterschiede zwischen CJS und ESM ergeben sich aus ihren Designphilosophien und der Art und Weise, wie sie Laden, Syntax und Geltungsbereich verwalten.
Hauptunterschiede
-
Syntax:
- CJS: Verwendet
require()
für Importe undmodule.exports
oderexports
für Exporte. - ESM: Verwendet
import
für Importe undexport
für Exporte.
CJS-Beispiel:
// math.js function add(a, b) { return a + b; } module.exports = { add }; // app.js const { add } = require('./math'); console.log(add(2, 3)); // 5
ESM-Beispiel:
// math.mjs export function add(a, b) { return a + b; } // app.mjs import { add } from './math.mjs'; console.log(add(2, 3)); // 5
- CJS: Verwendet
-
Laden Mechanismus:
- CJS: Synchrones Laden.
require()
blockiert die Ausführung, bis das Modul geladen ist. Das ist in serverseitigen Umgebungen, in denen die Dateiein- und -ausgabe schnell erfolgt, in Ordnung. - ESM: Asynchrones Laden.
import
-Anweisungen werden zuerst verarbeitet, bevor der Code des Moduls ausgeführt wird. Dies ermöglicht paralleles Laden und ist für Webumgebungen optimiert.
- CJS: Synchrones Laden.
-
Binding vs. Werte:
- CJS: Exporte sind Kopien von Werten. Wenn sich ein exportierter Wert im exportierenden Modul ändert, sieht das importierende Modul den aktualisierten Wert nicht.
- ESM: Exporte sind Live-Bindings. Wenn sich ein exportierter Wert im exportierenden Modul ändert, sieht das importierende Modul den aktualisierten Wert. Dies ist besonders nützlich für Dinge wie Instanzen von Klassen oder Konfigurationsobjekte, die geändert werden könnten.
ESM Live-Binding-Beispiel:
// counter.mjs export let count = 0; export function increment() { count++; } // app.mjs import { count, increment } from './counter.mjs'; console.log(count); // 0 increment(); console.log(count); // 1 (Live-Binding)
CJS-Wertkopie-Beispiel:
// counter.js let count = 0; function increment() { count++; } module.exports = { count, increment }; // app.js const { count, increment } = require('./counter'); console.log(count); // 0 increment(); console.log(count); // 0 (Kopierter Wert, kein Live-Binding)
-
this
Kontext:- CJS: Auf der obersten Ebene eines CJS-Moduls bezieht sich
this
aufmodule.exports
. - ESM: Auf der obersten Ebene eines ESM-Moduls ist
this
undefined
.
- CJS: Auf der obersten Ebene eines CJS-Moduls bezieht sich
-
Dateierweiterungen:
- CJS: Verwendet typischerweise
.js
(es sei denn,type: "module"
ist gesetzt). - ESM: Kann explizit
.mjs
verwenden oder.js
, wenntype: "module"
inpackage.json
gesetzt ist.
- CJS: Verwendet typischerweise
-
__dirname
und__filename
:- CJS: Diese globalen Variablen sind direkt verfügbar, um den Namen des aktuellen Verzeichnisses und den Dateinamen bereitzustellen.
- ESM: Diese sind nicht direkt verfügbar. Sie müssen sie mithilfe von
import.meta.url
konstruieren.
ESM-Äquivalent von
__dirname
und__filename
:import { fileURLToPath } from 'url'; import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); console.log(__filename); console.log(__dirname);
Interoperabilitätsstrategien
Node.js bietet Mechanismen, um CJS und ESM innerhalb desselben Projekts zusammen zu verwenden.
-
Importieren von CJS aus ESM: ESM-Module können CJS-Module direkt
import
ieren. Derdefault
-Export des CJS-Moduls wird zum importierten Wert. Benannte Exporte aus CJS-Modulen sind nicht direkt zugänglich; Sie müssen das gesamte Modul importieren und es dann dekonstruieren.// cjs-module.js module.exports = { greet: 'Hello CJS!', sayHi: () => 'Hi CJS!' }; // esm-app.mjs (oder .js mit \"type\": \"module\") import cjsModule from './cjs-module.js'; // Importiert das gesamte Exports-Objekt console.log(cjsModule.greet); // Hello CJS! console.log(cjsModule.sayHi()); // Hi CJS! // Direkter benannter Import von CJS wird nicht unterstützt // import { greet } from './cjs-module.js'; // Dies führt zu einem Fehler
-
Anfordern von ESM aus CJS (Experimentell/Indirekt): Ein ESM-Modul direkt von einem CJS-Modul mit
require()
zurequire()
ist von Node.js nicht standardmäßig unterstützt.require()
ist synchron, während das ESM-Laden asynchron ist.Um dies zu erreichen, müssen Sie eine dynamische
import()
-Funktion innerhalb eines CJS-Moduls verwenden, die ein Promise zurückgibt. Dies geschieht normalerweise in einerasync
-Funktion.// esm-module.mjs export const message = 'Hello ESM from CJS!'; // cjs-app.js async function run() { const esmModule = await import('./esm-module.mjs'); console.log(esmModule.message); // Hello ESM from CJS! // Für Standardexporte wäre es esmModule.default } run();
-
Gemischte Pakete (Dual Package Hazard): Beim Veröffentlichen eines Pakets, das sowohl CJS als auch ESM unterstützt, stehen Sie vor der "Dual Package Hazard". Dabei können verschiedene Teile einer Anwendung unterschiedliche Instanzen des Pakets laden, was zu unerwartetem Verhalten führt (z. B. werden Singleton-Klassen zu mehreren Instanzen).
Um dies zu mildern, stellen Bibliotheken oft separate Einstiegspunkte über das
exports
-Feld inpackage.json
bereit:{ "name": "my-package", "main": "./lib/cjs/index.js", "module": "./lib/esm/index.mjs", "exports": { ".": { "import": "./lib/esm/index.mjs", "require": "./lib/cjs/index.js" }, "./package.json": "./package.json" }, "type": "commonjs" }
Mit dieser Konfiguration:
- Wenn ein ESM-Modul
my-package
importiert, wirdlib/esm/index.mjs
geladen. - Wenn ein CJS-Modul
my-package
erfordert, wirdlib/cjs/index.js
geladen.
Dies stellt sicher, dass der korrekte Modultyp entsprechend dem Kontext geladen wird.
- Wenn ein ESM-Modul
Anwendungsszenarien und Best Practices
- Neue Projekte: Im Allgemeinen sollten neue Node.js-Projekte ESM bevorzugen. Es ist der Standard, bietet Vorteile bei der statischen Analyse und passt zum breiteren JavaScript-Ökosystem. Setzen Sie
"type": "module"
in Ihrerpackage.json
. - Bestehende CJS-Projekte: Die Migration eines großen CJS-Projekts zu ESM kann eine erhebliche Aufgabe sein. Die inkrementelle Annahme mithilfe der Interoperabilitätsfunktionen ist oft der praktischste Ansatz. Konvertieren Sie schrittweise Teile Ihrer Codebasis zu ESM, insbesondere neue Funktionalitäten.
- Bibliotheksentwicklung: Wenn Sie eine Bibliothek erstellen, sollten Sie unbedingt beide, CJS und ESM, mithilfe des
exports
-Feldes inpackage.json
unterstützen, um die breiteste Zielgruppe zu erreichen und die Dual Package Hazard zu vermeiden. - Tooling: Beachten Sie, dass einige ältere Node.js-Tools und Test-Frameworks möglicherweise eine bessere Unterstützung für CJS als für ESM haben. Überprüfen Sie die Dokumentation Ihrer Toolchain auf ESM-Kompatibilität.
Fazit
Die Koexistenz von CommonJS und ES Modules in Node.js birgt sowohl Herausforderungen als auch Chancen. Während CJS eine lange Geschichte hat und weiterhin verbreitet ist, stellt ESM die Zukunft der JavaScript-Modularität dar und bietet einen standardisierten Ansatz mit erheblichen Vorteilen wie statischer Analyse und Live-Bindings. Durch das Verständnis ihrer grundlegenden Unterschiede und die Nutzung der integrierten Interoperabilitätsfunktionen von Node.js können Entwickler diese Dual-Module-Umgebung effektiv navigieren. Die Übernahme von ESM für neue Projekte und die sorgfältige Verwaltung der Interoperabilität in bestehenden Projekten führen zu robusteren, wartbareren und zukunftssicheren Node.js-Anwendungen.
Der Wandel hin zu ESM vereinheitlicht die Modulgeschichte von JavaScript über Umgebungen hinweg, wodurch die Codeorganisation konsistenter und leistungsfähiger wird.