mitmario.dev

Module

JavaScript Im Browser 3 Min Lesezeit 3 BeispieleLektion 4 von 7

Bis hierher lag dein ganzer Code in einer Datei. Ab einer gewissen Größe will man ihn aufteilen, und dafür gibt es Module.

export und import

Das Modul, das etwas hergibt
// Datei: werkzeuge.js

// Benannter Export. Davon darf es beliebig viele geben.
export function grossAnfang(text) {
  return text.charAt(0).toUpperCase() + text.slice(1);
}

export function kuerzen(text, laenge) {
  return text.length <= laenge ? text : text.slice(0, laenge) + "...";
}

// Auch Werte lassen sich exportieren.
export const MAXLAENGE = 40;

// Was nicht exportiert wird, bleibt im Modul.
// Von außen gibt es keinen Weg hierher.
function intern(text) {
  return text.trim();
}

// Der Standardexport. Höchstens einer je Datei, und im
// Alltag brauchst du ihn selten.
export default function aufraeumen(text) {
  return kuerzen(grossAnfang(intern(text)), MAXLAENGE);
}

export markiert, was eine Datei nach außen hergibt. Alles andere bleibt drin.

import holt es in einer anderen Datei herein. Benannte Importe stehen in geschweiften Klammern und heißen genau wie der Export.

Das Modul, das es holt
// Datei: app.js
// Eingebunden mit: <script src="app.js" type="module"></script>

// Benannte Importe stehen in geschweiften Klammern und
// heißen genau wie der Export.
import { grossAnfang, kuerzen, MAXLAENGE } from "./werkzeuge.js";

// Der Standardexport bekommt seinen Namen erst hier.
import aufraeumen from "./werkzeuge.js";

// Der Pfad ist vollständig, mit ./ und mit Endung.
// Ohne beides findet der Browser die Datei nicht.

console.log(grossAnfang("hallo"));
console.log(kuerzen("Ein ziemlich langer Satz für dieses Feld", 20));
console.log("Grenze:", MAXLAENGE);
console.log(aufraeumen("  eine notiz  "));

// intern gibt es hier nicht. Es wurde nicht exportiert,
// und dieser Zugriff wäre ein Fehler:
// console.log(intern("x"));

Neben den benannten Exporten gibt es den Standardexport mit export default. Davon kann es höchstens einen je Datei geben, und beim Import bekommt er seinen Namen erst an der Stelle, an der er geholt wird.

Im Alltag nimmt man benannte Exporte. Der Grund ist banal und wiegt trotzdem: Sie heißen überall gleich. Ein Standardexport kann in jeder Datei anders heißen, und dann sucht man denselben Code unter drei Namen. Außerdem findet die Autovervollständigung deines Editors einen benannten Export, einen Standardexport nicht.

Der eigentliche Gewinn

Der wichtigste Unterschied steht im dritten Beispiel, und zwar in seinem letzten Kommentar.

Dasselbe ohne Module
const MAXLAENGE = 40;

function intern(text) {
  return text.trim();
}

function grossAnfang(text) {
  return text.charAt(0).toUpperCase() + text.slice(1);
}

function kuerzen(text, laenge) {
  return text.length <= laenge ? text : text.slice(0, laenge) + "...";
}

function aufraeumen(text) {
  return kuerzen(grossAnfang(intern(text)), MAXLAENGE);
}

console.log(grossAnfang("hallo"));
console.log(kuerzen("Ein ziemlich langer Satz für dieses Feld", 20));
console.log("Grenze:", MAXLAENGE);
console.log(aufraeumen("  eine notiz  "));

// Der Unterschied: hier liegt alles im selben Raum.
// Eine zweite Datei mit einer Funktion namens kuerzen
// würde diese hier still ersetzen.

Jedes Modul hat seinen eigenen Gültigkeitsbereich. Was du darin kuerzen nennst, ist deins. Eine andere Datei darf eine völlig andere Funktion desselben Namens haben, und die beiden wissen nichts voneinander.

