JavaScript für ungeduldige Programmierer (ES2022-Ausgabe)
Bitte unterstützen Sie dieses Buch: kaufen Sie es oder spenden Sie
(Werbung, bitte nicht blockieren.)

27 Module



27.1 Spickzettel: Module

27.1.1 Exportieren

// Named exports
export function f() {}
export const one = 1;
export {foo, b as bar};

// Default exports
export default function f() {} // declaration with optional name
// Replacement for `const` (there must be exactly one value)
export default 123;

// Re-exporting from another module
export {foo, b as bar} from './some-module.mjs';
export * from './some-module.mjs';
export * as ns from './some-module.mjs'; // ES2020

27.1.2 Importieren

// Named imports
import {foo, bar as b} from './some-module.mjs';
// Namespace import
import * as someModule from './some-module.mjs';
// Default import
import someModule from './some-module.mjs';

// Combinations:
import someModule, * as someModule from './some-module.mjs';
import someModule, {foo, bar as b} from './some-module.mjs';

// Empty import (for modules with side effects)
import './some-module.mjs';

27.2 JavaScript Quellcodeformate

Die aktuelle Landschaft von JavaScript-Modulen ist ziemlich vielfältig: ES6 brachte integrierte Module, aber auch die Quellcodeformate, die davor existierten, sind noch vorhanden. Das Verständnis der letzteren hilft beim Verständnis der ersteren, also lassen Sie uns das untersuchen. Die nächsten Abschnitte beschreiben die folgenden Wege, JavaScript-Quellcode bereitzustellen.

Tabelle 18 gibt einen Überblick über diese Codeformate. Beachten Sie, dass für CommonJS-Module und ECMAScript-Module zwei Dateierweiterungen üblicherweise verwendet werden. Welche davon geeignet ist, hängt davon ab, wie wir eine Datei verwenden möchten. Details werden später in diesem Kapitel erläutert.

Tabelle 18: Wege der Bereitstellung von JavaScript-Quellcode.
Läuft auf Geladen Dateiendung
Skript Browser async .js
CommonJS-Modul Server sync .js .cjs
AMD-Modul Browser async .js
ECMAScript-Modul Browser und Server async .js .mjs

27.2.1 Code vor integrierten Modulen wurde in ECMAScript 5 geschrieben

Bevor wir zu integrierten Modulen (die mit ES6 eingeführt wurden) kommen, wird aller Code, den wir sehen werden, in ES5 geschrieben sein. Unter anderem

27.3 Bevor es Module gab, gab es Skripte

Ursprünglich hatten Browser nur Skripte – Codefragmente, die im globalen Geltungsbereich ausgeführt wurden. Als Beispiel betrachten wir eine HTML-Datei, die Skriptdateien über das folgende HTML lädt

<script src="other-module1.js"></script>
<script src="other-module2.js"></script>
<script src="my-module.js"></script>

Die Hauptdatei ist my-module.js, in der wir ein Modul simulieren

var myModule = (function () { // Open IIFE
  // Imports (via global variables)
  var importedFunc1 = otherModule1.importedFunc1;
  var importedFunc2 = otherModule2.importedFunc2;

  // Body
  function internalFunc() {
    // ···
  }
  function exportedFunc() {
    importedFunc1();
    importedFunc2();
    internalFunc();
  }

  // Exports (assigned to global variable `myModule`)
  return {
    exportedFunc: exportedFunc,
  };
})(); // Close IIFE

myModule ist eine globale Variable, der das Ergebnis der sofortigen Ausführung eines Funktionsausdrucks zugewiesen wird. Der Funktionsausdruck beginnt in der ersten Zeile. Er wird in der letzten Zeile aufgerufen.

Diese Art, ein Codefragment zu verpacken, wird Immediately Invoked Function Expression (IIFE, geprägt von Ben Alman) genannt. Was gewinnen wir durch eine IIFE? var ist nicht block-scoped (wie const und let), es ist funktions-scoped: Der einzige Weg, neue Geltungsbereiche für mit var deklarierte Variablen zu schaffen, ist über Funktionen oder Methoden (mit const und let können wir Funktionen, Methoden oder Blöcke {} verwenden). Daher versteckt die IIFE im Beispiel alle folgenden Variablen vor dem globalen Geltungsbereich und minimiert Namenskonflikte: importedFunc1, importedFunc2, internalFunc, exportedFunc.

