Découvrez comment importer et regrouper différents types de composants à partir de JavaScript.
Supposons que vous travailliez sur une application Web. Dans ce cas, vous devrez probablement gérer non seulement des modules JavaScript, mais aussi toutes sortes d'autres ressources : nœuds de calcul Web (qui sont également JavaScript, mais qui ne font pas partie du graphique de module standard), images, feuilles de style, polices, modules WebAssembly et autres.
Il est possible d'inclure des références à certaines de ces ressources directement dans le code HTML, mais elles sont souvent associées de manière logique à des composants réutilisables. Par exemple, une feuille de style pour une liste déroulante personnalisée associée à sa partie JavaScript, des images d'icônes associées à un composant de barre d'outils ou un module WebAssembly associé à sa colle JavaScript. Dans ce cas, il est plus pratique de référencer les ressources directement à partir de leurs modules JavaScript et de les charger de manière dynamique lorsque (ou si) le composant correspondant est chargé.
Toutefois, la plupart des grands projets disposent de systèmes de compilation qui effectuent des optimisations et une réorganisation supplémentaires du contenu (par exemple, le regroupement et la minification). Ils ne peuvent pas exécuter le code et prédire le résultat de l'exécution, ni parcourir toutes les chaînes littérales possibles en JavaScript et deviner s'il s'agit d'une URL de ressource ou non. Comment pouvez-vous les faire "voir" ces éléments dynamiques chargés par les composants JavaScript et les inclure dans le build ?
Importations personnalisées dans les bundlers
Une approche courante consiste à réutiliser la syntaxe d'importation statique. Dans certains outils de regroupement, le format peut être détecté automatiquement à l'aide de l'extension de fichier, tandis que d'autres permettent aux plug-ins d'utiliser un schéma d'URL personnalisé, comme dans l'exemple suivant:
// regular JavaScript import
import { loadImg } from './utils.js';
// special "URL imports" for assets
import imageUrl from 'asset-url:./image.png';
import wasmUrl from 'asset-url:./module.wasm';
import workerUrl from 'js-url:./worker.js';
loadImg(imageUrl);
WebAssembly.instantiateStreaming(fetch(wasmUrl));
new Worker(workerUrl);
Lorsqu'un plug-in de bundler détecte une importation avec une extension qu'il reconnaît ou un schéma personnalisé explicite (asset-url:
et js-url:
dans l'exemple ci-dessus), il ajoute l'asset référencé au graphique de compilation, le copie à la destination finale, effectue les optimisations applicables au type de l'asset et renvoie l'URL finale à utiliser au moment de l'exécution.
Avantages de cette approche: la réutilisation de la syntaxe d'importation JavaScript garantit que toutes les URL sont statiques et relatives au fichier actuel, ce qui facilite la localisation de ces dépendances pour le système de compilation.
Cependant, ce code présente un inconvénient majeur: il ne peut pas fonctionner directement dans le navigateur, car celui-ci ne sait pas gérer ces schémas ou extensions d'importation personnalisés. Cela peut être acceptable si vous contrôlez tout le code et que vous vous appuyez de toute façon sur un bundler pour le développement, mais il est de plus en plus courant d'utiliser des modules JavaScript directement dans le navigateur, au moins pendant le développement, pour réduire les frictions. Un développeur qui travaille sur une petite démonstration n'a peut-être même pas besoin d'un bundler, même en production.
Modèle universel pour les navigateurs et les outils de compilation
Si vous travaillez sur un composant réutilisable, vous souhaitez qu'il fonctionne dans les deux environnements, qu'il soit utilisé directement dans le navigateur ou précompilé dans le cadre d'une application plus grande. La plupart des outils de compilation modernes le permettent en acceptant le modèle suivant dans les modules JavaScript:
new URL('./relative-path', import.meta.url)
Ce modèle peut être détecté de manière statique par des outils, presque comme s'il s'agissait d'une syntaxe spéciale. Il s'agit pourtant d'une expression JavaScript valide qui fonctionne également directement dans le navigateur.
Lorsque vous utilisez ce modèle, l'exemple ci-dessus peut être réécrit comme suit:
// regular JavaScript import
import { loadImg } from './utils.js';
loadImg(new URL('./image.png', import.meta.url));
WebAssembly.instantiateStreaming(
fetch(new URL('./module.wasm', import.meta.url)),
{ /* … */ }
);
new Worker(new URL('./worker.js', import.meta.url));
Fonctionnement Décomposons cela. Le constructeur new URL(...)
utilise une URL relative comme premier argument et la résout par rapport à une URL absolue fournie comme deuxième argument. Dans notre cas, le deuxième argument est import.meta.url
, qui fournit l'URL du module JavaScript actuel. Le premier argument peut donc être n'importe quel chemin relatif à celui-ci.
Il présente des compromis similaires à ceux de l'importation dynamique. Bien qu'il soit possible d'utiliser import(...)
avec des expressions arbitraires telles que import(someUrl)
, les outils de compilation de bundles traitent de manière spéciale un format avec une URL statique import('./some-static-url.js')
afin de prétraiter une dépendance connue au moment de la compilation, tout en la séparant en son propre bloc chargé dynamiquement.
De même, vous pouvez utiliser new URL(...)
avec des expressions arbitraires telles que new URL(relativeUrl, customAbsoluteBase)
. Cependant, le format new URL('...', import.meta.url)
est un signal clair pour les outils de compilation de prétraiter et d'inclure une dépendance avec le code JavaScript principal.
URL relatives ambiguës
Vous vous demandez peut-être pourquoi les outils de regroupement ne peuvent-ils pas détecter d'autres modèles courants, par exemple fetch('./module.wasm')
sans les wrappers new URL
?
En effet, contrairement aux instructions d'importation, toutes les requêtes dynamiques sont résolues par rapport au document lui-même, et non au fichier JavaScript actuel. Supposons que vous disposiez de la structure suivante:
index.html
:
html <script src="src/main.js" type="module"></script>
src/
main.js
module.wasm
Si vous souhaitez charger module.wasm
à partir de main.js
, vous pourriez être tenté d'utiliser un chemin d'accès relatif tel que fetch('./module.wasm')
.
Toutefois, fetch
ne connaît pas l'URL du fichier JavaScript dans lequel il est exécuté. À la place, il résout les URL par rapport au document. Par conséquent, fetch('./module.wasm')
tenterait de charger http://example.com/module.wasm
au lieu de http://example.com/src/module.wasm
et échouerait (ou, pire, chargerait silencieusement une autre ressource que celle prévue).
En encapsulant l'URL relative dans new URL('...', import.meta.url)
, vous pouvez éviter ce problème et vous assurer que toute URL fournie est résolue par rapport à l'URL du module JavaScript actuel (import.meta.url
) avant d'être transmise à des chargeurs.
Remplacez fetch('./module.wasm')
par fetch(new URL('./module.wasm', import.meta.url))
. Le module WebAssembly attendu sera alors chargé, et les outils de compilation pourront également trouver ces chemins relatifs au moment de la compilation.
Outils compatibles
Bundlers
Les bundlers suivants sont déjà compatibles avec le schéma new URL
:
- Webpack v5
- Réunion (réalisée via des plug-ins : @web/rollup-plugin-import-meta-assets pour les composants génériques et @surma/rollup-plugin-off-main-thread pour les workers en particulier.)
- Parcel v2 (bêta)
- Vite
WebAssembly
Lorsque vous travaillez avec WebAssembly, vous ne chargez généralement pas le module Wasm manuellement, mais importez plutôt la colle JavaScript émise par la chaîne d'outils. Les chaînes d'outils suivantes peuvent émettre le modèle new URL(...)
décrit en arrière-plan.
C/C++ via Emscripten
Lorsque vous utilisez Emscripten, vous pouvez lui demander d'émettre une colle JavaScript en tant que module ES6 au lieu d'un script standard via l'une des options suivantes:
$ emcc input.cpp -o output.mjs
## or, if you don't want to use .mjs extension
$ emcc input.cpp -o output.js -s EXPORT_ES6
Lorsque vous utilisez cette option, la sortie utilise le modèle new URL(..., import.meta.url)
en arrière-plan, afin que les bundlers puissent trouver automatiquement le fichier Wasm associé.
Vous pouvez également utiliser cette option avec les threads WebAssembly en ajoutant un indicateur -pthread
:
$ emcc input.cpp -o output.mjs -pthread
## or, if you don't want to use .mjs extension
$ emcc input.cpp -o output.js -s EXPORT_ES6 -pthread
Dans ce cas, le Web Worker généré sera inclus de la même manière et sera également visible par les bundlers et les navigateurs.
Rust via wasm-pack / wasm-bindgen
wasm-pack, la chaîne d'outils Rust principale pour WebAssembly, propose également plusieurs modes de sortie.
Par défaut, il émet un module JavaScript qui s'appuie sur la proposition d'intégration de l'ESM WebAssembly. Au moment de la rédaction de cet article, cette proposition est encore expérimentale, et la sortie ne fonctionnera que lorsqu'elle sera groupée avec Webpack.
Vous pouvez plutôt demander à wasm-pack d'émettre un module ES6 compatible avec les navigateurs via --target web
:
$ wasm-pack build --target web
La sortie utilisera le modèle new URL(..., import.meta.url)
décrit, et le fichier Wasm sera également automatiquement détecté par les bundlers.
Si vous souhaitez utiliser des threads WebAssembly avec Rust, la situation est un peu plus complexe. Pour en savoir plus, consultez la section correspondante du guide.
En résumé, vous ne pouvez pas utiliser d'API de thread arbitraires. Toutefois, si vous utilisez Rayon, vous pouvez le combiner à l'adaptateur wasm-bindgen-rayon afin qu'il puisse générer des workers sur le Web. La colle JavaScript utilisée par wasm-bindgen-rayon inclut également le modèle new URL(...)
sous le capot. Les workers seront donc détectables et inclus par les bundlers.
Fonctionnalités futures
import.meta.resolve
Un appel import.meta.resolve(...)
dédié est une amélioration potentielle à venir. Cela permettrait de résoudre les spécificateurs par rapport au module actuel de manière plus simple, sans paramètres supplémentaires:
new URL('...', import.meta.url)
await import.meta.resolve('...')
Il s'intégrerait également mieux aux cartes d'importation et aux résolveurs personnalisés, car il passerait par le même système de résolution de module que import
. Il s'agirait également d'un signal plus fort pour les bundlers, car il s'agit d'une syntaxe statique qui ne dépend pas des API d'exécution telles que URL
.
import.meta.resolve
est déjà implémenté à titre expérimental dans Node.js, mais certaines questions restent sans réponse sur son fonctionnement sur le Web.
Assertions d'importation
Les assertions d'importation sont une nouvelle fonctionnalité qui permet d'importer des types autres que des modules ECMAScript. Pour le moment, elles sont limitées au format JSON:
foo.json:
{ "answer": 42 }
main.mjs:
import json from './foo.json' assert { type: 'json' };
console.log(json.answer); // 42
Ils peuvent également être utilisés par les bundlers et remplacer les cas d'utilisation actuellement couverts par le modèle new URL
. Toutefois, les types dans les assertions d'importation sont ajoutés au cas par cas. Pour le moment, ils ne couvrent que le format JSON. Des modules CSS seront bientôt disponibles, mais d'autres types d'assets nécessiteront toujours une solution plus générique.
Pour en savoir plus sur cette fonctionnalité, consultez la présentation de la fonctionnalité v8.dev.
Conclusion
Comme vous pouvez le constater, il existe différentes manières d'inclure des ressources autres que JavaScript sur le Web, mais elles présentent divers inconvénients et ne fonctionnent pas avec différentes chaînes d'outils. De futures propositions pourraient nous permettre d'importer de tels éléments avec une syntaxe spécialisée, mais nous n'en sommes pas encore là.
En attendant, le modèle new URL(..., import.meta.url)
est la solution la plus prometteuse, qui fonctionne déjà dans les navigateurs, les différents bundlers et les chaînes d'outils WebAssembly.