JavaScript-Module
Dieser Leitfaden vermittelt Ihnen alles, was Sie für den Einstieg in die Syntax von JavaScript-Modulen benötigen.
Grundidee von Modulen
Bevor wir uns die Syntax innerhalb von Modulen ansehen, betrachten wir zunächst, was Module sind, wie sie sich von herkömmlichen Skripten unterscheiden und warum Sie sie benötigen könnten.
Hintergrund zu Modulen
JavaScript-Programme waren anfangs recht klein. In der Anfangszeit wurde JavaScript meist für einzelne Skriptaufgaben eingesetzt, um Webseiten bei Bedarf etwas Interaktivität zu verleihen. Große Skripte waren daher im Allgemeinen nicht nötig. Einige Jahre später laufen vollständige Anwendungen mit viel JavaScript in Browsern, und JavaScript wird auch in anderen Umgebungen verwendet, beispielsweise in Node.js.
Komplexe Projekte benötigen eine Möglichkeit, JavaScript-Programme in separate Module aufzuteilen: Codeeinheiten, die bei Bedarf importiert werden können. Node.js bietet diese Möglichkeit schon seit Langem. Daneben gibt es zahlreiche JavaScript-Bibliotheken und -Werkzeuge, die die Verwendung von Modulen ermöglichen, beispielsweise AMD-Loader wie RequireJS, Bundler wie webpack und Compiler wie Babel.
Alle modernen Browser unterstützen Modulfunktionen nativ, ohne dass eine Transpilierung erforderlich ist. Das ist von Vorteil: Browser können das Laden von Modulen automatisch übernehmen, ohne dass dafür eine Bibliothek nötig ist. Bundler wie webpack werden dadurch allerdings nicht überflüssig. Sie können Code weiterhin in sinnvoll große Teile aufteilen und weitere Optimierungen wie Minifizierung, das Entfernen ungenutzten Codes und Tree Shaking durchführen.
Was ist ein Modul?
JavaScript-Code kann auf zwei Arten ausgewertet werden: als Skript (auch „klassisches Skript“ oder „herkömmliches Skript“ genannt) oder als Modul. Es gibt zwei wesentliche Unterschiede:
- Beim Parsen werden für Module leicht andere Syntaxregeln verwendet. Insbesondere befinden sie sich automatisch im Strict Mode, und
import- undexport-Anweisungen können nur in Modulen verwendet werden. - Zur Laufzeit werden Module in ihrem eigenen Gültigkeitsbereich ausgeführt, nicht im globalen Gültigkeitsbereich. Das bedeutet, dass Variablen, Funktionen, Klassen usw., die in einem Modul deklariert werden, außerhalb des Moduls nicht sichtbar sind. Ausnahmen gelten, wenn sie ausdrücklich exportiert werden, damit andere Module sie importieren können, oder wenn sie durch Anhängen an das globale Objekt (z. B.
windowin einem Browser) global verfügbar gemacht werden. Nicht abgefangene Fehler aus Modulen werden weiterhin in den DevTools angezeigt, auf Variablen im Gültigkeitsbereich eines Moduls können Sie über die JavaScript-Konsole jedoch nicht zugreifen.
Es gibt weitere Unterschiede, die jedoch speziell für Module im Web gelten. Diese finden Sie unter Module im Web verwenden.
So werden Skripte herkömmlicherweise in Webseiten eingebunden:
<head>
<script src="external-script.js"></script>
</head>
<body>
<!-- page content -->
<script src="external-script-2.js"></script>
<script>
// inline script
</script>
</body>
// -- external-script.js --
function $() {
// …
}
// …
// -- external-script-2.js --
$.ajax(/* … */);
// …
Besonders wichtig ist, dass alle klassischen Skripte, die in die Webseite eingebunden sind, im selben Gültigkeitsbereich ausgeführt werden: dem globalen Gültigkeitsbereich. Auf Variablen kann später überall auf der Seite zugegriffen werden, sofern sie nicht innerhalb anderer Funktionen oder Blöcke definiert sind. Das kann praktisch, aber auch gefährlich sein. Die Abbildung zeigt, wie das zweite und das dritte Skript auf die im ersten Skript deklarierte Variable $ zugreifen können, weil sich alle im globalen Gültigkeitsbereich befinden.
Dieses Muster ist sehr praktisch, wird bei größeren Projekten aber unübersichtlich:
- Die Verschmutzung des Gültigkeitsbereichs und Namenskonflikte werden zu einem erheblichen Problem. Beliebte Bibliotheken wie jQuery und Underscore fassen alles unter speziellen Bezeichnern wie
$und_zusammen, um das Risiko zu verringern. Viele Bibliotheken von Drittanbietern konkurrieren jedoch um gängige Namen wiemap,mergeoderdebounce. - Alle Abhängigkeiten müssen von den Entwickelnden ausdrücklich angegeben und verwaltet werden. Wenn Bibliothek A Bibliothek B importiert, müssen die Entwickelnden der Anwendung ein
<script>-Element hinzufügen, das Bibliothek B lädt, und sicherstellen, dass dies vor A geschieht. - Bibliotheken lassen sich nur schwer bei Bedarf laden: Alles wird geladen und initialisiert, sobald die Seite geladen wird.
Sehen wir uns nun an, wie derselbe Code mit Modulen aussehen könnte. Das erste Skript, external-script.js, möchte die Variable $ mit den übrigen Skripten teilen. Oben geschieht dies, indem die Variable im globalen Gültigkeitsbereich abgelegt wird. In der Modulversion können wir die Variable exportieren und in den anderen Skripten importieren.
<head>
<script type="module" src="external-script-2.js"></script>
<script type="module">
import { $ } from "./external-script.js";
// inline script
</script>
</head>
<body>
<!-- page content -->
</body>
// -- external-script.js --
function $() {
// …
}
// …
export { $ };
// -- external-script-2.js --
import { $ } from "./external-script.js";
$.ajax(/* … */);
// …
Während die Skriptversion eine lineare Abfolge von Abhängigkeiten bildet, in der jeder Codeteil auf den gesamten vorhergehenden Code zugreifen kann, bildet die Modulversion einen Abhängigkeitsgraphen. Darin gibt jedes Modul ausdrücklich an, von welchen Modulen es abhängt und welche Variablen es aus diesen Abhängigkeiten benötigt.
Wie bereits erwähnt, werden Module anders geparst als Skripte. Die JavaScript-Engine muss daher wissen, ob sie die Parsing-Regeln für Module oder für Skripte anwenden soll. Hostumgebungen geben dies üblicherweise über einen Signalisierungsmechanismus außerhalb des Codes an: Das Verhalten des Codes wird durch Informationen festgelegt, die nicht im Code selbst stehen. (Das unterscheidet sich vom Strict Mode, bei dem der Interpretationsmodus innerhalb des Skripts mit "use strict" angegeben wird.) Solche Informationen lassen sich auf verschiedene Weise bereitstellen:
- Wenn das Modul über ein
<script>-Tag eingebunden wird, können Sie das Attributtype="module"verwenden. Siehe auch Module im Web verwenden. - Wenn Sie Node.js verwenden, können Sie die Dateiendung
.mjsnutzen oder"type": "module"in die nächstgelegene Dateipackage.jsoneintragen. Siehe auch Module auf verschiedenen Plattformen. - Soll das Modul als Worker verwendet werden, können Sie beim Aufruf des Konstruktors
Worker()type: "module"übergeben.
Hinweis:
Manche Laufzeitumgebungen wie Node.js erkennen den Modultyp auch ohne ausdrückliche Angabe anhand von Syntax wie import/export, die außerhalb von Modulen nicht verfügbar ist. Browser bieten diese Möglichkeit nicht.
Module können weiterhin auf globale Variablen zugreifen. Beispielsweise kann ein Modul eine Variable verwenden, die von einem klassischen Skript deklariert wurde:
<script>
var text = "Hello";
</script>
<script type="module" src="render.js"></script>
// -- render.js --
document.body.textContent = text;
Exkurs — .mjs gegenüber .js
In diesem Artikel verwenden wir für Moduldateien durchgehend die Endung .js. In anderen Quellen begegnet Ihnen möglicherweise stattdessen die Endung .mjs. Beispielsweise empfiehlt die Dokumentation von V8 deren Verwendung. Dafür werden folgende Gründe angeführt:
- Sie können direkt an der Datei erkennen, ob sie ein Modul oder ein Skript ist, ohne den Kontext wie das HTML-Tag oder die package.json betrachten zu müssen.
- Außerhalb von Browsern wird die Endung nahezu überall als eindeutiges Kennzeichen für den Modulmodus anerkannt, unter anderem von Laufzeitumgebungen wie Node.js und Build-Werkzeugen wie Babel.
Für Browser ist der Unterschied rein äußerlich. Die Dateiendung spielt im Web keine Rolle: JavaScript wird über den Header Content-Type identifiziert, Module über das Attribut type="module" im <script>-Tag. Damit Module in einem Browser korrekt funktionieren, muss Ihr Server sie mit einem Content-Type-Header ausliefern, der einen JavaScript-MIME-Typ wie text/javascript enthält. Andernfalls erhalten Sie bei der strengen MIME-Typ-Prüfung eine Fehlermeldung wie „The server responded with a non-JavaScript MIME type“, und der Browser führt Ihr JavaScript nicht aus.
Wir haben uns für .js entschieden. Der wichtigste Vorteil ist, dass die Endung .js deutlich verbreiteter ist als .mjs. Auch wenn .mjs zunehmend unterstützt wird, können Sie immer noch auf Software stoßen, die diese Endung nicht versteht.
- Die meisten Server setzen für
.js-Dateien automatisch den richtigen Typ, für.mjs-Dateien jedoch nicht immer. Zu den Servern, die.mjs-Dateien bereits korrekt ausliefern, gehören GitHub Pages undhttp-serverfür Node.js. Das ist kein Problem, wenn Sie eine solche Umgebung bereits verwenden oder Ihren Server so konfigurieren können, dass er für.mjs-Dateien den richtigenContent-Typesetzt. Wenn Sie den Server, von dem die Dateien ausgeliefert werden, nicht kontrollieren oder Dateien wie hier öffentlich bereitstellen, kann es jedoch zu Verwirrung führen. - Die Dateiendung wird von vielen Werkzeugen außer dem HTTP-Server verwendet, etwa von Ihrem Editor, dem Betriebssystem, Werkzeugen für statische Analysen oder Formatierungsprogrammen.
.mjswird von solchen Werkzeugen möglicherweise weniger gut unterstützt. Manche Betriebssysteme erkennen die Endung beispielsweise nicht oder versuchen, sie durch etwas anderes zu ersetzen, etwa indem sie beim Öffnen einer Datei stillschweigend die Endung.jsanhängen.
Wenn Ihnen die klare Unterscheidung zwischen .mjs für Module und .js für „normale“ JavaScript-Dateien wichtig ist, Sie das oben beschriebene Problem aber vermeiden möchten, können Sie während der Entwicklung .mjs verwenden und die Dateien beim Build in .js umwandeln.
Mit Beispielen arbeiten
Um die Verwendung von Modulen zu veranschaulichen, haben wir einige einfache Beispiele erstellt, die Sie auf GitHub finden. Sie zeigen eine kleine Gruppe von Modulen, die auf einer Webseite ein <canvas>-Element erstellen und anschließend verschiedene Formen auf dem Canvas zeichnen sowie Informationen darüber ausgeben.
Die Beispiele sind recht einfach gehalten, damit die Funktionsweise von Modulen deutlich wird.
Unser erstes Beispiel (siehe basic-modules) hat folgende Dateistruktur:
index.html
main.js
modules/
canvas.js
square.js
Hinweis: Alle Beispiele in diesem Leitfaden haben im Wesentlichen dieselbe Struktur.
Die Datei index.html definiert lediglich das DOM und bindet das Modul main.js ein. Der gesamte nachfolgend erläuterte Code betrifft ausschließlich die .js-Dateien.
Die beiden Module im Verzeichnis modules sind:
canvas.js— enthält Funktionen zum Einrichten des Canvas:create()— erstellt einen Canvas mit der angegebenenwidthundheightinnerhalb eines umgebenden<div>mit einer angegebenen ID. Dieses wird wiederum an ein angegebenes übergeordnetes Element angehängt. Die Funktion gibt ein Objekt zurück, das den 2D-Kontext des Canvas und die ID des umgebenden Elements enthält.createReportList()— erstellt eine ungeordnete Liste innerhalb eines angegebenen umgebenden Elements, in die Berichtsdaten ausgegeben werden können. Die Funktion gibt die ID der Liste zurück.
square.js— enthält:name— eine Konstante mit der Zeichenfolge 'square'.draw()— zeichnet auf einem angegebenen Canvas ein Quadrat mit festgelegter Größe, Position und Farbe. Die Funktion gibt ein Objekt mit Größe, Position und Farbe des Quadrats zurück.reportArea()— schreibt die Fläche eines Quadrats anhand seiner Seitenlänge in eine bestimmte Berichtsliste.reportPerimeter()— schreibt den Umfang eines Quadrats anhand seiner Seitenlänge in eine bestimmte Berichtsliste.
Nachdem Sie diese Beispiele heruntergeladen haben, starten Sie einen lokalen Server und rufen Sie die Datei index.html auf.
Benannte Exporte
Anders als bei Skripten, bei denen alle auf der obersten Ebene deklarierten Variablen für nachfolgende Skripte automatisch zugänglich sind, müssen Sie Variablen ausdrücklich mit export exportieren, damit andere Module auf sie zugreifen können. Sie können dieses Schlüsselwort vor jede Deklaration setzen, darunter var, let, const, function und class. Die Deklarationen müssen sich auf der obersten Ebene befinden: Innerhalb einer Funktion können Sie export beispielsweise nicht verwenden.
export const name = "square";
export function draw(ctx, length, x, y, color) {
ctx.fillStyle = color;
ctx.fillRect(x, y, length, length);
return { length, x, y, color };
}
Alternativ können Sie Elemente, die Sie bereits an anderer Stelle im Modul deklariert haben, exportieren. Verwenden Sie dazu export gefolgt von einer kommagetrennten Liste der Variablennamen in geschweiften Klammern:
export { name, draw, reportArea, reportPerimeter };
Benannte Importe
Nachdem Sie Variablen aus Ihrem Modul exportiert haben, müssen Sie sie in ein anderes Modul importieren, um sie dort zu verwenden. Dazu verwenden Sie die Anweisung import, gefolgt von einer kommagetrennten Liste der gewünschten Namen in geschweiften Klammern, dem Schlüsselwort from und dem Modulbezeichner.
import { name, draw, reportArea, reportPerimeter } from "./modules/square.js";
Solche Zeilen sehen Sie in main.js im Einsatz.
Hinweis:
Der Modulbezeichner ist eine Zeichenfolge, die auf das Modul verweist, das diese Variablen exportiert. In einem Browser ist dies üblicherweise die URL relativ zur URL des aktuellen Moduls, ähnlich wie beim Attribut href von <a>-Elementen. Wenn beispielsweise unser obiges Modul unter https://example.com/basic-modules/main.js und das Modul square.js unter https://example.com/basic-modules/modules/square.js ausgeliefert wird, sind alle folgenden Angaben gleichwertig:
"https://example.com/basic-modules/modules/square.js" "/basic-modules/modules/square.js" "./modules/square.js"
Für das Format von Modulbezeichnern gelten je nach Laufzeitumgebung (Browser, Node, Bundler usw.) unterschiedliche Regeln. Mehr dazu erfahren Sie unter Module im Web verwenden und Module auf verschiedenen Plattformen.
Nachdem Sie die Funktionen in Ihr Skript importiert haben, können Sie sie so verwenden, als wären sie in derselben Datei definiert. Der folgende Code steht in main.js unterhalb der Importzeilen:
const myCanvas = create("myCanvas", document.body, 480, 320);
const reportList = createReportList(myCanvas.id);
const square = draw(myCanvas.ctx, 50, 50, 100, "blue");
reportArea(square.length, reportList);
reportPerimeter(square.length, reportList);
Hinweis:
Importierte Werte sind schreibgeschützte Ansichten der exportierten Elemente. Ähnlich wie bei const-Variablen können Sie der importierten Variablen keinen neuen Wert zuweisen, Eigenschaften von Objektwerten aber weiterhin ändern. Ein neuer Wert kann nur durch das exportierende Modul zugewiesen werden. Ein Beispiel finden Sie in der Referenz zu import.
Standardexporte und -importe
In den obigen Beispielen werden Variablen anhand ihres Namens exportiert und importiert. Die Namen der einzelnen Variablen müssen im exportierenden und im importierenden Modul übereinstimmen. Funktionierenden Modulcode können Sie allein mit benannten Importen und Exporten schreiben. Wenn Sie jedoch aus Nicht-ESM-Modulen wie CommonJS-Modulen oder Nicht-JavaScript-Dateien importieren möchten, stehen in diesen Systemen keine benannten Exporte zur Verfügung. Dort entspricht jedes Modul genau einem JavaScript-Wert. Zur Darstellung solcher Module stellt JavaScript einen Standardexport bereit. Er ist auch in anderen Fällen nützlich, etwa bei Modulen, die auf einem zentralen Wert beruhen – einer einzelnen Klasse oder Funktion. Durch den Standardexport kann jedes importierende Modul einen eigenen lokalen Namen wählen, ohne wissen zu müssen, wie das exportierende Modul den Wert nennt.
Anders als benannte Exporte haben Standardexporte keinen zugeordneten Namen. Jedes Modul kann höchstens einen Standardexport besitzen. Um einen Standardexport zu erstellen, exportieren Sie statt einer Deklaration wie const oder function einen Ausdruck, dem Sie export default voranstellen. Beispielsweise funktionieren alle folgenden Varianten:
// An object literal is an expression
export default {
some: "object",
};
// An addition is an expression
export default 1 + 1;
// An identifier is also an expression
export default randomSquare;
Die letzte Variante sehen Sie in square.js unseres basic-modules-Beispiels. Dort wird die Funktion randomSquare() exportiert, die ein Quadrat mit zufälliger Farbe, Größe und Position erstellt. Beachten Sie, dass keine geschweiften Klammern verwendet werden.
Als Sonderfall werden Funktionen und Klassen als Deklarationen statt als Ausdrücke exportiert; diese Deklarationen können anonym sein. Sie können eine Funktion daher auch direkt bei ihrer Deklaration als Standardexport exportieren:
export default function randomSquare(ctx) {
// …
}
// -- main.js --
import randomSquare from "./modules/square.js";
Beachten Sie auch hier, dass keine geschweiften Klammern verwendet werden. Pro Modul ist nur ein Standardexport erlaubt, und wir wissen, dass es sich dabei um randomSquare handelt. Diesen Import können Sie beliebig benennen – der Name muss nicht mit dem Funktionsnamen im exportierenden Modul übereinstimmen.
Benannte Exporte und ein Standardexport können im selben Modul nebeneinander existieren.
Namenskonflikte vermeiden
Bisher scheinen unsere Module zum Zeichnen von Formen auf dem Canvas gut zu funktionieren. Was passiert aber, wenn wir ein Modul hinzufügen, das eine andere Form zeichnet, etwa einen Kreis oder ein Dreieck? Diese Formen hätten wahrscheinlich ebenfalls Funktionen wie draw() oder reportArea(). Wenn wir verschiedene Funktionen mit demselben Namen in dieselbe Moduldatei auf oberster Ebene importieren wollten, käme es zu Konflikten und Fehlern.
Sehen wir uns ein konkretes Beispiel an. Im Verzeichnis renaming finden Sie dasselbe Modulsystem wie im vorherigen Beispiel. Zusätzlich gibt es dort die Module circle.js und triangle.js, die Kreise und Dreiecke zeichnen und Informationen darüber ausgeben.
Jedes dieser Module exportiert Elemente mit denselben Namen und enthält daher am Ende dieselbe export-Anweisung:
export { name, draw, reportArea, reportPerimeter };
Wenn wir beim Import in main.js Folgendes versuchen würden:
import { name, draw, reportArea, reportPerimeter } from "./modules/square.js";
import { name, draw, reportArea, reportPerimeter } from "./modules/circle.js";
import { name, draw, reportArea, reportPerimeter } from "./modules/triangle.js";
würde der Browser einen Fehler wie „SyntaxError: redeclaration of import name“ ausgeben (Firefox).
Glücklicherweise gibt es mehrere Möglichkeiten, dieses Problem zu umgehen. Diese sehen wir uns in den folgenden Abschnitten an.
Importe und Exporte umbenennen
Innerhalb der geschweiften Klammern einer import- oder export-Anweisung können Sie das Schlüsselwort as zusammen mit einem neuen Namen verwenden, um den Bezeichner zu ändern, unter dem Sie ein Element im übergeordneten Modul nutzen.
In unserem Beispiel können wir die Importe so umbenennen, dass ihre Namen eindeutig sind:
import {
name as squareName,
draw as drawSquare,
reportArea as reportSquareArea,
reportPerimeter as reportSquarePerimeter,
} from "./modules/square.js";
import {
name as circleName,
draw as drawCircle,
reportArea as reportCircleArea,
reportPerimeter as reportCirclePerimeter,
} from "./modules/circle.js";
import {
name as triangleName,
draw as drawTriangle,
reportArea as reportTriangleArea,
reportPerimeter as reportTrianglePerimeter,
} from "./modules/triangle.js";
Sie könnten das Problem stattdessen auch in den Moduldateien lösen, beispielsweise so:
// -- square.js --
export {
name as squareName,
draw as drawSquare,
reportArea as reportSquareArea,
reportPerimeter as reportSquarePerimeter,
};
// -- main.js --
import {
squareName,
drawSquare,
reportSquareArea,
reportSquarePerimeter,
} from "./modules/square.js";
Das würde genauso funktionieren. Welchen Stil Sie wählen, bleibt Ihnen überlassen. Es spricht jedoch einiges dafür, den Modulcode unverändert zu lassen und die Importe anzupassen. Das gilt besonders, wenn Sie Module von Drittanbietern importieren, auf die Sie keinen Einfluss haben.
Übrigens können Sie dieselbe Syntax auch für Standardexporte verwenden, indem Sie auf den Exportnamen default verweisen. Dabei handelt es sich um ein reserviertes Wort, nicht um einen tatsächlich gültigen Bezeichnernamen. Der Unterschied zu export default randomSquare; besteht darin, dass hier die bestehende Live-Bindung exportiert wird, statt den Wert des Ausdrucks zu erfassen. Wenn Sie randomSquare im exportierenden Modul einen neuen Wert zuweisen, kann das importierende Modul mit dieser Form den neuen Wert sehen. Benannte Standarddeklarationen von Funktionen und Klassen, etwa export default function randomSquare() {}, exportieren ebenfalls Live-Bindungen.
import { default as randomSquare } from "./modules/square.js";
export { randomSquare as default };
Ein Modul-Namespace-Objekt erstellen
Die obige Methode funktioniert, ist aber etwas umständlich und langatmig. Eine bessere Lösung ist, die Elemente jedes Moduls in ein Modul-Namespace-Objekt zu importieren. Das geschieht mit folgender Syntax:
import * as Module from "./modules/module.js";
Damit werden alle Exporte aus module.js übernommen und als Eigenschaften eines Objekts Module verfügbar gemacht. Das Modul erhält so gewissermaßen einen eigenen Namensraum. Zum Beispiel:
Module.function1();
Module.function2();
Sehen wir uns auch hierfür ein konkretes Beispiel an. Im Verzeichnis module-objects finden Sie dasselbe Beispiel, diesmal für die Verwendung dieser Syntax umgeschrieben. In den Modulen haben alle Exporte die folgende einfache Form:
export { name, draw, reportArea, reportPerimeter };
Die Importe sehen dagegen so aus:
import * as Canvas from "./modules/canvas.js";
import * as Square from "./modules/square.js";
import * as Circle from "./modules/circle.js";
import * as Triangle from "./modules/triangle.js";
Sie können nun jeweils über den angegebenen Objektnamen auf die importierten Elemente eines Moduls zugreifen, beispielsweise:
const square = Square.draw(myCanvas.ctx, 50, 50, 100, "blue");
Square.reportArea(square.length, reportList);
Square.reportPerimeter(square.length, reportList);
Damit können Sie den Code genauso wie zuvor schreiben, solange Sie an den erforderlichen Stellen die Objektnamen angeben. Die Importe sind dabei deutlich übersichtlicher.
Module und Klassen
Wie bereits angedeutet, können Sie auch Klassen exportieren und importieren. Das ist eine weitere Möglichkeit, Konflikte in Ihrem Code zu vermeiden, und besonders nützlich, wenn Ihr Modulcode bereits objektorientiert geschrieben ist.
Ein Beispiel, bei dem unser Modul zum Zeichnen von Formen mit ES-Klassen umgeschrieben wurde, finden Sie im Verzeichnis classes. Die Datei square.js enthält beispielsweise nun ihre gesamte Funktionalität in einer einzigen Klasse:
class Square {
constructor(ctx, listId, length, x, y, color) {
// …
}
draw() {
// …
}
// …
}
Diese exportieren wir anschließend:
export { Square };
// -- main.js --
import { Square } from "./modules/square.js";
Danach verwenden wir die Klasse, um unser Quadrat zu zeichnen:
const square = new Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, "blue");
square.draw();
square.reportArea();
square.reportPerimeter();
Module zusammenfassen
Wir haben nun mehrere Module, die jeweils etwas Ähnliches tun. Wer mehrere davon verwenden möchte, muss für jedes eine eigene Importdeklaration schreiben:
import { Square } from "./modules/square.js";
import { Circle } from "./modules/circle.js";
import { Triangle } from "./modules/triangle.js";
Besser wäre es, alle diese Exporte in einem einzigen Modul zusammenzufassen. Dann könnten Nutzende unabhängig von der Anzahl benötigter Exporte alle mit nur einer import-Anweisung beziehen.
import { Square, Circle, Triangle } from "./modules/shapes.js";
In unserem Beispiel module-aggregation, das auf dem vorherigen Klassenbeispiel beruht, gibt es ein zusätzliches Modul namens shapes.js. Es fasst die Funktionalität von circle.js, square.js und triangle.js zusammen. Ein solches Modul wird „Barrel-Modul“ genannt, weil es nichts anderes tut, als Exporte anderer Module zu bündeln. Außerdem haben wir die Untermodule in ein Unterverzeichnis namens shapes innerhalb des Verzeichnisses modules verschoben. Die Modulstruktur dieses Beispiels sieht daher so aus:
modules/
canvas.js
shapes.js
shapes/
circle.js
square.js
triangle.js
// -- shapes.js --
import { Circle } from "./shapes/circle.js";
import { Square } from "./shapes/square.js";
import { Triangle } from "./shapes/triangle.js";
export { Circle, Square, Triangle };
Damit werden die Exporte der einzelnen Untermodule übernommen und über das Modul shapes.js verfügbar gemacht. Es ist jedoch nicht nötig, etwas nur deshalb zu importieren, um es anschließend zu exportieren. Mit der Syntax export ... from können Sie beides direkt in einer Zeile erledigen.
export { Square } from "./shapes/square.js";
export { Triangle } from "./shapes/triangle.js";
export { Circle } from "./shapes/circle.js";
Hinweis:
Die in shapes.js referenzierten Exporte werden im Wesentlichen durch die Datei hindurch weitergeleitet und existieren dort nicht wirklich. Deshalb können Sie in derselben Datei keinen sinnvollen Code schreiben, der auf sie zugreift.
Alternativ können Sie in shapes.js die spezielle Syntax export * from verwenden:
export * from "./shapes/square.js";
export * from "./shapes/triangle.js";
export * from "./shapes/circle.js";
Damit werden alle Exporte außer dem Standardexport aus dem Zielmodul als benannte Exporte des aktuellen Moduls erneut exportiert. Wenn Sie also weitere Elemente aus square.js exportieren, müssen Sie shapes.js nicht ebenfalls anpassen. Verwenden Sie dies nur, wenn shapes.js tatsächlich alles aus square.js erneut exportieren soll; andernfalls könnten Sie unbeabsichtigt Elemente zugänglich machen.
Für fast alle vorgestellten import-Syntaxformen gibt es eine Entsprechung mit export ... from. Die einzige Ausnahme ist der Standardimport import x from "mod": export x from "mod" existiert nicht. Stattdessen müssen Sie die spezielle Syntax export { default } from verwenden.
export { x } from "mod";
export { x as v } from "mod";
export * as ns from "mod";
export { default } from "mod";
Ein Modul wegen seiner Seiteneffekte importieren
Manchmal möchten Sie den Initialisierungscode eines Moduls ausführen, ohne einen seiner Exporte zu importieren, etwa um ein Polyfill zu installieren. Verwenden Sie dafür einen Import, der nur aus einem Modulbezeichner besteht:
import "./modules/polyfills.js";
Weitere Informationen finden Sie in der Referenz zu import.
Importdeklarationen werden vorgezogen
Importdeklarationen werden vorgezogen (Hoisting). Das bedeutet hier, dass importierte Werte im Modulcode schon vor der Stelle verfügbar sind, an der sie deklariert werden. Außerdem treten die Seiteneffekte des importierten Moduls ein, bevor der übrige Code des Moduls ausgeführt wird.
Beispielsweise würde der Import von Canvas in der Mitte von main.js weiterhin funktionieren:
// …
const myCanvas = new Canvas("myCanvas", document.body, 480, 320);
myCanvas.create();
import { Canvas } from "./modules/canvas.js";
myCanvas.createReportList();
// …
Es gilt jedoch als gute Praxis, alle Importe an den Anfang des Codes zu stellen. Dadurch lassen sich Abhängigkeiten leichter analysieren.
JSON-Module importieren
Wir haben gesehen, wie sich Daten aus JavaScript-Modulen importieren lassen, die mit export-Anweisungen exportiert werden. Sie können auch Werte aus Modulen importieren, die in anderen Sprachen geschrieben sind, sofern die Laufzeitumgebung weiß, wie sie diese interpretieren muss. Die Spezifikation definiert nur zwei weitere Ressourcentypen: JSON-Module und Textmodule. Hier betrachten wir die häufiger verwendeten JSON-Module.
Ein JSON-Modul ist im Wesentlichen eine eigenständige JSON-Datei. Beim Import stellt es einen einzigen Standardexport bereit, der den geparsten JSON-Wert enthält. Sie importieren es so:
import data from "./data.json" with { type: "json" };
Beachten Sie das zusätzliche with { type: "json" } am Ende. Dabei handelt es sich um ein Importattribut, das die Laufzeitumgebung anweist zu prüfen, ob die geladene Datei tatsächlich JSON enthält. Im Leitfaden Module im Web verwenden gehen wir näher darauf ein, da die Semantik dieser Attribute nicht in der Kernsprache definiert ist. Möglicherweise können Sie JSON-Module auch ohne with { type: "json" } importieren. Die einzige Vorgabe lautet: Wenn with { type: "json" } angegeben ist, muss das geladene Modul als JSON geparst werden. Es empfiehlt sich, den Typ eines importierten Moduls immer anzugeben, damit der Import überall funktioniert.
Textmodule funktionieren ähnlich und liefern Zeichenfolgen.
import text from "./text.txt" with { type: "text" };
Module dynamisch laden
Sie können Module auch erst dann dynamisch laden, wenn sie benötigt werden, statt alles im Voraus zu laden. Das bietet offensichtliche Leistungsvorteile. Sehen wir uns an, wie es funktioniert.
Der Operator import() kann mit dem Pfad zum Modul als Parameter aufgerufen werden. Er gibt ein Promise zurück, das mit einem Modul-Namespace-Objekt erfüllt wird (siehe Ein Modul-Namespace-Objekt erstellen). Über dieses Objekt können Sie auf die Exporte des Moduls zugreifen. Zum Beispiel:
import("./modules/myModule.js").then((module) => {
// Do something with the module.
});
Hinweis:
Dynamische Importe sind im Hauptthread des Browsers sowie in Shared Workers und Dedicated Workers erlaubt.
Ein Aufruf von import() in einem Service Worker oder Worklet wird jedoch abgelehnt.
Sehen wir uns ein Beispiel an. Im Verzeichnis dynamic-module-imports finden Sie ein weiteres Beispiel auf Grundlage unseres Klassenbeispiels. Diesmal zeichnen wir beim Laden des Beispiels jedoch nichts auf den Canvas. Stattdessen gibt es drei Schaltflächen – „Circle“, „Square“ und „Triangle“. Beim Anklicken laden sie dynamisch das benötigte Modul und verwenden es, um die entsprechende Form zu zeichnen.
In diesem Beispiel haben wir nur die Dateien index.html und main.js geändert. Die Modulexporte bleiben unverändert.
In main.js holen wir mit einem Aufruf von document.querySelector() eine Referenz auf jede Schaltfläche, zum Beispiel:
const squareBtn = document.querySelector(".square");
Anschließend fügen wir jeder Schaltfläche einen Event Listener hinzu. Wenn die Schaltfläche angeklickt wird, lädt er das zugehörige Modul dynamisch und verwendet es zum Zeichnen der Form:
squareBtn.addEventListener("click", () => {
import("./modules/square.js").then((Module) => {
const square = new Module.Square(
myCanvas.ctx,
myCanvas.listId,
50,
50,
100,
"blue",
);
square.draw();
square.reportArea();
square.reportPerimeter();
});
});
Da bei der Erfüllung des Promise ein Modul-Namespace-Objekt zurückgegeben wird, ist die Klasse anschließend eine Eigenschaft dieses Objekts. Deshalb müssen wir nun auf den Konstruktor zugreifen, indem wir Module. voranstellen, beispielsweise Module.Square( /* … */ ).
Ein weiterer Vorteil dynamischer Importe ist, dass sie auch in Skriptumgebungen verfügbar sind. Wenn Ihr HTML also bereits ein <script>-Tag ohne type="module" enthält, können Sie als Module bereitgestellten Code dennoch durch dynamischen Import wiederverwenden.
<script>
import("./modules/square.js").then((module) => {
// Do something with the module.
});
// Other code that operates on the global scope and is not
// ready to be refactored into modules yet.
var btn = document.querySelector(".square");
</script>
Top-Level-await
Top-Level-await ist eine Funktion von Modulen: Das Schlüsselwort await kann auf der obersten Ebene außerhalb einer async-Funktion verwendet werden. Dadurch kann ein Modul wie eine große asynchrone Funktion arbeiten. Es kann asynchrone Aufgaben abschließen, bevor abhängige Module ausgeführt werden, ohne dabei die Auswertung anderer, nicht abhängiger Module zu blockieren.
Sehen wir uns ein Beispiel an. Alle in diesem Abschnitt beschriebenen Dateien und Codebeispiele finden Sie im Verzeichnis top-level-await, das die vorherigen Beispiele erweitert.
Zunächst definieren wir unsere Farbpalette in einer separaten Datei colors.json:
{
"yellow": "#F4D03F",
"green": "#52BE80",
"blue": "#5499C7",
"red": "#CD6155",
"orange": "#F39C12"
}
Dann erstellen wir ein Modul namens getColors.js. Es lädt die Datei colors.json mit einer Fetch-Anfrage und gibt die Daten als Objekt zurück.
// fetch request
const colors = fetch(
"https://mdn.github.io/js-examples/module-examples/top-level-await/data/colors.json",
).then((response) => response.json());
export default await colors;
Beachten Sie hier die letzte Exportzeile.
Wir verwenden das Schlüsselwort await, bevor wir die zu exportierende Konstante colors festlegen. Das bedeutet, dass andere Module, die dieses Modul einbinden, warten, bis colors heruntergeladen und geparst wurde, bevor sie es verwenden.
// -- main.js --
import colors from "./modules/getColors.js";
import { Canvas } from "./modules/canvas.js";
const circleBtn = document.querySelector(".circle");
// …
Beim Aufruf unserer Funktionen für die Formen verwenden wir colors anstelle der zuvor verwendeten Zeichenfolgen:
const square = new Module.Square(
myCanvas.ctx,
myCanvas.listId,
50,
50,
100,
colors.blue,
);
const circle = new Module.Circle(
myCanvas.ctx,
myCanvas.listId,
75,
200,
100,
colors.green,
);
const triangle = new Module.Triangle(
myCanvas.ctx,
myCanvas.listId,
100,
75,
190,
colors.yellow,
);
Das ist nützlich, weil der Code in main.js erst ausgeführt wird, nachdem der Code in getColors.js gelaufen ist. Die Auswertung unabhängiger Module wird dadurch jedoch nicht aufgehalten: Unser Modul canvas.js wird weiterhin ausgewertet, während colors abgerufen wird.
Top-Level-await hat seinen Preis: Es hat weitreichende Auswirkungen, weil ein Teil des Modulgraphen nur noch asynchron ausgewertet werden kann. Weitere Informationen finden Sie im Leitfaden zum Modulgraphen.
Modulmetadaten
Skripte werden im globalen Kontext ausgeführt und können daher über globale Variablen wie Window.document oder Window.location Informationen über ihre Umgebung abrufen. Jedes Modul besitzt außerdem einen eigenen Ausführungskontext, auf den über ein modulspezifisches Objekt import.meta zugegriffen wird. Die Eigenschaften dieses Objekts werden von der Hostumgebung festgelegt; die Spezifikation der Kernsprache definiert keine davon. Informationen zu Browsern finden Sie unter Module im Web verwenden, Informationen zu Node.js, Bundlern und weiteren Umgebungen unter Module auf verschiedenen Plattformen.
In den meisten webähnlichen Umgebungen (Browsern, Node.js, Bundlern usw.) stellt import.meta.url beispielsweise die URL des Moduls bereit. Damit können Sie eine Ressource relativ zum Modul finden:
// -- modules/getColors.js --
const colorsURL = new URL("../data/colors.json", import.meta.url);
So bleibt die Ressourcen-URL relativ zu getColors.js, auch wenn das Modul von einer Seite oder einem Modul in einem anderen Verzeichnis importiert wird.
Aufgaben der Sprache und der Hostumgebung
Dieser Artikel stellt Module so vor, wie sie in der ECMAScript-Spezifikation definiert sind. JavaScript-Module sind eng mit der Hostumgebung verknüpft. Sie müssen sich an unterschiedliche Ein-/Ausgabebedingungen, Architekturen und technische Anforderungen anpassen. Deshalb definiert die Kernsprache nur Folgendes:
- Die Syntax von Modulfunktionen, etwa
import- undexport-Deklarationen,import.metaund den Ausdruckimport(). - Den Aufbau, die Verknüpfung und die Auswertung des Modulgraphen, einschließlich der Erkennung von Zyklen.
- Die Struktur des Modul-Namespace-Objekts.
Folgende Aspekte sind für die Kernsprache nicht relevant:
- Das Konzept von „Dateien“. Zwar ist jedes Modul üblicherweise eine eigene Datei, es könnte aber ebenso ein Objekt im Arbeitsspeicher, eine dynamisch abgerufene Zeichenfolge oder etwas anderes sein, das sich als JavaScript-Wert darstellen lässt.
- Die Struktur der Zeichenfolge, die als Modulbezeichner dient. Sie könnte eine URL, ein Dateipfad oder ein anderer Bezeichner sein, den die Hostumgebung erkennt.
- Die Eigenschaften von
import.meta. Alle Eigenschaften, einschließlichimport.meta.url, werden von der Hostumgebung definiert. - Der Ladevorgang von Modulen. Die Hostumgebung ist für das Abrufen der Module zuständig, einschließlich der Anwendung etwaiger Importattribute. Dabei muss sie die Vorgaben der Sprache einhalten, beispielsweise jene für JSON-Module.
In der Praxis implementieren Laufzeitumgebungen wie Browser, Node.js und Deno oft dieselben Funktionen, damit Code möglichst plattformübergreifend funktioniert. Dieser Leitfaden verwendet ein Beispiel, das im Browser läuft, konzentriert sich aber auf grundlegende Konzepte, die in allen Umgebungen gelten. Im Leitfaden Module im Web verwenden behandeln wir die Besonderheiten des Ladens von Modulen in Browsern, insbesondere Importbezeichner. Im Leitfaden Module auf verschiedenen Plattformen gehen wir anschließend darauf ein, wie das Modulsystem in andere Umgebungen integriert ist.
Siehe auch
- Module im Web verwenden
- Den Modulgraphen verstehen
- Module auf verschiedenen Plattformen
- JavaScript-Module auf v8.dev (2018)
- ES-Module: Eine ausführliche Erklärung als Comic auf hacks.mozilla.org (2018)
- ES6 im Detail: Module auf hacks.mozilla.org (2015)
- Exploring JS, Kapitel 16: Module von Dr. Axel Rauschmayer