Beachten Sie, dass wir eine IIFE auf besondere Weise verwenden: Am Ende wählen wir aus, was wir exportieren wollen, und geben es über ein Objekt-Literal zurück. Das nennt man das Revealing Module Pattern (geprägt von Christian Heilmann).

Diese Art der Simulation von Modulen hat mehrere Probleme

27.4 Modulsysteme vor ES6

Vor ECMAScript 6 hatte JavaScript keine integrierten Module. Daher wurde die flexible Syntax der Sprache verwendet, um benutzerdefinierte Modulsysteme innerhalb der Sprache zu implementieren. Zwei beliebte sind

27.4.1 Serverseitig: CommonJS-Module

Der ursprüngliche CommonJS-Standard für Module wurde für Server- und Desktop-Plattformen entwickelt. Er war die Grundlage des ursprünglichen Node.js-Modulsystems, wo er enorme Popularität erlangte. Zu dieser Popularität trugen der npm-Paketmanager für Node und Werkzeuge bei, die die Verwendung von Node-Modulen auf der Clientseite ermöglichten (browserify, webpack und andere).

Von nun an bedeutet CommonJS-Modul die Node.js-Version dieses Standards (die einige zusätzliche Features hat). Dies ist ein Beispiel für ein CommonJS-Modul

// Imports
var importedFunc1 = require('./other-module1.js').importedFunc1;
var importedFunc2 = require('./other-module2.js').importedFunc2;

// Body
function internalFunc() {
  // ···
}
function exportedFunc() {
  importedFunc1();
  importedFunc2();
  internalFunc();
}

// Exports
module.exports = {
  exportedFunc: exportedFunc,
};

CommonJS kann wie folgt charakterisiert werden

27.4.2 Clientseitig: AMD (Asynchronous Module Definition) Module

Das AMD-Modulformat wurde entwickelt, um in Browsern einfacher zu verwenden zu sein als das CommonJS-Format. Seine beliebteste Implementierung ist RequireJS. Das Folgende ist ein Beispiel für ein AMD-Modul.

define(['./other-module1.js', './other-module2.js'],
  function (otherModule1, otherModule2) {
    var importedFunc1 = otherModule1.importedFunc1;
    var importedFunc2 = otherModule2.importedFunc2;

    function internalFunc() {
      // ···
    }
    function exportedFunc() {
      importedFunc1();
      importedFunc2();
      internalFunc();
    }
    
    return {
      exportedFunc: exportedFunc,
    };
  });

AMD kann wie folgt charakterisiert werden

Auf der positiven Seite können AMD-Module direkt ausgeführt werden. Im Gegensatz dazu müssen CommonJS-Module entweder vor der Bereitstellung kompiliert werden oder benutzerdefinierter Quellcode muss generiert und dynamisch ausgewertet werden (denken Sie an eval()). Das ist im Web nicht immer erlaubt.

27.4.3 Merkmale von JavaScript-Modulen

Wenn wir uns CommonJS und AMD ansehen, tauchen Gemeinsamkeiten zwischen JavaScript-Modulsystemen auf

27.5 ECMAScript-Module

ECMAScript-Module (ES-Module oder ESM) wurden mit ES6 eingeführt. Sie setzen die Tradition der JavaScript-Module fort und haben all ihre oben genannten Merkmale. Zusätzlich

ES-Module haben auch neue Vorteile

Dies ist ein Beispiel für die ES-Modulsyntax

import {importedFunc1} from './other-module1.mjs';
import {importedFunc2} from './other-module2.mjs';

function internalFunc() {
  ···
}

export function exportedFunc() {
  importedFunc1();
  importedFunc2();
  internalFunc();
}

Von nun an bedeutet "Modul" "ECMAScript-Modul".

27.5.1 ES-Module: Syntax, Semantik, Loader-API

Der vollständige Standard von ES-Modulen umfasst die folgenden Teile

  1. Syntax (wie Code geschrieben wird): Was ist ein Modul? Wie werden Importe und Exporte deklariert? Etc.
  2. Semantik (wie Code ausgeführt wird): Wie werden Variablenbindungen exportiert? Wie werden Importe mit Exporten verbunden? Etc.
  3. Eine programmatische Loader-API zur Konfiguration des Modulladens.

Teile 1 und 2 wurden mit ES6 eingeführt. An Teil 3 wird gearbeitet.

27.6 Benannte Exporte und Importe

27.6.1 Benannte Exporte

Jedes Modul kann null oder mehr benannte Exporte haben.

Als Beispiel betrachten wir die folgenden beiden Dateien

