import.meta – Metadaten für das aktuelle Modul [ES2020]import.meta.urlimport.meta.url und die Klasse URLimport.meta.url auf Node.jsimport() [ES2020] (Fortgeschrittene)await in Modulen [ES2022] (Fortgeschrittene)// 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// 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';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.
| 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 |
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
const und let, nur var.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 IIFEmyModule 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
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
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
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.
Wenn wir uns CommonJS und AMD ansehen, tauchen Gemeinsamkeiten zwischen JavaScript-Modulsystemen auf
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".
Der vollständige Standard von ES-Modulen umfasst die folgenden Teile
Teile 1 und 2 wurden mit ES6 eingeführt. An Teil 3 wird gearbeitet.
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.
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);Sowohl benanntes Importieren als auch Destrukturierung sehen ähnlich aus
import {foo} from './bar.mjs'; // import
const {foo} = require('./bar.mjs'); // destructuringAber sie sind ganz anders
Importe bleiben mit ihren Exporten verbunden.
Wir können erneut innerhalb eines Destrukturierungsmusters destrukturieren, aber die {} in einer Importanweisung können nicht verschachtelt werden.
Die Syntax für die Umbenennung ist anders
import {foo as f} from './bar.mjs'; // importing
const {foo: f} = require('./bar.mjs'); // destructuringBegründung: Destrukturierung erinnert an ein Objekt-Literal (einschließlich Verschachtelung), während das Importieren die Idee der Umbenennung hervorruft.
Übung: Benannte Exporte
exercises/modules/export_named_test.mjs
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']);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
};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.
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 };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
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);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
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.
Nach dem Parsen werden diese Module in zwei Phasen eingerichtet
Dieser Ansatz behandelt zyklische Importe korrekt, aufgrund zweier Merkmale von ES-Modulen
Aufgrund der statischen Struktur von ES-Modulen sind die Exporte bereits nach dem Parsen bekannt. Das ermöglicht es, P zu instanziieren, bevor sein Kind M instanziiert wird: P kann bereits auf die Exporte von M zugreifen.
Wenn P ausgewertet wird, wurde M noch nicht ausgewertet. Entitäten in P können jedoch bereits Importe aus M erwähnen. Sie können sie nur noch nicht verwenden, da die importierten Werte später eingefügt werden. Zum Beispiel kann eine Funktion in P auf einen Import aus M zugreifen. Die einzige Einschränkung ist, dass wir warten müssen, bis M ausgewertet wurde, bevor wir diese Funktion aufrufen.
Das spätere Einfügen von Importen wird dadurch ermöglicht, dass sie "live immutable views" auf Exporte sind.
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
name gibt den Namen dieses Pakets an. Sobald es im npm-Repository hochgeladen ist, kann es über npm install my-package installiert werden.version wird für die Versionsverwaltung verwendet und folgt semantischer Versionierung, mit drei Zahlendescription, keywords, author erleichtern das Finden von Paketen.license klärt, wie wir dieses Paket verwenden dürfen.Andere Eigenschaften ermöglichen erweiterte Konfigurationen
main: gibt das Modul an, das das Paket "ist" (wird später in diesem Kapitel erklärt).scripts: sind Befehle, die wir über npm run ausführen können. Zum Beispiel kann das Skript test über npm run test ausgeführt werden.Weitere Informationen zu package.json finden Sie in der npm-Dokumentation.
node_modules/ installiertnpm 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
/tmp/a/b/node_modules/tmp/a/node_modules/tmp/node_modulesBeim 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
/home/jane/proj/node_modules/the-package/the-module.mjs/home/jane/node_modules/the-package/the-module.mjs/home/node_modules/the-package/the-module.mjsDas 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.
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
Die Namen von Moduldateien sind Dash-cased und beginnen mit Kleinbuchstaben.
./my-module.mjs
./some-func.mjsDie Namen von Namespace-Importen sind kleingeschrieben und Camel-cased.
import * as myModule from './my-module.mjs';Die Namen von Standardimporten sind kleingeschrieben und Camel-cased.
import someFunc from './some-func.mjs';Was sind die Begründungen hinter diesem Stil?
npm erlaubt keine Großbuchstaben in Paketnamen (Quelle). Daher vermeiden wir Camel Case, damit "lokale" Dateien Namen haben, die mit denen von npm-Paketen konsistent sind. Nur Kleinbuchstaben zu verwenden, minimiert auch Konflikte zwischen Dateisystemen, die case-sensitiv sind, und Dateisystemen, die es nicht sind: Erstere unterscheiden Dateien, deren Namen die gleichen Buchstaben mit unterschiedlicher Groß-/Kleinschreibung haben; letztere nicht.
Es gibt klare Regeln für die Umwandlung von Dash-cased Dateinamen in Camel-cased JavaScript-Variablennamen. Aufgrund der Art und Weise, wie wir Namespace-Importe benennen, funktionieren diese Regeln sowohl für Namespace-Importe als auch für Standardimporte.
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.
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.
In ES-Modulen unterscheiden wir folgende Kategorien von Spezifizierern. Diese Kategorien stammen von CommonJS-Modulen.
Relativer Pfad: beginnt mit einem Punkt. Beispiele.
'./some/other/module.mjs'
'../../lib/counter.mjs'Absoluter Pfad: beginnt mit einem Schrägstrich. Beispiel.
'/home/jane/file-tools.mjs'URL: enthält ein Protokoll (technisch gesehen sind Pfade auch URLs). Beispiele.
'https://example.com/some-module.mjs'
'file:///home/john/tmp/main.mjs'Bare path: beginnt nicht mit einem Punkt, einem Schrägstrich oder einem Protokoll und besteht aus einem einzelnen Dateinamen ohne Erweiterung. Beispiele.
'lodash'
'the-package'Deep import path: beginnt mit einem Bare Path und hat mindestens einen Schrägstrich. Beispiel.
'the-package/dist/the-module.mjs'Browser verarbeiten Modulspezifizierer wie folgt
text/javascript ausgeliefert werden.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.
Node.js verarbeitet Modulspezifizierer wie folgt
Relative Pfade werden wie in Webbrowsern aufgelöst – relativ zum Pfad des aktuellen Moduls.
Absolute Pfade werden derzeit nicht unterstützt. Als Workaround können wir URLs verwenden, die mit file:/// beginnen. Solche URLs können wir über url.pathToFileURL() erstellen.
Nur file: wird als Protokoll für URL-Spezifizierer unterstützt.
Ein Bare Path wird als Paketname interpretiert und relativ zum nächstgelegenen node_modules-Verzeichnis aufgelöst. Welches Modul geladen werden soll, wird durch Betrachten der Eigenschaft "main" der package.json des Pakets bestimmt (ähnlich wie bei CommonJS).
Deep Import Paths werden ebenfalls relativ zum nächstgelegenen node_modules-Verzeichnis aufgelöst. Sie enthalten Dateinamen, sodass immer klar ist, welches Modul gemeint ist.
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
CommonJS fügt automatisch fehlende Dateierweiterungen hinzu.
CommonJS kann ein Verzeichnis dir importieren, wenn es eine dir/package.json mit einer "main"-Eigenschaft gibt.
CommonJS kann ein Verzeichnis dir importieren, wenn es ein Modul dir/index.js gibt.
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');Node.js unterstützt die folgenden Standard-Dateierweiterungen
.mjs für ES-Module.cjs für CommonJS-ModuleDie 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
"commonjs" (Standard): Dateien mit der Erweiterung .js oder ohne Erweiterung werden als CommonJS-Module interpretiert.
"module": Dateien mit der Erweiterung .js oder ohne Erweiterung werden als ESM-Module interpretiert.
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.
--input-type=commonjs--input-type=moduleimport.meta – Metadaten für das aktuelle Modul [ES2020]Das Objekt import.meta enthält Metadaten für das aktuelle Modul.
import.meta.urlDie 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'
import.meta.url und die Klasse URLDie 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);import.meta.url auf Node.jsAuf Node.js ist import.meta.url immer eine Zeichenkette mit einer file:-URL – zum Beispiel
'file:///Users/rauschma/my-module.mjs'
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'});
}fs und URLsFür die meisten Funktionen des Moduls fs können wir auf Dateien verweisen über
Buffer.URL (mit dem Protokoll file:)Weitere Informationen zu diesem Thema finden Sie in der Node.js-API-Dokumentation.
file: URLs und PfadenDas Node.js-Modul url hat zwei Funktionen zur Konvertierung zwischen file: URLs und Pfaden
fileURLToPath(url: URL|string): stringfile: URL in einen Pfad.pathToFileURL(path: string): URLfile: URL.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 UnixEbenso leistet pathToFileURL() mehr, als nur 'file://' einem absoluten Pfad voranzustellen.
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.
import-AnweisungenBisher war die einzige Möglichkeit, ein Modul zu importieren, über eine import-Anweisung. Diese Anweisung hat mehrere Einschränkungen
if-Anweisung befinden.import()-OperatorDer 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().
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.
import() eine Funktion wäre, müssten wir diese Information explizit übergeben (z. B. über einen Parameter).import()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 */
})
});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(···);
}Für Anwendungen wie die Internationalisierung ist es hilfreich, Modulspezifizierer dynamisch berechnen zu können.
import(`messages_${getLocale()}.mjs`)
.then(···);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.
awaitWarum 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.
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.
let lodash;
try {
lodash = await import('https://primary.example.com/lodash');
} catch {
lodash = await import('https://secondary.example.com/lodash');
}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.
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
await (first.mjs).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.
awaitDie 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.
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.
Quiz
Siehe Quiz-App.