Bei mehreren gewöhnlichen <script>-Tags ist das anders: Die teilen sich einen einzigen Raum. Zwei Dateien mit einer Funktion desselben Namens, und die später geladene ersetzt die frühere still. Kein Fehler, keine Warnung, nur seltsames Verhalten an einer dritten Stelle.

Genau deshalb gibt es Module, und alles andere ist Beiwerk.

Vier Eigenheiten, die du kennen solltest

Der Pfad muss vollständig sein. ./werkzeuge.js mit dem Punkt am Anfang und der Endung am Schluss. Die Gewohnheit, sie wegzulassen, kommt von Bündlern und vom alten require in Node; mit import verlangt auch Node sie.

Module laufen automatisch im strikten Modus und automatisch verzögert. Ein defer bei type="module" ist deshalb überflüssig (Lektion 1.2).

Ein Modul wird nur einmal ausgewertet. Importieren es zehn Dateien, läuft sein Code trotzdem genau einmal, und alle zehn bekommen dasselbe Ergebnis. Das ist praktisch für gemeinsamen Zustand und eine Falle, wenn man mit einem frischen Anfang rechnet.

In einem Modul darf await ganz oben stehen, außerhalb jeder Funktion. Das heißt Top-Level-Await und funktioniert nur dort, nicht in einem gewöhnlichen Skript.

Warum das hier nicht läuft

Und jetzt der ehrliche Teil: Die ersten beiden Beispiele laufen hier nicht.

Der Editor hängt deine app.js als gewöhnliches <script> ein, ohne type="module". Damit ist sie ein Skript und kein Modul, und import und export gibt es dort schlicht nicht. Der Browser bricht schon beim Einlesen ab.

In der Console steht beim ersten Beispiel Uncaught SyntaxError: Unexpected token 'export' und daneben die Fundstelle app.js:4:1, beim zweiten Uncaught SyntaxError: Cannot use import statement outside a module mit app.js:6:1. Firefox sagt es in eigenen Worten (export declarations may only appear at top level of a module, import declarations may only appear at top level of a module), mit denselben Fundstellen. Klick auf die Fundstelle, und der Editor holt app.js nach vorn und färbt die Zeile: Es ist die export-Zeile beziehungsweise die import-Zeile, und mehr als diese eine Zeile ist auch nicht falsch.

Wichtig ist der Zeitpunkt: Das ist ein Syntaxfehler, kein fehlgeschlagener Ladevorgang. Zwei Dinge belegen das. Die Meldung kommt auch beim ersten Beispiel, in dem gar kein import steht, es also gar nichts zu laden gäbe. Und im Reiter Netzwerk stehen genau drei Zeilen, index.html, styles.css und app.js. Keine Zeile für werkzeuge.js, weder eine erfolgreiche noch eine rote. Der Browser hat diese Datei nie angefragt, weil er nie so weit gekommen ist.

Ein type="module" ließe sich technisch nachrüsten, aber nur, indem eine Sicherheitseinstellung dieser ganzen Website für eine einzige Lektion gelockert wird. Dafür ist eine Lektion zu wenig.

Die beiden Beispiele stehen trotzdem hier, weil du diesen Code lesen können musst: Jedes Projekt, dem du begegnest, ist so aufgebaut. Ausprobieren kannst du ihn in Lektion 20.5 auf deinem eigenen Rechner, wo Module ganz normal funktionieren.

Das dritte Beispiel zeigt dieselbe kleine Anwendung ohne Module. Es läuft, und der Vergleich der drei Dateien nebeneinander ist der Inhalt dieser Lektion.

Zum Mitnehmen

Die ersten beiden Beispiele laufen hier nicht, und das ist der ehrliche Teil dieser Lektion. Die Seite bindet deinen Code als gewöhnliches Skript ein, und dort gibt es import und export nicht.

Jetzt du

Basis Konto, kostenlos

Im Editor änderst du die Beispiele dieser Lektion und lässt sie gleich laufen. So merkst du am schnellsten, ob es sitzt.

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

Was in dieser Lektion steckt

  • Artikel mit 3 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.