mitmario.dev

ESM

Node.js Sandbox 4 Min Lesezeit 3 BeispieleLektion 2 von 7

Das zweite Modulsystem heißt ESM, kurz für ECMAScript Modules. Es ist keine Erfindung von Node, sondern Teil der Sprache selbst, und es funktioniert im Browser genauso. Ab dieser Lektion schreiben wir im Kurs nur noch ESM.

Herausgeben und hereinholen

Statt module.exports steht hier das Schlüsselwort export direkt vor dem, was herausgehen soll. Statt require gibt es import.

Dieselbe Rechnung als Modul
import formatiere, { brutto, SATZ } from "./preise.mjs";

console.log("Satz:", SATZ);
console.log(formatiere(brutto(100)));

Das ist dieselbe Rechnung wie in der vorigen Lektion, nur anders geschrieben. Beachte die zwei Sorten Export im Beispiel:

Benannte Exporte stehen vor einer Deklaration und behalten ihren Namen. Wer sie hereinholt, schreibt sie in geschweifte Klammern und muss den Namen genau treffen.

Der Standard-Export heißt export default und ist pro Datei nur einmal erlaubt. Er hat keinen Namen, deshalb darf die andere Seite ihn nennen, wie sie will. Im Beispiel steht er ohne Klammern ganz vorn im Import.

Wann nimmt man was? Eine brauchbare Faustregel: Gibt eine Datei genau ein Ding heraus, ist default in Ordnung. Gibt sie mehrere heraus, nimm benannte Exporte. Sie sind leichter zu finden, und dein Editor kann sie automatisch ergänzen.

Drei Wege, eine Datei zu ESM zu machen

Node muss vor dem Ausführen wissen, welches der beiden Systeme gilt. Es gibt genau drei Antworten:

  1. In der package.json des Projekts steht "type": "module". Dann sind alle .js-Dateien Module. Das ist der Normalfall in neuen Projekten, und wir sehen ihn in Abschnitt 5.
  2. Die Datei heißt .mjs. Dann gilt sie als Modul, egal was drumherum steht. So machen wir es in diesem Kurs, bis die package.json dazukommt.
  3. Keins von beidem. Dann schaut Node in die Datei und entscheidet an der Syntax: Steht dort ein import, ein export oder ein await auf oberster Ebene, liest es sie als Modul.

Es gibt spiegelbildlich auch .cjs für den umgekehrten Fall: eine CommonJS-Datei in einem Projekt, das sonst auf Module steht.

Der dritte Punkt ist neu und stand jahrelang anders in jedem Tutorial. Früher war ein import in einer .js ohne Eintrag ein harter Abbruch, und die Meldung dazu lautete SyntaxError: Cannot use import statement outside a module. Node rät seit einer Weile nicht mehr, sondern sieht nach.

Verlass dich trotzdem nicht darauf. Liegt eine package.json daneben, in der kein "type" steht, liest Node die Datei zweimal und schreibt eine Warnung auf den Fehlerkanal. Und "type": "commonjs" schaltet das Nachsehen ganz ab, dann ist der alte Abbruch wieder da. Lektion 5.1 kommt darauf zurück, wenn die package.json dazukommt.

Probier die drei Wege aus

Anlegen musst du dafür nichts: Hol das erste Beispiel dieser Lektion nach vorn, starte die Sandbox, und preise.mjs und index.mjs liegen dort. Tipp dann im Terminal erst cp index.mjs index.js und danach node index.js.

Dieselben Zeilen, andere Endung, kein Eintrag irgendwo, und es läuft trotzdem: Satz: 19 und 119,00 EUR. Node hat den import gesehen und die Datei als Modul gelesen.

Jetzt nimm ihm die Entscheidung ab. Tipp echo '{"type": "commonjs"}' > package.json und danach noch einmal node index.js. Da ist der alte Fehler, wörtlich, mit dem import in der Zeile darüber und einem Zirkumflex darunter. Ein node index.mjs läuft daneben weiter, denn die Endung schlägt den Eintrag.

Mit rm index.js package.json räumst du hinterher wieder auf. Nötig ist das nicht, ein Wechsel zu einem anderen Beispiel wischt das Verzeichnis ohnehin, aber es ist eine gute Gewohnheit.

Die Endung im Import ist Pflicht

Das ist der Punkt, an dem fast jeder Umsteiger einmal hängen bleibt.

Ein Import ohne Endung
// In ESM fehlt hier die Endung .mjs, und das ist ein Fehler.
import { brutto } from "./preise";

console.log(brutto(100));

In CommonJS durftest du require("./preise") schreiben, Node hat die Endung dazugeraten. In ESM tut es das nicht. Der Pfad im Import ist eine Adresse, und eine Adresse ohne Endung zeigt auf nichts.

Verwirrend ist das vor allem, weil du es in React-, Vue- oder Astro-Projekten dauernd ohne Endung siehst. Dort läuft aber ein Bündler dazwischen, der die Datei für dich sucht. Node hat keinen Bündler. Was du schreibst, wird gesucht.

Lies die Fehlermeldung im Beispiel einmal in Ruhe. ERR_MODULE_NOT_FOUND und ein Pfad ohne Endung darin sind zusammen ein ziemlich eindeutiges Zeichen.

Was jedes System besser kann

ESM kann await auf oberster Ebene, also außerhalb jeder Funktion.

Warten ganz oben
const antwort = await Promise.resolve("Das geht nur in ESM");

console.log(antwort);
console.log("require gibt es hier nicht:", typeof require);

Das ist praktischer, als es klingt: Konfiguration einlesen, eine Verbindung aufbauen, einen Wert holen, bevor der Rest startet. In CommonJS bräuchtest du dafür eine async-Funktion, die du sofort selbst aufrufst.

Im selben Beispiel siehst du auch, warum require in der vorigen Lektion nicht in der Namensliste stand: In einem Modul gibt es das Wort gar nicht.

CommonJS kann dafür etwas anderes: mitten im Code laden. Ein require darf in einer Bedingung stehen und erst dann ausgeführt werden, wenn es wirklich gebraucht wird. Ein import darf das nicht, es gehört auf die oberste Ebene der Datei und wird immer ausgeführt. Wer wirklich erst später laden will, nimmt die Funktionsform import(), die ein Promise zurückgibt und überall stehen darf.

Und was heißt das für dich?

Neue Projekte schreibst du in ESM. Es ist der Standard der Sprache, es läuft im Browser und in Node, und alle Werkzeuge unterstützen es. CommonJS liest du, wenn du in ältere Projekte kommst.

Zwei Systeme im selben Projekt zu mischen geht übrigens auch, ist aber ein eigenes kleines Thema voller Sonderfälle. Solange du eins von beidem konsequent durchziehst, begegnet es dir nicht.

Zum Mitnehmen

Die Dateiendung im Import ist in ESM Pflicht. Genau daran scheitert fast jeder Umsteiger einmal.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.

Was in dieser Lektion steckt

  • Artikel mit 3 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe, dein Code läuft auf einem Server

    Öffnet sich mit dem Basis Konto.