Oder wie man das Verhalten von JavaScript-Importen steuert
<script>
nicht<base>
-Elementimport.meta.resolve()
Dieser Vorschlag ermöglicht die Kontrolle darüber, welche URLs von JavaScript- import
und import()
-Ausdrücken abgerufen werden. Dadurch können „bloße Importspezifizierer“ wie import moment from "moment"
funktionieren.
Der Mechanismus hierfür erfolgt über eine Importzuordnung , mit der die Auflösung von Modulspezifizierern allgemein gesteuert werden kann. Betrachten Sie als einführendes Beispiel den Code
import moment from "moment" ;
import { partition } from "lodash" ;
Heutzutage ist dies nicht der Fall, da solche bloßen Spezifizierer explizit reserviert sind. Indem Sie dem Browser die folgende Importkarte bereitstellen
< script type =" importmap " >
{
"imports" : {
"moment" : "/node_modules/moment/src/moment.js" ,
"lodash" : "/node_modules/lodash-es/lodash.js"
}
}
</ script >
Das oben Gesagte würde sich so verhalten, als ob Sie es geschrieben hätten
import moment from "/node_modules/moment/src/moment.js" ;
import { partition } from "/node_modules/lodash-es/lodash.js" ;
Weitere Informationen zum neuen "importmap"
-Wert für das type=""
-Attribut von <script>
finden Sie im Abschnitt „Installation“. Wir konzentrieren uns vorerst auf die Semantik des Mappings und verschieben die Installationsdiskussion.
Webentwickler mit Erfahrung mit Modulsystemen vor ES2015 wie CommonJS (entweder in Node oder gebündelt mit Webpack/Browserify für den Browser) sind es gewohnt, Module mit einer einfachen Syntax importieren zu können:
const $ = require ( "jquery" ) ;
const { pluck } = require ( "lodash" ) ;
Übersetzt in die Sprache des integrierten Modulsystems von JavaScript wären dies:
import $ from "jquery" ;
import { pluck } from "lodash" ;
In solchen Systemen werden diese bloßen Importspezifizierer "jquery"
oder "lodash"
vollständigen Dateinamen oder URLs zugeordnet. Genauer gesagt stellen diese Spezifizierer Pakete dar, die normalerweise auf npm verteilt werden; Indem sie nur den Namen des Pakets angeben, fordern sie implizit das Hauptmodul dieses Pakets an.
Der Hauptvorteil dieses Systems besteht darin, dass es eine einfache Koordination im gesamten Ökosystem ermöglicht. Jeder kann ein Modul schreiben und eine Importanweisung unter Verwendung des bekannten Namens eines Pakets einfügen und die Node.js-Laufzeitumgebung oder ihre Build-Time-Tools mit der Übersetzung in eine tatsächliche Datei auf der Festplatte beauftragen (einschließlich Überlegungen zur Versionierung).
Heutzutage verwenden viele Webentwickler sogar die native Modulsyntax von JavaScript, kombinieren sie jedoch mit bloßen Importspezifizierern, sodass ihr Code ohne vorherige Modifikation pro Anwendung nicht im Web ausgeführt werden kann. Wir möchten dieses Problem lösen und diese Vorteile ins Web bringen.
Wir erklären die Funktionen der Importkarte anhand einer Reihe von Beispielen.
Wie in der Einleitung erwähnt,
{
"imports" : {
"moment" : " /node_modules/moment/src/moment.js " ,
"lodash" : " /node_modules/lodash-es/lodash.js "
}
}
Bietet Unterstützung für bloße Importspezifizierer im JavaScript-Code:
import moment from "moment" ;
import ( "lodash" ) . then ( _ => ... ) ;
Beachten Sie, dass die rechte Seite der Zuordnung (bekannt als „Adresse“) mit /
, ../
oder ./
beginnen oder als absolute URL analysierbar sein muss, um eine URL zu identifizieren. Im Fall von relativen URL-ähnlichen Adressen werden sie relativ zur Basis-URL der Importkarte aufgelöst, d. h. der Basis-URL der Seite für Inline-Importkarten und der URL der Importkartenressource für externe Importkarten.
Insbesondere „nackte“ relative URLs wie node_modules/moment/src/moment.js
funktionieren an diesen Positionen vorerst nicht. Dies geschieht als konservative Standardeinstellung, da wir in Zukunft möglicherweise mehrere Importkarten zulassen möchten, was die Bedeutung der rechten Seite in einer Weise ändern könnte, die sich insbesondere auf diese nackten Fälle auswirkt.
Im JavaScript-Ökosystem ist es üblich, dass ein Paket (im Sinne von npm) mehrere Module oder andere Dateien enthält. In solchen Fällen möchten wir ein Präfix im Modulspezifiziererbereich einem anderen Präfix im Fetchable-URL-Bereich zuordnen.
Importzuordnungen tun dies, indem sie Spezifiziererschlüsseln, die mit einem abschließenden Schrägstrich enden, eine besondere Bedeutung zuweisen. Also eine Karte wie
{
"imports" : {
"moment" : " /node_modules/moment/src/moment.js " ,
"moment/" : " /node_modules/moment/src/ " ,
"lodash" : " /node_modules/lodash-es/lodash.js " ,
"lodash/" : " /node_modules/lodash-es/ "
}
}
würde es ermöglichen, nicht nur die Hauptmodule zu importieren
import moment from "moment" ;
import _ from "lodash" ;
aber auch Nicht-Hauptmodule, z
import localeData from "moment/locale/zh-cn.js" ;
import fp from "lodash/fp.js" ;
Im Node.js-Ökosystem ist es auch üblich, Dateien zu importieren, ohne die Erweiterung einzuschließen. Wir können uns nicht den Luxus leisten, mehrere Dateierweiterungen auszuprobieren, bis wir eine gute Übereinstimmung gefunden haben. Wir können jedoch etwas Ähnliches emulieren, indem wir eine Importkarte verwenden. Zum Beispiel,
{
"imports" : {
"lodash" : " /node_modules/lodash-es/lodash.js " ,
"lodash/" : " /node_modules/lodash-es/ " ,
"lodash/fp" : " /node_modules/lodash-es/fp.js " ,
}
}
würde nicht nur import fp from "lodash/fp.js"
zulassen, sondern auch den import fp from "lodash/fp"
.
Obwohl dieses Beispiel zeigt, wie es möglich ist, erweiterungslose Importe mit Importkarten zuzulassen, ist dies nicht unbedingt wünschenswert . Dadurch wird die Importkarte aufgebläht und die Schnittstelle des Pakets weniger einfach – sowohl für Menschen als auch für Tools.
Diese Aufblähung ist besonders problematisch, wenn Sie erweiterungslose Importe innerhalb eines Pakets zulassen müssen. In diesem Fall benötigen Sie einen Importzuordnungseintrag für jede Datei im Paket, nicht nur für die Einstiegspunkte der obersten Ebene. Um beispielsweise import "./fp"
aus der Datei /node_modules/lodash-es/lodash.js
zu ermöglichen, benötigen Sie einen Importeintrag /node_modules/lodash-es/fp
zu /node_modules/lodash-es/fp.js
zuordnet /node_modules/lodash-es/fp.js
. Stellen Sie sich nun vor, Sie würden dies für jede Datei wiederholen, auf die ohne Erweiterung verwiesen wird.
Daher empfehlen wir Vorsicht bei der Verwendung solcher Muster in Ihren Importkarten oder beim Schreiben von Modulen. Für das Ökosystem wird es einfacher, wenn wir uns nicht auf Importkarten verlassen, um Nichtübereinstimmungen im Zusammenhang mit Dateierweiterungen zu beheben.
Im Rahmen der Ermöglichung einer allgemeinen Neuzuordnung von Spezifizierern ermöglichen Importzuordnungen insbesondere die Neuzuordnung von URL-ähnlichen Spezifizierern wie "https://example.com/foo.mjs"
oder "./bar.mjs"
. Eine praktische Anwendung hierfür ist die Zuordnung von Hashes, aber hier zeigen wir einige grundlegende, um das Konzept zu vermitteln:
{
"imports" : {
"https://www.unpkg.com/vue/dist/vue.runtime.esm.js" : " /node_modules/vue/dist/vue.runtime.esm.js "
}
}
Diese Neuzuordnung stellt sicher, dass alle Importe der unpkg.com-Version von Vue (zumindest unter dieser URL) stattdessen die Version vom lokalen Server abrufen.
{
"imports" : {
"/app/helpers.mjs" : " /app/helpers/index.mjs "
}
}
Diese Neuzuordnung stellt sicher, dass alle URL-ähnlichen Importe, die in /app/helpers.mjs
aufgelöst werden, einschließlich z. B. eines import "./helpers.mjs"
aus Dateien in /app/
oder eines import "../helpers.mjs"
aus Dateien innerhalb von /app/models
wird stattdessen in /app/helpers/index.mjs
aufgelöst. Das ist wahrscheinlich keine gute Idee; Anstatt eine Indirektion zu erstellen, die Ihren Code verschleiert, sollten Sie einfach Ihre Quelldateien aktualisieren, um die richtigen Dateien zu importieren. Es ist jedoch ein nützliches Beispiel, um die Möglichkeiten von Importkarten zu demonstrieren.
Eine solche Neuzuordnung kann auch auf Präfix-angepasster Basis erfolgen, indem der Spezifiziererschlüssel mit einem abschließenden Schrägstrich abgeschlossen wird:
{
"imports" : {
"https://www.unpkg.com/vue/" : " /node_modules/vue/ "
}
}
Diese Version stellt sicher, dass Importanweisungen für Spezifizierer, die mit der Teilzeichenfolge "https://www.unpkg.com/vue/"
beginnen, der entsprechenden URL unter /node_modules/vue/
zugeordnet werden.
Im Allgemeinen funktioniert die Neuzuordnung bei URL-ähnlichen Importen genauso wie bei Bare-Importen. Unsere vorherigen Beispiele haben die Auflösung von Spezifizierern wie "lodash"
und damit die Bedeutung von import "lodash"
geändert. Hier ändern wir die Auflösung von Spezifizierern wie "/app/helpers.mjs"
und damit die Bedeutung von import "/app/helpers.mjs"
.
Beachten Sie, dass diese Variante der Zuordnung von URL-ähnlichen Spezifizierern mit abschließendem Schrägstrich nur funktioniert, wenn der URL-ähnliche Spezifizierer ein spezielles Schema hat: z. B. hat eine Zuordnung von "data:text/": "/foo"
keinen Einfluss auf die Bedeutung von import "data:text/javascript,console.log('test')"
, wirkt sich jedoch nur auf import "data:text/"
aus.
Skriptdateien erhalten häufig einen eindeutigen Hash im Dateinamen, um die Zwischenspeicherbarkeit zu verbessern. Sehen Sie sich diese allgemeine Diskussion der Technik oder diese eher auf JavaScript und Webpack ausgerichtete Diskussion an.
Bei Moduldiagrammen kann diese Technik problematisch sein:
Stellen Sie sich ein einfaches Moduldiagramm vor, bei dem app.mjs
von dep.mjs
und wiederum von sub-dep.mjs
abhängt. Wenn Sie sub-dep.mjs
aktualisieren oder ändern, können app.mjs
und dep.mjs
normalerweise zwischengespeichert bleiben, sodass nur die neuen sub-dep.mjs
über das Netzwerk übertragen werden müssen.
Betrachten Sie nun das gleiche Moduldiagramm und verwenden Sie gehashte Dateinamen für die Produktion. Dort generiert unser Build-Prozess app-8e0d62a03.mjs
, dep-16f9d819a.mjs
und sub-dep-7be2aa47f.mjs
aus den ursprünglichen drei Dateien.
Wenn wir sub-dep.mjs
aktualisieren oder ändern, generiert unser Build-Prozess einen neuen Dateinamen für die Produktionsversion, beispielsweise sub-dep-5f47101dc.mjs
. Dies bedeutet jedoch, dass wir die import
in der Produktionsversion von dep.mjs
ändern müssen. Dadurch ändert sich der Inhalt, was bedeutet, dass die Produktionsversion von dep.mjs
selbst einen neuen Dateinamen benötigt. Das bedeutet dann aber, dass wir die import
in der Produktionsversion von app.mjs
aktualisieren müssen ...
Das bedeutet, dass bei Moduldiagrammen und import
, die Skriptdateien mit gehashten Dateinamen enthalten, Aktualisierungen eines beliebigen Teils des Diagramms für alle seine Abhängigkeiten viral werden und alle Vorteile der Zwischenspeicherung verloren gehen.
Importzuordnungen bieten einen Ausweg aus diesem Dilemma, indem sie die Modulspezifizierer, die in import
erscheinen, von den URLs auf dem Server entkoppeln. Beispielsweise könnte unsere Website mit einer Importkarte wie beginnen
{
"imports" : {
"/js/app.mjs" : " /js/app-8e0d62a03.mjs " ,
"/js/dep.mjs" : " /js/dep-16f9d819a.mjs " ,
"/js/sub-dep.mjs" : " /js/sub-dep-7be2aa47f.mjs "
}
}
und mit Importanweisungen, die die Form import "./sub-dep.mjs"
anstelle von import "./sub-dep-7be2aa47f.mjs"
haben. Wenn wir nun sub-dep.mjs
ändern, aktualisieren wir einfach unsere Importzuordnung:
{
"imports" : {
"/js/app.mjs" : " /js/app-8e0d62a03.mjs " ,
"/js/dep.mjs" : " /js/dep-16f9d819a.mjs " ,
"/js/sub-dep.mjs" : " /js/sub-dep-5f47101dc.mjs "
}
}
und lassen Sie die import "./sub-dep.mjs"
in Ruhe. Das bedeutet, dass sich der Inhalt von dep.mjs
nicht ändert und daher im Cache bleibt; das gleiche gilt für app.mjs
.
<script>
nicht Ein wichtiger Hinweis zur Verwendung von Importzuordnungen zum Ändern der Bedeutung von Importspezifizierern besteht darin, dass dadurch die Bedeutung von Roh-URLs, wie sie beispielsweise in <script src="">
oder <link rel="modulepreload">
erscheinen, nicht geändert wird. Das heißt, angesichts des obigen Beispiels, während
import "./app.mjs" ;
würde in Browsern, die Importkarten unterstützen, korrekt auf die gehashte Version umgeleitet werden,
< script type =" module " src =" ./app.mjs " > </ script >
würde nicht: In allen Browserklassen würde es versuchen, app.mjs
direkt abzurufen, was zu einem 404 führen würde. Was in Browsern, die Importkarten unterstützen, funktionieren würde , wäre
< script type =" module " > import "./app.mjs" ; </ script >
Es kommt häufig vor, dass Sie denselben Importspezifizierer verwenden möchten, um auf mehrere Versionen einer einzelnen Bibliothek zu verweisen, je nachdem, wer sie importiert. Dies kapselt die Versionen jeder verwendeten Abhängigkeit und vermeidet die Abhängigkeitshölle (längerer Blogbeitrag).
Wir unterstützen diesen Anwendungsfall in Importkarten, indem wir Ihnen ermöglichen, die Bedeutung eines Spezifizierers innerhalb eines bestimmten Bereichs zu ändern:
{
"imports" : {
"querystringify" : " /node_modules/querystringify/index.js "
},
"scopes" : {
"/node_modules/socksjs-client/" : {
"querystringify" : " /node_modules/socksjs-client/querystringify/index.js "
}
}
}
(Dieses Beispiel ist eines von mehreren echten Beispielen für mehrere Versionen pro Anwendung, die von @zkat bereitgestellt werden. Danke, @zkat!)
Mit dieser Zuordnung verweist der Spezifizierer "querystringify"
in allen Modulen, deren URLs mit /node_modules/socksjs-client/
beginnen, auf /node_modules/socksjs-client/querystringify/index.js
. Andernfalls stellt die Zuordnung auf oberster Ebene sicher, dass "querystringify"
auf /node_modules/querystringify/index.js
verweist.
Beachten Sie, dass sich die Art und Weise, wie eine Adresse aufgelöst wird, nicht ändert, wenn sie sich in einem Bereich befindet. Die Basis-URL der Importkarte wird weiterhin verwendet, anstelle beispielsweise des Bereichs-URL-Präfixes.
Bereiche „erben“ voneinander auf bewusst einfache Weise, indem sie miteinander verschmelzen, sich aber im Laufe der Zeit überschreiben. Zum Beispiel die folgende Importkarte:
{
"imports" : {
"a" : " /a-1.mjs " ,
"b" : " /b-1.mjs " ,
"c" : " /c-1.mjs "
},
"scopes" : {
"/scope2/" : {
"a" : " /a-2.mjs "
},
"/scope2/scope3/" : {
"b" : " /b-3.mjs "
}
}
}
würde folgende Beschlüsse fassen:
Spezifizierer | Referrer | Resultierende URL |
---|---|---|
A | /scope1/foo.mjs | /a-1.mjs |
B | /scope1/foo.mjs | /b-1.mjs |
C | /scope1/foo.mjs | /c-1.mjs |
A | /scope2/foo.mjs | /a-2.mjs |
B | /scope2/foo.mjs | /b-1.mjs |
C | /scope2/foo.mjs | /c-1.mjs |
A | /scope2/scope3/foo.mjs | /a-2.mjs |
B | /scope2/scope3/foo.mjs | /b-3.mjs |
C | /scope2/scope3/foo.mjs | /c-1.mjs |
Sie können eine Importzuordnung für Ihre Anwendung mithilfe eines <script>
-Elements installieren, entweder inline oder mit einem src=""
-Attribut:
< script type =" importmap " >
{
"imports" : { ... } ,
"scopes" : { ... }
}
</ script >
< script type =" importmap " src =" import-map.importmap " > </ script >
Wenn das Attribut src=""
verwendet wird, muss die resultierende HTTP-Antwort den MIME-Typ application/importmap+json
haben. (Warum nicht application/json
wiederverwenden? Dies könnte CSP-Umgehungen ermöglichen.) Wie bei Modulskripten wird die Anfrage mit aktiviertem CORS gestellt und die Antwort wird immer als UTF-8 interpretiert.
Da sie sich auf alle Importe auswirken, müssen alle Importzuordnungen vorhanden und erfolgreich abgerufen werden, bevor eine Modulauflösung durchgeführt wird. Dies bedeutet, dass das Abrufen von Moduldiagrammen beim Abrufen von Importkarten blockiert ist.
Das bedeutet, dass die Inline-Form von Importkarten für eine optimale Leistung dringend empfohlen wird. Dies ähnelt der bewährten Vorgehensweise beim Inlining von kritischem CSS. Da beide Arten von Ressourcen Ihre Anwendung daran hindern, wichtige Aufgaben auszuführen, bis sie verarbeitet werden, ist die Einführung eines zweiten Netzwerk-Roundtrips (oder sogar eines Festplatten-Cache-Roundtrips) keine gute Idee. Wenn Ihnen die Verwendung externer Importkarten am Herzen liegt, können Sie versuchen, diesen Round-Trip-Nachteil mit Technologien wie HTTP/2 Push oder gebündelten HTTP-Austauschen zu mildern.
Als weitere Folge davon, wie sich Importzuordnungen auf alle Importe auswirken, ist der Versuch, ein neues <script type="importmap">
hinzuzufügen, nachdem das Abrufen eines Moduldiagramms gestartet wurde, ein Fehler. Die Importzuordnung wird ignoriert und das <script>
-Element löst ein error
aus.
Derzeit ist nur ein <script type="importmap">
auf der Seite zulässig. Wir planen, dies in Zukunft zu erweitern, sobald wir die richtige Semantik für die Kombination mehrerer Importkarten herausgefunden haben. Siehe Diskussion in Nr. 14, Nr. 137 und Nr. 167.
Was machen wir als Arbeiter? Wahrscheinlich new Worker(someURL, { type: "module", importMap: ... })
? Oder sollten Sie es im Worker festlegen? Sollten engagierte Mitarbeiter standardmäßig oder immer die Karte ihres Controlling-Dokuments verwenden? Besprechen Sie in #2.
Die oben genannten Regeln bedeuten, dass Sie Importkarten dynamisch generieren können , sofern Sie dies vor der Durchführung von Importen tun. Zum Beispiel:
< script >
const im = document . createElement ( 'script' ) ;
im . type = 'importmap' ;
im . textContent = JSON . stringify ( {
imports : {
'my-library' : Math . random ( ) > 0.5 ? '/my-awesome-library.mjs' : '/my-rad-library.mjs'
}
} ) ;
document . currentScript . after ( im ) ;
</ script >
< script type =" module " >
import 'my-library' ; // will fetch the randomly-chosen URL
</ script >
Ein realistischeres Beispiel könnte diese Funktion nutzen, um die Importkarte basierend auf der Feature-Erkennung zusammenzustellen:
< script >
const importMap = {
imports : {
moment : '/moment.mjs' ,
lodash : someFeatureDetection ( ) ?
'/lodash.mjs' :
'/lodash-legacy-browsers.mjs'
}
} ;
const im = document . createElement ( 'script' ) ;
im . type = 'importmap' ;
im . textContent = JSON . stringify ( importMap ) ;
document . currentScript . after ( im ) ;
</ script >
< script type =" module " >
import _ from "lodash" ; // will fetch the right URL for this browser
</ script >
Beachten Sie, dass (wie bei anderen <script>
-Elementen) das Ändern des Inhalts eines <script type="importmap">
nachdem es bereits in das Dokument eingefügt wurde, nicht funktioniert. Aus diesem Grund haben wir das obige Beispiel geschrieben, indem wir den Inhalt der Importzuordnung zusammengestellt haben, bevor wir das <script type="importmap">
erstellt und eingefügt haben.
Importkarten sind eine Sache auf Anwendungsebene, ähnlich wie Servicemitarbeiter. (Formeller ausgedrückt wären sie pro Modulzuordnung und damit pro Bereich.) Sie sollen nicht zusammengestellt werden, sondern von einem Menschen oder einem Tool mit einer ganzheitlichen Sicht auf Ihre Webanwendung erstellt werden. Beispielsweise wäre es für eine Bibliothek nicht sinnvoll, eine Importkarte einzubinden; Bibliotheken können Module einfach nach Spezifizierern referenzieren und die Anwendung entscheiden lassen, welchen URLs diese Spezifizierer zugeordnet werden.
Dies ist neben der allgemeinen Einfachheit teilweise der Grund für die oben genannten Einschränkungen für <script type="importmap">
.
Da die Importzuordnung einer Anwendung den Auflösungsalgorithmus für jedes Modul in der Modulzuordnung ändert, hat es keinen Einfluss darauf, ob der Quelltext eines Moduls ursprünglich von einer Cross-Origin-URL stammt. Wenn Sie ein Modul von einem CDN laden, das bloße Importspezifizierer verwendet, müssen Sie im Voraus wissen, welche bloßen Importspezifizierer dieses Modul Ihrer App hinzufügt, und diese in die Importzuordnung Ihrer Anwendung aufnehmen. (Das heißt, Sie müssen alle transitiven Abhängigkeiten Ihrer Anwendung kennen.) Es ist wichtig, dass die Kontrolle darüber, welche URLs für jedes Paket verwendet werden, beim Anwendungsautor verbleibt, damit dieser die Versionierung und gemeinsame Nutzung von Modulen ganzheitlich verwalten kann.
Die meisten Browser verfügen über einen spekulativen HTML-Parser, der versucht, im HTML-Markup deklarierte Ressourcen zu erkennen, während der HTML-Parser darauf wartet, dass blockierende Skripte abgerufen und ausgeführt werden. Dies ist noch nicht spezifiziert, obwohl in whatwg/html#5959 laufende Bemühungen unternommen werden, dies zu tun. In diesem Abschnitt werden einige der möglichen Wechselwirkungen besprochen, die Sie beachten sollten.
Beachten Sie zunächst, dass dies unseres Wissens nach derzeit bei keinem Browser der Fall ist. Es wäre jedoch möglich, dass ein spekulativer Parser im folgenden Beispiel https://example.com/foo.mjs
abruft, während er auf das Blockierungsskript https://example.com/blocking-1.js
wartet. https://example.com/blocking-1.js
:
<!DOCTYPE html >
<!-- This file is https://example.com/ -->
< script src =" blocking-1.js " > </ script >
< script type =" module " >
import "./foo.mjs" ;
</ script >
In ähnlicher Weise könnte ein Browser im folgenden Beispiel spekulativ https://example.com/foo.mjs
und https://example.com/bar.mjs
abrufen, indem er die Importzuordnung als Teil des spekulativen Analyseprozesses analysiert:
<!DOCTYPE html >
<!-- This file is https://example.com/ -->
< script src =" blocking-2.js " > </ script >
< script type =" importmap " >
{
"imports" : {
"foo" : "./foo.mjs" ,
"https://other.example/bar.mjs" : "./bar.mjs"
}
}
</ script >
< script type =" module " >
import "foo" ;
import "https://other.example/bar.mjs" ;
</ script >
Eine hier zu beachtende Interaktion besteht darin, dass Browser, die Inline-JS-Module spekulativ analysieren, aber keine Importkarten unterstützen, für dieses Beispiel wahrscheinlich falsch spekulieren würden: Sie könnten spekulativ https://other.example/bar.mjs
anstelle von abrufen https://example.com/bar.mjs
dem es zugeordnet ist.
Generell können Importkarten-basierte Spekulationen denselben Fehlern unterliegen wie andere Spekulationen. Wenn zum Beispiel der Inhalt von blocking-1.js
wäre
const el = document . createElement ( "base" ) ;
el . href = "/subdirectory/" ;
document . currentScript . after ( el ) ;
dann wäre der spekulative Abruf von https://example.com/foo.mjs
im No-Import-Map-Beispiel verschwendet, da wir zum Zeitpunkt der eigentlichen Auswertung des Moduls den relativen Spezifizierer neu berechnen würden "./foo.mjs"
und stellen Sie fest, dass tatsächlich https://example.com/subdirectory/foo.mjs
angefordert wird.
Ähnliches gilt für den Import-Map-Fall, wenn der Inhalt von blocking-2.js
wäre
document . write ( `<script type="importmap">
{
"imports": {
"foo": "./other-foo.mjs",
"https://other.example/bar.mjs": "./other-bar.mjs"
}
}
</script>` ) ;
dann wären die spekulativen Abrufe von https://example.com/foo.mjs
und https://example.com/bar.mjs
verschwendet, da die neu geschriebene Importzuordnung anstelle der gesehenen wirksam wäre inline im HTML.
<base>
-Element Wenn das Element <base>
im Dokument vorhanden ist, werden alle URLs und URL-ähnlichen Spezifizierer in der Importzuordnung mithilfe des href
von <base>
in absolute URLs konvertiert.
< base href =" https://www.unpkg.com/vue/dist/ " >
< script type =" importmap " >
{
"imports" : {
"vue" : "./vue.runtime.esm.js" ,
}
}
</ script >
< script >
import ( "vue" ) ; // resolves to https://www.unpkg.com/vue/dist/vue.runtime.esm.js
</ script >
Wenn der Browser die „supports(type)“-Methode von HTMLScriptElement unterstützt, muss HTMLScriptElement.supports('importmap')
„true“ zurückgeben.
if ( HTMLScriptElement . supports && HTMLScriptElement . supports ( 'importmap' ) ) {
console . log ( 'Your browser supports import maps.' ) ;
}
Anders als in Node.js haben wir im Browser nicht den Luxus eines einigermaßen schnellen Dateisystems, das wir nach Modulen durchsuchen können. Daher können wir den Knotenmodul-Auflösungsalgorithmus nicht direkt implementieren. Es würde die Durchführung mehrerer Server-Roundtrips für jede import
erfordern, was Bandbreite und Zeit verschwendet, da wir weiterhin 404-Fehler erhalten. Wir müssen sicherstellen, dass jede import
nur eine HTTP-Anfrage verursacht; Dies erfordert ein gewisses Maß an Vorberechnung.
Einige haben vorgeschlagen, den Modulauflösungsalgorithmus des Browsers mithilfe eines JavaScript-Hooks anzupassen, um jeden Modulspezifizierer zu interpretieren.
Leider ist dies fatal für die Leistung; Das Springen in und aus JavaScript für jede Kante eines Moduldiagramms verlangsamt den Anwendungsstart drastisch. (Typische Webanwendungen verfügen über etwa Tausende von Modulen mit drei- bis viermal so vielen Importanweisungen.) Sie können sich verschiedene Abhilfemaßnahmen vorstellen, z. B. die Beschränkung der Aufrufe auf nur reine Importspezifizierer oder die Anforderung, dass der Hook Stapel von Spezifizierern und akzeptiert gibt Stapel von URLs zurück, aber am Ende geht nichts über die Vorberechnung.
Ein weiteres Problem dabei ist, dass man sich kaum einen nützlichen Zuordnungsalgorithmus vorstellen kann, den ein Webentwickler schreiben könnte, selbst wenn ihm dieser Haken gegeben wäre. Node.js verfügt zwar über eine, diese basiert jedoch auf dem wiederholten Crawlen des Dateisystems und der Überprüfung, ob Dateien vorhanden sind. Wie wir oben besprochen haben, ist das im Internet nicht machbar. Die einzige Situation, in der ein allgemeiner Algorithmus machbar wäre, ist, wenn (a) Sie nie eine Anpassung pro Untergraph benötigen, dh in Ihrer Anwendung nur eine Version jedes Moduls vorhanden ist; (b) Die Tools haben es geschafft, Ihre Module im Voraus auf eine einheitliche, vorhersehbare Weise anzuordnen, sodass der Algorithmus beispielsweise zu „return /js/${specifier}.js
“ wird. Aber wenn wir sowieso auf dieser Welt wären, wäre eine deklarative Lösung einfacher.
Eine heute verwendete Lösung (z. B. im unpkg-CDN über babel-plugin-unpkg) besteht darin, alle bloßen Importspezifizierer vorab mithilfe von Build-Tools in ihre entsprechenden absoluten URLs umzuschreiben. Dies könnte auch zum Zeitpunkt der Installation erfolgen, sodass bei der Installation eines Pakets mit npm der Inhalt des Pakets automatisch neu geschrieben wird, um absolute oder relative URLs anstelle bloßer Importspezifizierer zu verwenden.
Das Problem bei diesem Ansatz besteht darin, dass er mit dynamischem import()
nicht funktioniert, da es unmöglich ist, die an diese Funktion übergebenen Zeichenfolgen statisch zu analysieren. Sie könnten eine Korrektur einfügen, die beispielsweise jede Instanz von import(x)
in import(specifierToURL(x, import.meta.url))
ändert, wobei specifierToURL
eine weitere vom Build-Tool generierte Funktion ist. Letztendlich handelt es sich jedoch um eine ziemlich undichte Abstraktion, und die specifierToURL
-Funktion dupliziert ohnehin weitgehend die Arbeit dieses Vorschlags.
Auf den ersten Blick scheinen Servicemitarbeiter der richtige Ort für diese Art der Ressourcenübersetzung zu sein. Wir haben in der Vergangenheit darüber gesprochen, eine Möglichkeit zu finden, den Spezifizierer zusammen mit dem Fetch-Ereignis eines Servicemitarbeiters zu übergeben, damit dieser eine entsprechende Response
zurückgeben kann.
Allerdings sind bei der ersten Ladung keine Servicemitarbeiter verfügbar . Daher können sie nicht wirklich Teil der kritischen Infrastruktur sein, die zum Laden von Modulen verwendet wird. Sie können nur als progressive Erweiterung zusätzlich zu Abrufvorgängen verwendet werden, die ansonsten im Allgemeinen funktionieren.
Wenn Sie über einfache Anwendungen verfügen, für die keine bereichsbezogene Abhängigkeitsauflösung erforderlich ist, und über ein Paketinstallationstool verfügen, mit dem Pfade auf der Festplatte innerhalb des Pakets bequem neu geschrieben werden können (im Gegensatz zu aktuellen Versionen von npm), könnten Sie mit einer viel einfacheren Zuordnung auskommen. Wenn Ihr Installationstool beispielsweise eine flache Liste des Formulars erstellt hat
node_modules_flattened/
lodash/
index.js
core.js
fp.js
moment/
index.js
html-to-dom/
index.js
dann sind die einzigen Informationen, die Sie benötigen
/node_modules_flattened/
)index.js
)Sie könnten sich ein Konfigurationsformat für den Modulimport vorstellen, das nur diese Dinge oder sogar nur eine Teilmenge spezifiziert (wenn wir Annahmen für die anderen berücksichtigen würden).
Diese Idee funktioniert nicht für komplexere Anwendungen, die eine bereichsbezogene Auflösung benötigen. Daher glauben wir, dass der vollständige Importkartenvorschlag erforderlich ist. Aber es bleibt für einfache Anwendungen attraktiv, und wir fragen uns, ob es eine Möglichkeit gibt, den Vorschlag auch über einen einfachen Modus zu verfügen, der nicht die Auflistung aller Module erfordert, sondern sich stattdessen auf Konventionen und Tools verlässt, um sicherzustellen, dass nur minimale Zuordnungen erforderlich sind. Besprechen Sie es in Nr. 7.
Mittlerweile kommt es immer wieder vor, dass Benutzer Metadaten für jedes Modul bereitstellen möchten. zum Beispiel Integritätsmetadaten oder Abrufoptionen. Obwohl einige vorgeschlagen haben, dies mit einer Importanweisung zu tun, führt eine sorgfältige Prüfung der Optionen dazu, dass eine Out-of-Band-Manifestdatei bevorzugt wird.
Die Importzuordnung könnte diese Manifestdatei sein. Aus mehreren Gründen ist es jedoch möglicherweise nicht die beste Lösung:
Wie derzeit vorgesehen, hätten die meisten Module in einer Anwendung keine Einträge in der Importzuordnung. Der Hauptanwendungsfall sind Module, auf die Sie durch bloße Spezifizierer verweisen müssen, oder Module, bei denen Sie etwas Kniffliges wie Polyfilling oder Virtualisierung durchführen müssen. Wenn wir uns vorstellen würden, dass jedes Modul in der Karte enthalten ist, würden wir keine praktischen Funktionen wie Pakete über abschließende Schrägstriche einschließen.
Alle bisher vorgeschlagenen Metadaten sind auf jede Art von Ressource anwendbar, nicht nur auf JavaScript-Module. Eine Lösung sollte wahrscheinlich auf einer allgemeineren Ebene funktionieren.
Es ist normal, dass mehrere <script type="importmap">
s auf einer Seite erscheinen, so wie es auch bei mehreren <script>
s anderer Typen der Fall sein kann. Wir möchten dies in Zukunft ermöglichen.
Die größte Herausforderung besteht hier darin, zu entscheiden, wie sich die verschiedenen Importkarten zusammensetzen. Das heißt, wenn zwei Importzuordnungen vorhanden sind, die beide dieselbe URL neu zuordnen, oder zwei Bereichsdefinitionen, die denselben URL-Präfixbereich abdecken, welche Auswirkungen sollte dies auf die Seite haben? Der derzeit führende Kandidat ist die kaskadierende Auflösung, die Importzuordnungen von Importspezifizierer → URL-Zuordnungen in eine kaskadierende Reihe von Importspezifizierer → Importspezifiziererzuordnungen umwandelt und schließlich in einem „abrufbaren Importspezifizierer“ (im Wesentlichen einer URL) endet.
Weitere Diskussionen finden Sie in diesen offenen Fragen.
Einige Anwendungsfälle erfordern eine Möglichkeit, die Importzuordnung eines Bereichs aus einem Skript zu lesen oder zu bearbeiten, anstatt deklarative <script type="importmap">
Elemente einzufügen. Betrachten Sie es als ein „Importkartenobjektmodell“, ähnlich dem CSS-Objektmodell, das es einem ermöglicht, die normalerweise deklarativen CSS-Regeln der Seite zu manipulieren.
Die Herausforderungen bestehen hier darin, wie die deklarativen Importkarten mit etwaigen programmatischen Änderungen in Einklang gebracht werden können und wann im Lebenszyklus der Seite eine solche API funktionieren kann. Im Allgemeinen sind die einfacheren Designs weniger leistungsstark und erfüllen möglicherweise weniger Anwendungsfälle.
Weitere Diskussionen und Anwendungsfälle, bei denen eine programmatische API hilfreich sein könnte, finden Sie in diesen offenen Fragen.
import.meta.resolve()
Mit der vorgeschlagenen Funktion import.meta.resolve(specifier)
können Modulskripte jederzeit Importspezifizierer in URLs auflösen. Weitere Informationen finden Sie unter whatwg/html#5572. Dies hängt mit Importkarten zusammen, da Sie damit „paketbezogene“ Ressourcen auflösen können, z
const url = import . meta . resolve ( "somepackage/resource.json" ) ;
würde Ihnen den entsprechend zugeordneten Speicherort von resource.json
innerhalb des somepackage/
-Namespace geben, der von der Importzuordnung der Seite gesteuert wird.
Mehrere Mitglieder der Community haben an Polyfills und Werkzeugen im Zusammenhang mit Importkarten gearbeitet. Hier sind diejenigen, die wir kennen:
package.json
und node_modules/
.package.json
.<script type="systemjs-importmap">
.Senden Sie gerne eine Pull-Anfrage mit mehr! Außerdem können Sie #146 im Issue-Tracker für Diskussionen über diesen Bereich verwenden.
Dieses Dokument entstand aus einem eintägigen Sprint mit @domenic, @hiroshige-g, @justinfagnani, @MylesBorins und @nyaxt. Seitdem war @guybedford maßgeblich am Prototyping beteiligt und hat die Diskussion zu diesem Vorschlag vorangetrieben.
Vielen Dank auch an alle Mitwirkenden des Issue-Trackers für ihre Hilfe bei der Weiterentwicklung des Vorschlags!