lib/my-math.mjs
main.mjs

Das Modul my-math.mjs hat zwei benannte Exporte: square und LIGHTSPEED.

// Not exported, private to module
function times(a, b) {
  return a * b;
}
export function square(x) {
  return times(x, x);
}
export const LIGHTSPEED = 299792458;

Um etwas zu exportieren, stellen wir das Schlüsselwort export vor eine Deklaration. Entitäten, die nicht exportiert werden, sind privat für ein Modul und können von außen nicht darauf zugegriffen werden.

27.6.2 Benannte Importe

Das Modul main.mjs hat einen einzigen benannten Import, square

import {square} from './lib/my-math.mjs';
assert.equal(square(3), 9);

Es kann auch seinen Import umbenennen

import {square as sq} from './lib/my-math.mjs';
assert.equal(sq(3), 9);
27.6.2.1 Syntaktischer Fallstrick: Benanntes Importieren ist keine Destrukturierung

Sowohl benanntes Importieren als auch Destrukturierung sehen ähnlich aus

import {foo} from './bar.mjs'; // import
const {foo} = require('./bar.mjs'); // destructuring

Aber sie sind ganz anders

  Übung: Benannte Exporte

exercises/modules/export_named_test.mjs

27.6.3 Namespace-Importe

Namespace-Importe sind eine Alternative zu benannten Importen. Wenn wir ein Modul als Namespace importieren, wird es zu einem Objekt, dessen Eigenschaften die benannten Exporte sind. So sieht main.mjs aus, wenn wir einen Namespace-Import verwenden

import * as myMath from './lib/my-math.mjs';
assert.equal(myMath.square(3), 9);

assert.deepEqual(
  Object.keys(myMath), ['LIGHTSPEED', 'square']);

27.6.4 Benannte Exportstile: Inline gegen Klausel (Fortgeschrittene)

Der bisher gesehene benannte Exportstil war inline: Wir exportierten Entitäten, indem wir ihnen das Schlüsselwort export voranstellten.

Aber wir können auch separate Exportklauseln verwenden. Zum Beispiel sieht lib/my-math.mjs mit einer Exportklausel so aus

function times(a, b) {
  return a * b;
}
function square(x) {
  return times(x, x);
}
const LIGHTSPEED = 299792458;

export { square, LIGHTSPEED }; // semicolon!

Mit einer Exportklausel können wir vor dem Export umbenennen und intern unterschiedliche Namen verwenden.

function times(a, b) {
  return a * b;
}
function sq(x) {
  return times(x, x);
}
const LS = 299792458;

export {
  sq as square,
  LS as LIGHTSPEED, // trailing comma is optional
};

27.7 Standardexporte und -importe

Jedes Modul kann höchstens einen Standardexport haben. Die Idee ist, dass das Modul der Standard-exportierte Wert ist.

  Vermeiden Sie die Mischung von benannten und Standardexporten

Ein Modul kann sowohl benannte Exporte als auch einen Standardexport haben, aber es ist normalerweise besser, sich pro Modul auf einen Exportstil zu beschränken.

Als Beispiel für Standardexporte betrachten wir die folgenden beiden Dateien

my-func.mjs
main.mjs

Das Modul my-func.mjs hat einen Standardexport

const GREETING = 'Hello!';
export default function () {
  return GREETING;
}

Das Modul main.mjs importiert die exportierte Funktion standardmäßig.

import myFunc from './my-func.mjs';
assert.equal(myFunc(), 'Hello!');

Beachten Sie den syntaktischen Unterschied: Die geschweiften Klammern um benannte Importe zeigen an, dass wir in das Modul hineingreifen, während ein Standardimport das Modul ist.

  Was sind Anwendungsfälle für Standardexporte?

Der häufigste Anwendungsfall für einen Standardexport ist ein Modul, das eine einzelne Funktion oder eine einzelne Klasse enthält.

27.7.1 Die zwei Stile des Standardexportierens

Es gibt zwei Stile für die Standardexporte.

Erstens können wir bestehende Deklarationen mit export default kennzeichnen.

export default function myFunc() {} // no semicolon!
export default class MyClass {} // no semicolon!

Zweitens können wir Werte direkt als Standard exportieren. Dieser Stil von export default ist sehr ähnlich einer Deklaration.

export default myFunc; // defined elsewhere
export default MyClass; // defined previously
export default Math.sqrt(2); // result of invocation is default-exported
export default 'abc' + 'def';
export default { no: false, yes: true };
27.7.1.1 Warum gibt es zwei Stile des Standardexports?

