Ou comment contrôler le comportement des importations JavaScript
<script>
<base>
import.meta.resolve()
Cette proposition permet de contrôler les URL récupérées par les instructions import
JavaScript et les expressions import()
. Cela permet aux "spécificateurs d'importation nus", tels que import moment from "moment"
, de fonctionner.
Le mécanisme pour ce faire consiste à utiliser une carte d'importation qui peut être utilisée pour contrôler la résolution des spécificateurs de module en général. À titre d'exemple d'introduction, considérons le code
import moment from "moment" ;
import { partition } from "lodash" ;
Aujourd'hui, cela ne change pas, car ces spécificateurs simples sont explicitement réservés. En fournissant au navigateur la carte d'importation suivante
< script type =" importmap " >
{
"imports" : {
"moment" : "/node_modules/moment/src/moment.js" ,
"lodash" : "/node_modules/lodash-es/lodash.js"
}
}
</ script >
ce qui précède agirait comme si vous aviez écrit
import moment from "/node_modules/moment/src/moment.js" ;
import { partition } from "/node_modules/lodash-es/lodash.js" ;
Pour en savoir plus sur la nouvelle valeur "importmap"
pour l'attribut type=""
de <script>
, consultez la section d'installation. Pour l'instant, nous allons nous concentrer sur la sémantique du mappage, en reportant la discussion sur l'installation.
Les développeurs Web ayant une expérience avec les systèmes de modules antérieurs à ES2015, tels que CommonJS (soit dans Node, soit fournis à l'aide de webpack/browserify pour le navigateur), sont habitués à pouvoir importer des modules en utilisant une syntaxe simple :
const $ = require ( "jquery" ) ;
const { pluck } = require ( "lodash" ) ;
Traduits dans le langage du système de modules intégré de JavaScript, ceux-ci seraient
import $ from "jquery" ;
import { pluck } from "lodash" ;
Dans de tels systèmes, ces simples spécificateurs d'importation de "jquery"
ou "lodash"
sont mappés sur des noms de fichiers ou des URL complets. Plus en détail, ces spécificateurs représentent des packages , généralement distribués sur npm ; en spécifiant uniquement le nom du package, ils demandent implicitement le module principal de ce package.
Le principal avantage de ce système est qu’il permet une coordination facile à travers l’écosystème. N'importe qui peut écrire un module et inclure une instruction d'importation en utilisant le nom bien connu d'un package, et laisser le runtime Node.js ou ses outils de construction se charger de le traduire en un fichier réel sur le disque (y compris la détermination des considérations de version).
Aujourd'hui, de nombreux développeurs Web utilisent même la syntaxe de module native de JavaScript, mais en la combinant avec de simples spécificateurs d'importation, rendant ainsi leur code incapable de s'exécuter sur le Web sans modification préalable par application. Nous aimerions résoudre ce problème et apporter ces avantages au Web.
Nous expliquons les fonctionnalités de la carte d'importation via une série d'exemples.
Comme mentionné dans l'introduction,
{
"imports" : {
"moment" : " /node_modules/moment/src/moment.js " ,
"lodash" : " /node_modules/lodash-es/lodash.js "
}
}
donne la prise en charge du spécificateur d'importation nu dans le code JavaScript :
import moment from "moment" ;
import ( "lodash" ) . then ( _ => ... ) ;
Notez que le côté droit du mappage (appelé « adresse ») doit commencer par /
, ../
ou ./
, ou être analysable comme une URL absolue, pour identifier une URL. Dans le cas d'adresses de type URL relative, elles sont résolues par rapport à l'URL de base de la carte d'importation, c'est-à-dire l'URL de base de la page pour les cartes d'importation en ligne et l'URL de la ressource de carte d'importation pour les cartes d'importation externes.
En particulier, les URL relatives « nues » comme node_modules/moment/src/moment.js
ne fonctionneront pas dans ces positions, pour l'instant. Ceci est effectué par défaut, car à l'avenir, nous souhaiterons peut-être autoriser plusieurs cartes d'importation, ce qui pourrait modifier la signification du côté droit d'une manière qui affecterait particulièrement ces cas simples.
Il est courant dans l'écosystème JavaScript qu'un package (au sens de npm) contienne plusieurs modules ou d'autres fichiers. Dans de tels cas, nous souhaitons mapper un préfixe dans l’espace du spécificateur de module sur un autre préfixe dans l’espace des URL récupérables.
Pour ce faire, les cartes d'importation donnent une signification particulière aux clés de spécificateur qui se terminent par une barre oblique finale. Ainsi, une carte comme
{
"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/ "
}
}
permettrait non seulement d'importer les modules principaux comme
import moment from "moment" ;
import _ from "lodash" ;
mais aussi des modules non principaux, par exemple
import localeData from "moment/locale/zh-cn.js" ;
import fp from "lodash/fp.js" ;
Il est également courant dans l'écosystème Node.js d'importer des fichiers sans inclure l'extension. Nous n'avons pas le luxe d'essayer plusieurs extensions de fichiers jusqu'à ce que nous trouvions une bonne correspondance. Cependant, nous pouvons émuler quelque chose de similaire en utilisant une carte d'importation. Par exemple,
{
"imports" : {
"lodash" : " /node_modules/lodash-es/lodash.js " ,
"lodash/" : " /node_modules/lodash-es/ " ,
"lodash/fp" : " /node_modules/lodash-es/fp.js " ,
}
}
permettrait non seulement import fp from "lodash/fp.js"
, mais permettrait également import fp from "lodash/fp"
.
Bien que cet exemple montre comment il est possible d'autoriser des importations sans extension avec des cartes d'importation, ce n'est pas nécessairement souhaitable . Cela gonfle la carte d'importation et rend l'interface du package moins simple, à la fois pour les humains et pour les outils.
Cette surcharge est particulièrement problématique si vous devez autoriser les importations sans extension au sein d'un package. Dans ce cas, vous aurez besoin d'une entrée de mappage d'importation pour chaque fichier du package, pas seulement pour les points d'entrée de niveau supérieur. Par exemple, pour autoriser import "./fp"
depuis le fichier /node_modules/lodash-es/lodash.js
, vous aurez besoin d'un mappage d'entrée d'importation /node_modules/lodash-es/fp
vers /node_modules/lodash-es/fp.js
. Imaginez maintenant répéter cela pour chaque fichier référencé sans extension.
En tant que tel, nous vous recommandons d'être prudent lorsque vous utilisez des modèles comme celui-ci dans vos cartes d'importation ou dans l'écriture de modules. Ce sera plus simple pour l'écosystème si nous ne nous appuyons pas sur des cartes d'importation pour corriger les incohérences liées aux extensions de fichiers.
Dans le cadre du remappage général des spécificateurs, les mappages d'importation autorisent spécifiquement le remappage des spécificateurs de type URL, tels que "https://example.com/foo.mjs"
ou "./bar.mjs"
. Une utilisation pratique de cela consiste à cartographier les hachages, mais nous en démontrons ici quelques-uns de base pour communiquer le concept :
{
"imports" : {
"https://www.unpkg.com/vue/dist/vue.runtime.esm.js" : " /node_modules/vue/dist/vue.runtime.esm.js "
}
}
Ce remappage garantit que toutes les importations de la version unpkg.com de Vue (au moins à cette URL) récupèrent plutôt celle du serveur local.
{
"imports" : {
"/app/helpers.mjs" : " /app/helpers/index.mjs "
}
}
Ce remappage garantit que toutes les importations de type URL qui se résolvent en /app/helpers.mjs
, y compris par exemple une import "./helpers.mjs"
à partir de fichiers à l'intérieur /app/
, ou une import "../helpers.mjs"
à partir de fichiers à l'intérieur /app/models
, sera plutôt résolu en /app/helpers/index.mjs
. Ce n'est probablement pas une bonne idée ; au lieu de créer une indirection qui obscurcit votre code, vous devez simplement mettre à jour vos fichiers sources pour importer les fichiers corrects. Mais c’est un exemple utile pour démontrer les capacités des cartes d’importation.
Un tel remappage peut également être effectué sur la base d'une correspondance de préfixe, en terminant la clé du spécificateur par une barre oblique finale :
{
"imports" : {
"https://www.unpkg.com/vue/" : " /node_modules/vue/ "
}
}
Cette version garantit que les instructions d'importation pour les spécificateurs commençant par la sous-chaîne "https://www.unpkg.com/vue/"
seront mappées à l'URL correspondante sous /node_modules/vue/
.
En général, le fait est que le remappage fonctionne de la même manière pour les importations de type URL que pour les importations simples. Nos exemples précédents ont modifié la résolution des spécificateurs comme "lodash"
, et ont ainsi modifié la signification de import "lodash"
. Ici, nous modifions la résolution des spécificateurs comme "/app/helpers.mjs"
, et changeons ainsi la signification de import "/app/helpers.mjs"
.
Notez que cette variante avec barre oblique finale du mappage de spécificateur de type URL ne fonctionne que si le spécificateur de type URL a un schéma spécial : par exemple, un mappage de "data:text/": "/foo"
n'aura pas d'impact sur la signification de import "data:text/javascript,console.log('test')"
, mais n'impactera que import "data:text/"
.
Les fichiers de script reçoivent souvent un hachage unique dans leur nom de fichier, pour améliorer la mise en cache. Voir cette discussion générale sur la technique, ou cette discussion plus axée sur JavaScript et Webpack.
Avec les graphes de modules, cette technique peut être problématique :
Considérons un simple graphique de module, avec app.mjs
dépendant de dep.mjs
qui dépend de sub-dep.mjs
. Normalement, si vous mettez à niveau ou modifiez sub-dep.mjs
, app.mjs
et dep.mjs
peuvent rester en cache, nécessitant uniquement le transfert du nouveau sub-dep.mjs
sur le réseau.
Considérons maintenant le même graphique de module, en utilisant des noms de fichiers hachés pour la production. Là, nous avons notre processus de construction générant app-8e0d62a03.mjs
, dep-16f9d819a.mjs
et sub-dep-7be2aa47f.mjs
à partir des trois fichiers d'origine.
Si nous mettons à niveau ou modifions sub-dep.mjs
, notre processus de construction générera à nouveau un nouveau nom de fichier pour la version de production, par exemple sub-dep-5f47101dc.mjs
. Mais cela signifie que nous devons modifier l'instruction import
dans la version de production de dep.mjs
. Cela modifie son contenu, ce qui signifie que la version de production de dep.mjs
elle-même a besoin d'un nouveau nom de fichier. Mais cela signifie que nous devons mettre à jour l'instruction import
dans la version de production de app.mjs
...
Autrement dit, avec les graphiques de module et les instructions import
contenant des fichiers de script de nom de fichier haché, les mises à jour de n'importe quelle partie du graphique deviennent virales pour toutes ses dépendances, perdant ainsi tous les avantages de la mise en cache.
Les cartes d'importation offrent un moyen de sortir de ce problème, en dissociant les spécificateurs de module qui apparaissent dans les instructions import
des URL sur le serveur. Par exemple, notre site pourrait commencer avec une carte d'importation comme
{
"imports" : {
"/js/app.mjs" : " /js/app-8e0d62a03.mjs " ,
"/js/dep.mjs" : " /js/dep-16f9d819a.mjs " ,
"/js/sub-dep.mjs" : " /js/sub-dep-7be2aa47f.mjs "
}
}
et avec des instructions d'importation de la forme import "./sub-dep.mjs"
au lieu de import "./sub-dep-7be2aa47f.mjs"
. Maintenant, si nous modifions sub-dep.mjs
, nous mettons simplement à jour notre map d'importation :
{
"imports" : {
"/js/app.mjs" : " /js/app-8e0d62a03.mjs " ,
"/js/dep.mjs" : " /js/dep-16f9d819a.mjs " ,
"/js/sub-dep.mjs" : " /js/sub-dep-5f47101dc.mjs "
}
}
et laissez l'instruction import "./sub-dep.mjs"
seule. Cela signifie que le contenu de dep.mjs
ne change pas et qu'il reste donc mis en cache ; la même chose pour app.mjs
.
<script>
Une remarque importante concernant l'utilisation de mappages d'importation pour modifier la signification des spécificateurs d'importation est que cela ne modifie pas la signification des URL brutes, telles que celles qui apparaissent dans <script src="">
ou <link rel="modulepreload">
. Autrement dit, compte tenu de l'exemple ci-dessus, alors que
import "./app.mjs" ;
serait correctement remappé vers sa version hachée dans les navigateurs prenant en charge l'importation de cartes,
< script type =" module " src =" ./app.mjs " > </ script >
ne fonctionnerait pas : dans toutes les classes de navigateurs, il tenterait de récupérer directement app.mjs
, ce qui entraînerait un 404. Ce qui fonctionnerait , dans les navigateurs prenant en charge l'importation de cartes, serait
< script type =" module " > import "./app.mjs" ; </ script >
Il arrive souvent que vous souhaitiez utiliser le même spécificateur d'importation pour faire référence à plusieurs versions d'une même bibliothèque, en fonction de la personne qui les importe. Cela encapsule les versions de chaque dépendance utilisée et évite l'enfer des dépendances (article de blog plus long).
Nous prenons en charge ce cas d'utilisation dans les cartes d'importation en vous permettant de modifier la signification d'un spécificateur dans une portée donnée :
{
"imports" : {
"querystringify" : " /node_modules/querystringify/index.js "
},
"scopes" : {
"/node_modules/socksjs-client/" : {
"querystringify" : " /node_modules/socksjs-client/querystringify/index.js "
}
}
}
(Cet exemple est l'un des nombreux exemples concrets de versions multiples par application fournis par @zkat. Merci, @zkat !)
Avec ce mappage, à l'intérieur de tous les modules dont les URL commencent par /node_modules/socksjs-client/
, le spécificateur "querystringify"
fera référence à /node_modules/socksjs-client/querystringify/index.js
. Alors que sinon, le mappage de niveau supérieur garantira que "querystringify"
fait référence à /node_modules/querystringify/index.js
.
Notez que le fait d'être dans une portée ne change pas la façon dont une adresse est résolue ; l'URL de base de la carte d'importation est toujours utilisée, au lieu par exemple du préfixe de l'URL de portée.
Les étendues « héritent » les unes des autres d’une manière intentionnellement simple, fusionnant mais supplantant au fur et à mesure. Par exemple, la carte d'importation suivante :
{
"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 "
}
}
}
donnerait les résolutions suivantes :
Spécificateur | Référent | URL résultante |
---|---|---|
un | /scope1/foo.mjs | /a-1.mjs |
b | /scope1/foo.mjs | /b-1.mjs |
c | /scope1/foo.mjs | /c-1.mjs |
un | /scope2/foo.mjs | /a-2.mjs |
b | /scope2/foo.mjs | /b-1.mjs |
c | /scope2/foo.mjs | /c-1.mjs |
un | /scope2/scope3/foo.mjs | /a-2.mjs |
b | /scope2/scope3/foo.mjs | /b-3.mjs |
c | /scope2/scope3/foo.mjs | /c-1.mjs |
Vous pouvez installer une carte d'importation pour votre application à l'aide d'un élément <script>
, soit en ligne, soit avec un attribut src=""
:
< script type =" importmap " >
{
"imports" : { ... } ,
"scopes" : { ... }
}
</ script >
< script type =" importmap " src =" import-map.importmap " > </ script >
Lorsque l'attribut src=""
est utilisé, la réponse HTTP résultante doit avoir le type MIME application/importmap+json
. (Pourquoi ne pas réutiliser application/json
? Cela pourrait permettre les contournements CSP.) Comme les scripts de module, la requête est effectuée avec CORS activé et la réponse est toujours interprétée comme UTF-8.
Parce qu'elles affectent toutes les importations, toutes les cartes d'importation doivent être présentes et récupérées avec succès avant toute résolution de module. Cela signifie que la récupération du graphique du module est bloquée lors de la récupération de la carte d'importation.
Cela signifie que la forme en ligne des cartes d'importation est fortement recommandée pour de meilleures performances. Ceci est similaire à la meilleure pratique consistant à intégrer du CSS critique ; les deux types de ressources empêchent votre application d'effectuer un travail important jusqu'à ce qu'elles soient traitées, donc introduire un deuxième aller-retour réseau (ou même aller-retour vers le cache disque) est une mauvaise idée. Si vous souhaitez utiliser des cartes d'importation externes, vous pouvez tenter d'atténuer cette pénalité aller-retour avec des technologies telles que HTTP/2 Push ou des échanges HTTP groupés.
Autre conséquence de la façon dont les cartes d'importation affectent toutes les importations, tenter d'ajouter un nouveau <script type="importmap">
après le début de la récupération d'un graphique de module est une erreur. La carte d'importation sera ignorée et l'élément <script>
déclenchera un événement error
.
Pour l'instant, un seul <script type="importmap">
est autorisé sur la page. Nous prévoyons d'étendre cela à l'avenir, une fois que nous aurons trouvé la sémantique correcte pour combiner plusieurs cartes d'importation. Voir la discussion aux numéros 14, 137 et 167.
Que faisons-nous chez les travailleurs? Probablement new Worker(someURL, { type: "module", importMap: ... })
? Ou devriez-vous le régler depuis l’intérieur du travailleur ? Les travailleurs dédiés doivent-ils utiliser la carte de leur document de contrôle, soit par défaut, soit toujours ? Discutez-en au n°2.
Les règles ci-dessus signifient que vous pouvez générer dynamiquement des cartes d'importation, à condition de le faire avant d'effectuer toute importation. Par exemple:
< 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 >
Un exemple plus réaliste pourrait utiliser cette fonctionnalité pour assembler la carte d'importation en fonction de la détection de fonctionnalités :
< 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 >
Notez que (comme les autres éléments <script>
) modifier le contenu d'un <script type="importmap">
une fois qu'il est déjà inséré dans le document ne fonctionnera pas. C'est pourquoi nous avons écrit l'exemple ci-dessus en assemblant le contenu de la carte d'importation avant de créer et d'insérer le <script type="importmap">
.
Les cartes d'importation sont une opération au niveau de l'application, un peu comme les service Workers. (Plus formellement, il s'agirait d'une carte par module, et donc par domaine.) Ils ne sont pas destinés à être composés, mais plutôt produits par un humain ou un outil avec une vue holistique de votre application Web. Par exemple, cela n'aurait aucun sens qu'une bibliothèque inclue une carte d'importation ; les bibliothèques peuvent simplement référencer les modules par spécificateur et laisser l'application décider à quelles URL ces spécificateurs correspondent.
Ceci, en plus de la simplicité générale, est en partie ce qui motive les restrictions ci-dessus sur <script type="importmap">
.
Étant donné que la carte d'importation d'une application modifie l'algorithme de résolution pour chaque module de la carte de module, ils ne sont pas affectés par le fait que le texte source d'un module provienne ou non d'une URL d'origine croisée. Si vous chargez un module à partir d'un CDN qui utilise des spécificateurs d'importation nus, vous devrez savoir à l'avance quels spécificateurs d'importation nus ce module ajoute à votre application et les inclure dans la carte d'importation de votre application. (En d'autres termes, vous devez connaître toutes les dépendances transitives de votre application.) Il est important que le contrôle des URL utilisées pour chaque package reste la responsabilité de l'auteur de l'application, afin qu'il puisse gérer de manière globale la gestion des versions et le partage des modules.
La plupart des navigateurs disposent d'un analyseur HTML spéculatif qui tente de découvrir les ressources déclarées dans le balisage HTML pendant que l'analyseur HTML attend que les scripts de blocage soient récupérés et exécutés. Ceci n'est pas encore spécifié, bien que des efforts soient en cours pour le faire dans whatwg/html#5959. Cette section aborde certaines des interactions potentielles dont il faut être conscient.
Tout d'abord, notez que même si, à notre connaissance, aucun navigateur ne le fait actuellement, il serait possible pour un analyseur spéculatif de récupérer https://example.com/foo.mjs
dans l'exemple suivant, en attendant le script de blocage 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 >
De même, un navigateur pourrait récupérer de manière spéculative https://example.com/foo.mjs
et https://example.com/bar.mjs
dans l'exemple suivant, en analysant la carte d'importation dans le cadre du processus d'analyse spéculative :
<!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 >
Une interaction à noter ici est que les navigateurs qui analysent de manière spéculative les modules JS en ligne, mais ne prennent pas en charge les cartes d'importation, spéculeraient probablement de manière incorrecte pour cet exemple : ils pourraient récupérer de manière spéculative https://other.example/bar.mjs
, au lieu du https://example.com/bar.mjs
auquel il est mappé.
Plus généralement, les spéculations basées sur des cartes d'importation peuvent être sujettes au même type d'erreurs que les autres spéculations. Par exemple, si le contenu de blocking-1.js
était
const el = document . createElement ( "base" ) ;
el . href = "/subdirectory/" ;
document . currentScript . after ( el ) ;
alors la récupération spéculative de https://example.com/foo.mjs
dans l'exemple de carte sans importation serait inutile, car au moment d'effectuer l'évaluation réelle du module, nous recalculerions le spécificateur relatif "./foo.mjs"
et réalisez que ce qui est réellement demandé est https://example.com/subdirectory/foo.mjs
.
De même pour le cas de la carte d'importation, si le contenu de blocking-2.js
était
document . write ( `<script type="importmap">
{
"imports": {
"foo": "./other-foo.mjs",
"https://other.example/bar.mjs": "./other-bar.mjs"
}
}
</script>` ) ;
alors les récupérations spéculatives de https://example.com/foo.mjs
et https://example.com/bar.mjs
seraient inutiles, car la carte d'importation nouvellement écrite serait en vigueur au lieu de celle qui a été vue en ligne dans le HTML.
<base>
Lorsque l'élément <base>
est présent dans le document, toutes les URL et spécificateurs de type URL dans la carte d'importation sont convertis en URL absolues à l'aide du href
de <base>
.
< 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 >
Si le navigateur prend en charge la méthode supports(type) de HTMLScriptElement, HTMLScriptElement.supports('importmap')
doit renvoyer true.
if ( HTMLScriptElement . supports && HTMLScriptElement . supports ( 'importmap' ) ) {
console . log ( 'Your browser supports import maps.' ) ;
}
Contrairement à Node.js, dans le navigateur, nous n'avons pas le luxe d'un système de fichiers raisonnablement rapide que nous pouvons explorer à la recherche de modules. Ainsi, nous ne pouvons pas implémenter directement l’algorithme de résolution du module Node ; cela nécessiterait d'effectuer plusieurs allers-retours sur le serveur pour chaque instruction import
, ce qui gaspillerait de la bande passante et du temps alors que nous continuons à obtenir des 404. Nous devons nous assurer que chaque instruction import
ne provoque qu'une seule requête HTTP ; cela nécessite une certaine mesure de précalcul.
Certains ont suggéré de personnaliser l'algorithme de résolution de module du navigateur à l'aide d'un hook JavaScript pour interpréter chaque spécificateur de module.
Malheureusement, cela est fatal aux performances ; entrer et sortir de JavaScript pour chaque bord d'un graphique de module ralentit considérablement le démarrage de l'application. (Les applications Web typiques ont de l'ordre de milliers de modules, avec 3 à 4 fois plus d'instructions d'importation.) Vous pouvez imaginer diverses mesures d'atténuation, telles que restreindre les appels aux seuls spécificateurs d'importation ou exiger que le hook prenne des lots de spécificateurs et renvoie des lots d'URL, mais au final, rien ne vaut le précalcul.
Un autre problème est qu'il est difficile d'imaginer un algorithme de mappage utile qu'un développeur Web pourrait écrire, même s'il disposait de ce crochet. Node.js en a un, mais il est basé sur une analyse répétée du système de fichiers et une vérification si les fichiers existent ; comme nous l'avons vu ci-dessus, c'est irréalisable sur le Web. La seule situation dans laquelle un algorithme général serait réalisable est si (a) vous n'avez jamais eu besoin de personnalisation par sous-graphe, c'est-à-dire qu'une seule version de chaque module existait dans votre application ; (b) les outils ont réussi à organiser vos modules à l'avance d'une manière uniforme et prévisible, de sorte que par exemple l'algorithme devienne "return /js/${specifier}.js
". Mais si nous sommes de toute façon dans ce monde, une solution déclarative serait plus simple.
Une solution utilisée aujourd'hui (par exemple dans le CDN unpkg via babel-plugin-unpkg) consiste à réécrire à l'avance tous les spécificateurs d'importation nus dans leurs URL absolues appropriées, à l'aide des outils de construction. Cela pourrait également être fait au moment de l'installation, de sorte que lorsque vous installez un package à l'aide de npm, il réécrit automatiquement le contenu du package pour utiliser des URL absolues ou relatives au lieu de simples spécificateurs d'importation.
Le problème avec cette approche est qu'elle ne fonctionne pas avec Dynamic import()
, car il est impossible d'analyser statiquement les chaînes transmises à cette fonction. Vous pouvez injecter un correctif qui, par exemple, modifie chaque instance de import(x)
en import(specifierToURL(x, import.meta.url))
, où specifierToURL
est une autre fonction générée par l'outil de construction. Mais en fin de compte, il s’agit d’une abstraction assez fuyante, et la fonction specifierToURL
duplique de toute façon largement le travail de cette proposition.
À première vue, les techniciens de service semblent être l’endroit idéal pour effectuer ce type de traduction de ressources. Nous avons parlé dans le passé de trouver un moyen de transmettre le spécificateur avec l'événement fetch d'un service worker, lui permettant ainsi de renvoyer une Response
appropriée.
Cependant, les techniciens de service ne sont pas disponibles lors du premier chargement . Ainsi, ils ne peuvent pas vraiment faire partie de l’infrastructure critique utilisée pour charger les modules. Ils ne peuvent être utilisés que comme une amélioration progressive en plus des récupérations qui, autrement, fonctionneraient généralement.
Si vous disposez d'applications simples sans besoin de résolution de dépendance étendue et que vous disposez d'un outil d'installation de package capable de réécrire facilement les chemins sur le disque à l'intérieur du package (contrairement aux versions actuelles de npm), vous pouvez vous en sortir avec un mappage beaucoup plus simple. Par exemple, si votre outil d'installation a créé une liste plate du formulaire
node_modules_flattened/
lodash/
index.js
core.js
fp.js
moment/
index.js
html-to-dom/
index.js
alors la seule information dont vous avez besoin est
/node_modules_flattened/
)index.js
)Vous pourriez imaginer un format de configuration d'importation de module qui spécifiait uniquement ces éléments, ou même seulement certains sous-ensembles (si nous prévoyions des hypothèses pour les autres).
Cette idée ne fonctionne pas pour les applications plus complexes qui nécessitent une résolution étendue, nous pensons donc que la proposition de carte d'importation complète est nécessaire. Mais cela reste attrayant pour les applications simples, et nous nous demandons s'il existe un moyen de faire en sorte que la proposition ait également un mode simple qui ne nécessite pas de lister tous les modules, mais s'appuie plutôt sur des conventions et des outils pour garantir qu'un mappage minimal est nécessaire. Discutez-en au n°7.
Plusieurs fois, il arrive que des gens souhaitent fournir des métadonnées pour chaque module ; par exemple, les métadonnées d'intégrité ou les options de récupération. Bien que certains aient proposé de le faire avec une instruction d'importation, un examen attentif des options conduit à préférer un fichier manifeste hors bande.
La carte d'importation pourrait être ce fichier manifeste. Cependant, ce n’est peut-être pas la meilleure solution, pour plusieurs raisons :
Tel qu'actuellement envisagé, la plupart des modules d'une application n'auraient pas d'entrées dans la carte d'importation. Le cas d'utilisation principal concerne les modules auxquels vous devez faire référence par de simples spécificateurs, ou les modules pour lesquels vous devez faire quelque chose de délicat comme le polyfilling ou la virtualisation. Si nous imaginions que chaque module soit sur la carte, nous n'inclurions pas de fonctionnalités pratiques telles que les packages via des barres obliques finales.
Toutes les métadonnées proposées jusqu'à présent sont applicables à tout type de ressource, pas seulement aux modules JavaScript. Une solution devrait probablement fonctionner à un niveau plus général.
Il est naturel que plusieurs <script type="importmap">
apparaissent sur une page, tout comme plusieurs <script>
d'autres types peuvent le faire. Nous aimerions permettre cela à l’avenir.
Le plus grand défi ici consiste à décider de la composition des multiples cartes d’importation. Autrement dit, étant donné deux mappages d'importation qui remappent tous deux la même URL, ou deux définitions de portée qui couvrent le même espace de préfixe d'URL, quel devrait être l'effet sur la page ? Le principal candidat actuel est la résolution en cascade, qui transforme les cartes d'importation en passant du statut de spécificateur d'importation → mappages d'URL à une série en cascade de mappages de spécificateur d'importation → de spécificateur d'importation, pour finalement aboutir à un « spécificateur d'importation récupérable » (essentiellement une URL).
Consultez ces problèmes ouverts pour plus de discussion.
Certains cas d'utilisation souhaitent un moyen de lire ou de manipuler la carte d'importation d'un domaine à partir d'un script, plutôt que via l'insertion d'éléments déclaratifs <script type="importmap">
. Considérez-le comme un "modèle d'objet de carte d'importation", similaire au modèle d'objet CSS qui permet de manipuler les règles CSS généralement déclaratives de la page.
Les défis ici concernent la façon de réconcilier les cartes d'importation déclaratives avec tout changement de programme, ainsi que le moment où une telle API peut fonctionner dans le cycle de vie de la page. En général, les conceptions les plus simples sont moins puissantes et peuvent répondre à moins de cas d’utilisation.
Consultez ces problèmes ouverts pour plus de discussions et de cas d’utilisation dans lesquels une API programmatique pourrait vous aider.
import.meta.resolve()
La fonction import.meta.resolve(specifier)
proposée permet aux scripts de module de résoudre les spécificateurs d'importation en URL à tout moment. Voir whatwg/html#5572 pour en savoir plus. Ceci est lié aux cartes d'importation car cela vous permet de résoudre des ressources "relatives au package", par exemple
const url = import . meta . resolve ( "somepackage/resource.json" ) ;
vous donnerait l'emplacement correctement mappé de resource.json
dans l'espace de noms somepackage/
contrôlé par la carte d'importation de la page.
Plusieurs membres de la communauté ont travaillé sur les polyfills et les outils liés à l'importation de cartes. Voici ceux que nous connaissons :
package.json
et node_modules/
.package.json
.<script type="systemjs-importmap">
.N'hésitez pas à envoyer une pull request avec plus ! Vous pouvez également utiliser le numéro 146 dans le suivi des problèmes pour discuter de cet espace.
Ce document est issu d'un sprint d'une journée impliquant @domenic, @hiroshige-g, @justinfagnani, @MylesBorins et @nyaxt. Depuis lors, @guybedford a joué un rôle déterminant dans le prototypage et dans l’avancement des discussions sur cette proposition.
Merci également à tous les contributeurs du issue tracker pour leur aide dans l'évolution de la proposition !