Der Grund dafür ist, dass export default nicht verwendet werden kann, um const zu kennzeichnen: const kann mehrere Werte definieren, aber export default benötigt genau einen Wert. Betrachten Sie den folgenden hypothetischen Code

// Not legal JavaScript!
export default const foo = 1, bar = 2, baz = 3;

Mit diesem Code wissen wir nicht, welcher der drei Werte der Standardexport ist.

  Übung: Standardexporte

exercises/modules/export_default_test.mjs

27.7.2 Der Standardexport als benannter Export (Fortgeschrittene)

Intern ist ein Standardexport einfach ein benannter Export, dessen Name default ist. Als Beispiel betrachten wir das vorherige Modul my-func.mjs mit einem Standardexport

const GREETING = 'Hello!';
export default function () {
  return GREETING;
}

Das folgende Modul my-func2.mjs ist äquivalent zu diesem Modul.

const GREETING = 'Hello!';
function greet() {
  return GREETING;
}

export {
  greet as default,
};

Für den Import können wir einen normalen Standardimport verwenden.

import myFunc from './my-func2.mjs';
assert.equal(myFunc(), 'Hello!');

Oder wir können einen benannten Import verwenden.

import {default as myFunc} from './my-func2.mjs';
assert.equal(myFunc(), 'Hello!');

Der Standardexport ist auch über die Eigenschaft .default von Namespace-Importen verfügbar.

import * as mf from './my-func2.mjs';
assert.equal(mf.default(), 'Hello!');

  Ist default als Variablenname illegal?

default kann kein Variablenname sein, aber es kann ein Exportname und eine Eigenschaft sein.

const obj = {
  default: 123,
};
assert.equal(obj.default, 123);

27.8 Weitere Details zu Export und Import

27.8.1 Importe sind schreibgeschützte Ansichten auf Exporte

Bisher haben wir Importe und Exporte intuitiv verwendet, und alles schien wie erwartet funktioniert zu haben. Aber jetzt ist es an der Zeit, genauer hinzusehen, wie Importe und Exporte wirklich zusammenhängen.

Betrachten wir die folgenden beiden Module

counter.mjs
main.mjs

counter.mjs exportiert eine (veränderbare!) Variable und eine Funktion.

export let counter = 3;
export function incCounter() {
  counter++;
}

main.mjs importiert beide Exporte namentlich. Wenn wir incCounter() verwenden, stellen wir fest, dass die Verbindung zu counter live ist – wir können immer auf den Live-Status dieser Variablen zugreifen.

import { counter, incCounter } from './counter.mjs';

// The imported value `counter` is live
assert.equal(counter, 3);
incCounter();
assert.equal(counter, 4);

Beachten Sie, dass wir, obwohl die Verbindung live ist und wir counter lesen können, diese Variable nicht ändern können (z. B. über counter++).

Es gibt zwei Vorteile bei dieser Handhabung von Importen

27.8.2 ESM's transparente Unterstützung für zyklische Importe (Fortgeschrittene)

ESM unterstützt zyklische Importe transparent. Um zu verstehen, wie das erreicht wird, betrachten wir das folgende Beispiel: Abb. 7 zeigt einen gerichteten Graphen von Modulen, die andere Module importieren. P, das M importiert, ist in diesem Fall der Zyklus.

Figure 7: A directed graph of modules importing modules: M imports N and O, N imports P and Q, etc.

Nach dem Parsen werden diese Module in zwei Phasen eingerichtet

Dieser Ansatz behandelt zyklische Importe korrekt, aufgrund zweier Merkmale von ES-Modulen

27.9 npm-Pakete

Das npm-Software-Repository ist der dominierende Weg, JavaScript-Bibliotheken und Apps für Node.js und Webbrowser zu verteilen. Es wird über den npm-Paketmanager (kurz: npm) verwaltet. Software wird als sogenannte Pakete vertrieben. Ein Paket ist ein Verzeichnis, das beliebige Dateien und eine Datei package.json auf oberster Ebene enthält, die das Paket beschreibt. Zum Beispiel erhalten wir, wenn npm ein leeres Paket in einem Verzeichnis my-package/ erstellt, diese package.json

{
  "name": "my-package",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Einige dieser Eigenschaften enthalten einfache Metadaten

Andere Eigenschaften ermöglichen erweiterte Konfigurationen

Weitere Informationen zu package.json finden Sie in der npm-Dokumentation.

27.9.1 Pakete werden in einem Verzeichnis node_modules/ installiert

npm installiert Pakete immer in einem Verzeichnis node_modules. Davon gibt es normalerweise viele. Welches npm verwendet, hängt vom Verzeichnis ab, in dem man sich gerade befindet. Wenn wir uns zum Beispiel in einem Verzeichnis /tmp/a/b/ befinden, versucht npm, ein node_modules im aktuellen Verzeichnis, im übergeordneten Verzeichnis, im übergeordneten Verzeichnis des übergeordneten Verzeichnisses usw. zu finden. Mit anderen Worten, es durchsucht die folgende Kette von Orten

Beim Installieren eines Pakets some-pkg verwendet npm das nächstgelegene node_modules. Wenn sich zum Beispiel in /tmp/a/b/ ein node_modules in diesem Verzeichnis befindet, legt npm das Paket im Verzeichnis ab

/tmp/a/b/node_modules/some-pkg/

Beim Importieren eines Moduls können wir einen speziellen Modulspezifizierer verwenden, um Node.js mitzuteilen, dass wir es aus einem installierten Paket importieren möchten. Wie genau das funktioniert, wird später erklärt. Vorerst betrachten wir das folgende Beispiel

// /home/jane/proj/main.mjs
import * as theModule from 'the-package/the-module.mjs';

Um the-module.mjs (Node.js bevorzugt die Dateierweiterung .mjs für ES-Module) zu finden, durchläuft Node.js die node_module-Kette und sucht an folgenden Orten

27.9.2 Warum kann npm zum Installieren von Frontend-Bibliotheken verwendet werden?

Das Finden von installierten Modulen in node_modules-Verzeichnissen wird nur auf Node.js unterstützt. Warum können wir also auch npm verwenden, um Bibliotheken für Browser zu installieren?

Das wird durch Bundling-Tools wie webpack ermöglicht, die Code vor der Online-Bereitstellung kompilieren und optimieren. Während dieses Kompilierungsprozesses wird der Code in npm-Paketen so angepasst, dass er in Browsern funktioniert.

27.10 Module benennen

Es gibt keine etablierten Best Practices für die Benennung von Moduldateien und den Variablen, in die sie importiert werden.

In diesem Kapitel verwende ich den folgenden Benennungsstil

Was sind die Begründungen hinter diesem Stil?

Ich mag auch Unterstrich-cased Moduldateinamen, da wir diese Namen direkt für Namespace-Importe verwenden können (ohne Übersetzung).

import * as my_module from './my_module.mjs';

Aber dieser Stil funktioniert nicht für Standardimporte: Ich mag Unterstrich-casing für Namespace-Objekte, aber es ist keine gute Wahl für Funktionen usw.

27.11 Modulspezifizierer

Modulspezifizierer sind die Zeichenketten, die Module identifizieren. Sie funktionieren leicht unterschiedlich in Browsern und Node.js. Bevor wir uns die Unterschiede ansehen können, müssen wir die verschiedenen Kategorien von Modulspezifizierern lernen.

27.11.1 Kategorien von Modulspezifizierern

In ES-Modulen unterscheiden wir folgende Kategorien von Spezifizierern. Diese Kategorien stammen von CommonJS-Modulen.

27.11.2 ES-Modulspezifizierer in Browsern

Browser verarbeiten Modulspezifizierer wie folgt

Beachten Sie, dass Bundling-Tools wie webpack, die Module in weniger Dateien zusammenfassen, oft weniger streng mit Spezifizierern umgehen als Browser. Das liegt daran, dass sie zur Build-/Kompilierungszeit (nicht zur Laufzeit) arbeiten und Dateien durch Durchqueren des Dateisystems suchen können.

27.11.3 ES-Modulspezifizierer auf Node.js

Node.js verarbeitet Modulspezifizierer wie folgt

Alle Spezifizierer, außer Bare Paths, müssen sich auf tatsächliche Dateien beziehen. Das heißt, ESM unterstützt nicht die folgenden CommonJS-Features

Alle integrierten Node.js-Module sind über Bare Paths verfügbar und haben benannte ESM-Exporte – zum Beispiel

import * as assert from 'assert/strict';
import * as path from 'path';

assert.equal(
  path.join('a/b/c', '../d'), 'a/b/d');
27.11.3.1 Dateierweiterungen unter Node.js

Node.js unterstützt die folgenden Standard-Dateierweiterungen

Die Dateierweiterung .js steht entweder für ESM oder CommonJS. Welches es ist, wird über die "nächstgelegene" package.json (im aktuellen Verzeichnis, im übergeordneten Verzeichnis usw.) konfiguriert. Die Verwendung von package.json auf diese Weise ist unabhängig von Paketen.

In dieser package.json gibt es eine Eigenschaft "type" mit zwei Einstellungen

27.11.3.2 Interpretation von Nicht-Datei-Quellcode als CommonJS oder ESM

Nicht aller von Node.js ausgeführte Quellcode stammt aus Dateien. Wir können ihm auch Code über stdin, --eval und --print senden. Die Kommandozeilenoption --input-type ermöglicht es uns, anzugeben, wie solcher Code interpretiert wird.

27.12 import.meta – Metadaten für das aktuelle Modul [ES2020]

Das Objekt import.meta enthält Metadaten für das aktuelle Modul.

27.12.1 import.meta.url

Die wichtigste Eigenschaft von import.meta ist .url, die eine Zeichenkette mit der URL der Datei des aktuellen Moduls enthält – zum Beispiel

'https://example.com/code/main.mjs'

27.12.2 import.meta.url und die Klasse URL

Die Klasse URL ist in Browsern und auf Node.js über eine globale Variable verfügbar. Wir können ihre vollständige Funktionalität in der Node.js-Dokumentation nachschlagen. Wenn wir mit import.meta.url arbeiten, ist ihr Konstruktor besonders nützlich.

new URL(input: string, base?: string|URL)

Der Parameter input enthält die zu parsende URL. Er kann relativ sein, wenn der zweite Parameter, base, angegeben wird.

Mit anderen Worten, dieser Konstruktor ermöglicht es uns, einen relativen Pfad gegen eine Basis-URL aufzulösen.

> new URL('other.mjs', 'https://example.com/code/main.mjs').href
'https://example.com/code/other.mjs'
> new URL('../other.mjs', 'https://example.com/code/main.mjs').href
'https://example.com/other.mjs'

So erhalten wir eine URL-Instanz, die auf eine Datei data.txt zeigt, die sich neben dem aktuellen Modul befindet.

const urlOfData = new URL('data.txt', import.meta.url);

27.12.3 import.meta.url auf Node.js

Auf Node.js ist import.meta.url immer eine Zeichenkette mit einer file:-URL – zum Beispiel

'file:///Users/rauschma/my-module.mjs'
27.12.3.1 Beispiel: Lesen einer Geschwisterdatei eines Moduls

Viele Node.js-Dateisystemoperationen akzeptieren entweder Zeichenketten mit Pfaden oder Instanzen von URL. Das ermöglicht es uns, eine Geschwisterdatei data.txt des aktuellen Moduls zu lesen.

import * as fs from 'fs';
function readData() {
  // data.txt sits next to current module
  const urlOfData = new URL('data.txt', import.meta.url);
  return fs.readFileSync(urlOfData, {encoding: 'UTF-8'});
}
27.12.3.2 Modul fs und URLs

Für die meisten Funktionen des Moduls fs können wir auf Dateien verweisen über

Weitere Informationen zu diesem Thema finden Sie in der Node.js-API-Dokumentation.

27.12.3.3 Konvertierung zwischen file: URLs und Pfaden

Das Node.js-Modul url hat zwei Funktionen zur Konvertierung zwischen file: URLs und Pfaden

Wenn wir einen Pfad benötigen, der im lokalen Dateisystem verwendet werden kann, funktioniert die Eigenschaft .pathname von URL-Instanzen nicht immer.

assert.equal(
  new URL('file:///tmp/with%20space.txt').pathname,
  '/tmp/with%20space.txt');

Daher ist es besser, fileURLToPath() zu verwenden.

import * as url from 'url';
assert.equal(
  url.fileURLToPath('file:///tmp/with%20space.txt'),
  '/tmp/with space.txt'); // result on Unix

Ebenso leistet pathToFileURL() mehr, als nur 'file://' einem absoluten Pfad voranzustellen.

27.13 Module dynamisch laden über import() [ES2020] (Fortgeschrittene)

  Der import()-Operator verwendet Promises

Promises sind eine Technik zur Handhabung von Ergebnissen, die asynchron (d. h. nicht sofort) berechnet werden. Sie werden in §40 "Promises für asynchrone Programmierung [ES6]" erklärt. Es kann sinnvoll sein, das Lesen dieses Abschnitts zu verschieben, bis Sie sie verstehen.

27.13.1 Die Einschränkungen statischer import-Anweisungen

Bisher war die einzige Möglichkeit, ein Modul zu importieren, über eine import-Anweisung. Diese Anweisung hat mehrere Einschränkungen

27.13.2 Dynamische Importe über den import()-Operator

Der import()-Operator hat nicht die Einschränkungen von import-Anweisungen. Er sieht so aus:

import(moduleSpecifierStr)
.then((namespaceObject) => {
  console.log(namespaceObject.namedExport);
});

Dieser Operator wird wie eine Funktion verwendet, empfängt einen String mit einem Modulspezifizierer und gibt ein Promise zurück, das zu einem Namespace-Objekt aufgelöst wird. Die Eigenschaften dieses Objekts sind die Exporte des importierten Moduls.

import() ist über await sogar noch einfacher zu verwenden.

const namespaceObject = await import(moduleSpecifierStr);
console.log(namespaceObject.namedExport);

Beachten Sie, dass await auf der obersten Ebene von Modulen verwendet werden kann (siehe nächster Abschnitt).

Betrachten wir ein Beispiel für die Verwendung von import().

27.13.2.1 Beispiel: Modul dynamisch laden

Betrachten Sie die folgenden Dateien:

lib/my-math.mjs
main1.mjs
main2.mjs

Das Modul my-math.mjs haben wir bereits gesehen.

// Not exported, private to module
function times(a, b) {
  return a * b;
}
export function square(x) {
  return times(x, x);
}
export const LIGHTSPEED = 299792458;

Wir können import() verwenden, um dieses Modul bei Bedarf zu laden.

// main1.mjs
const moduleSpecifier = './lib/my-math.mjs';

function mathOnDemand() {
  return import(moduleSpecifier)
  .then(myMath => {
    const result = myMath.LIGHTSPEED;
    assert.equal(result, 299792458);
    return result;
  });
}

mathOnDemand()
.then((result) => {
  assert.equal(result, 299792458);
});

Zwei Dinge in diesem Code können nicht mit import-Anweisungen gemacht werden:

Als Nächstes implementieren wir dieselbe Funktionalität wie in main1.mjs, aber über ein Feature namens asynchrone Funktion oder async/await, das eine schönere Syntax für Promises bietet.

// main2.mjs
const moduleSpecifier = './lib/my-math.mjs';

async function mathOnDemand() {
  const myMath = await import(moduleSpecifier);
  const result = myMath.LIGHTSPEED;
  assert.equal(result, 299792458);
  return result;
}

  Warum ist import() ein Operator und keine Funktion?

import() sieht aus wie eine Funktion, konnte aber nicht als Funktion implementiert werden.

27.13.3 Anwendungsfälle für import()

27.13.3.1 Code bei Bedarf laden

Einige Funktionalitäten von Web-Apps müssen nicht vorhanden sein, wenn sie starten, sie können bei Bedarf geladen werden. Dann hilft import(), da wir solche Funktionalitäten in Module packen können – zum Beispiel:

button.addEventListener('click', event => {
  import('./dialogBox.mjs')
    .then(dialogBox => {
      dialogBox.open();
    })
    .catch(error => {
      /* Error handling */
    })
});
27.13.3.2 Bedingtes Laden von Modulen

Wir möchten möglicherweise ein Modul laden, je nachdem, ob eine Bedingung wahr ist. Zum Beispiel ein Modul mit einem Polyfill, das ein neues Feature auf älteren Plattformen verfügbar macht.

if (isLegacyPlatform()) {
  import('./my-polyfill.mjs')
    .then(···);
}
27.13.3.3 Berechnete Modulspezifizierer

Für Anwendungen wie die Internationalisierung ist es hilfreich, Modulspezifizierer dynamisch berechnen zu können.

import(`messages_${getLocale()}.mjs`)
  .then(···);

27.14 Top-Level await in Modulen [ES2022] (Fortgeschritten)

  await ist ein Feature von asynchronen Funktionen

await wird in §41 „Asynchrone Funktionen“ erklärt. Es kann sinnvoll sein, das Lesen dieses Abschnitts aufzuschieben, bis Sie asynchrone Funktionen verstehen.

Wir können den await-Operator auf der obersten Ebene eines Moduls verwenden. Wenn wir das tun, wird das Modul asynchron und funktioniert anders. Glücklicherweise sehen wir das als Programmierer normalerweise nicht, da es transparent von der Sprache gehandhabt wird.

27.14.1 Anwendungsfälle für Top-Level await

Warum sollten wir den await-Operator auf der obersten Ebene eines Moduls verwenden wollen? Er erlaubt uns, ein Modul mit asynchron geladenen Daten zu initialisieren. Die nächsten drei Unterabschnitte zeigen drei Beispiele, wo das nützlich ist.

27.14.1.1 Module dynamisch laden
const params = new URLSearchParams(location.search);
const language = params.get('lang');
const messages = await import(`./messages-${language}.mjs`); // (A)

console.log(messages.welcome);

In Zeile A importieren wir dynamisch ein Modul. Dank Top-Level await ist das fast so bequem wie die Verwendung eines normalen, statischen Imports.

27.14.1.2 Fallback verwenden, wenn das Laden des Moduls fehlschlägt
let lodash;
try {
  lodash = await import('https://primary.example.com/lodash');
} catch {
  lodash = await import('https://secondary.example.com/lodash');
}
27.14.1.3 Welche Ressource zuerst geladen wird, verwenden
const resource = await Promise.any([
  fetch('http://example.com/first.txt')
    .then(response => response.text()),
  fetch('http://example.com/second.txt')
    .then(response => response.text()),
]);

Dank Promise.any() wird die Variable resource über den Download initialisiert, der zuerst abgeschlossen wird.

27.14.2 Wie funktioniert Top-Level await intern?

Betrachten Sie die folgenden beiden Dateien.

first.mjs:

const response = await fetch('http://example.com/first.txt');
export const first = await response.text();

main.mjs:

import {first} from './first.mjs';
import {second} from './second.mjs';
assert.equal(first, 'First!');
assert.equal(second, 'Second!');

Beide sind ungefähr äquivalent zu folgendem Code:

first.mjs:

export let first;
export const promise = (async () => { // (A)
  const response = await fetch('http://example.com/first.txt');
  first = await response.text();
})();

main.mjs:

import {promise as firstPromise, first} from './first.mjs';
import {promise as secondPromise, second} from './second.mjs';
export const promise = (async () => { // (B)
  await Promise.all([firstPromise, secondPromise]); // (C)
  assert.equal(first, 'First content!');
  assert.equal(second, 'Second content!');
})();

Ein Modul wird asynchron, wenn

  1. Es verwendet direkt Top-Level await (first.mjs).
  2. Es importiert ein oder mehrere asynchrone Module (main.mjs).

Jedes asynchrone Modul exportiert ein Promise (Zeile A und Zeile B), das nach der Ausführung seines Körpers erfüllt wird. Zu diesem Zeitpunkt ist es sicher, auf die Exporte dieses Moduls zuzugreifen.

In Fall (2) wartet das importierende Modul, bis die Promises aller importierten asynchronen Module erfüllt sind, bevor es seinen Körper (Zeile C) betritt. Synchrone Module werden wie üblich behandelt.

Abgefangene Fehler und synchrone Ausnahmen werden wie in asynchronen Funktionen gehandhabt.

27.14.3 Die Vor- und Nachteile von Top-Level await

Die beiden wichtigsten Vorteile von Top-Level await sind:

Auf der negativen Seite verzögert Top-Level await die Initialisierung von importierenden Modulen. Daher sollte es sparsam eingesetzt werden. Asynchrone Aufgaben, die länger dauern, werden besser später, bei Bedarf ausgeführt.

Allerdings können auch Module ohne Top-Level await Importierende blockieren (z. B. durch eine Endlosschleife auf der obersten Ebene), so dass Blockierung an sich kein Argument dagegen ist.

27.15 Polyfills: Emulieren nativer Webplattform-Features (Fortgeschritten)

  Backends haben auch Polyfills

Dieser Abschnitt befasst sich mit Frontend-Entwicklung und Webbrowsern, aber ähnliche Ideen gelten für die Backend-Entwicklung.

Polyfills helfen bei einem Konflikt, dem wir bei der Entwicklung einer Webanwendung in JavaScript gegenüberstehen.

Gegeben sei ein Webplattform-Feature X.

Jedes Mal, wenn unsere Webanwendung startet, muss sie zuerst alle Polyfills für Features ausführen, die möglicherweise nicht überall verfügbar sind. Danach können wir sicher sein, dass diese Features nativ verfügbar sind.

27.15.1 Quellen dieses Abschnitts

  Quiz

Siehe Quiz-App.