Scopri come importare e raggruppare vari tipi di asset da JavaScript.
Supponiamo che tu stia lavorando a un'app web. In questo caso, è probabile che tu debba gestire non solo i moduli JavaScript, ma anche tutti i tipi di altre risorse: web worker (che sono anche JavaScript, ma non fanno parte del grafico dei moduli normale), immagini, fogli di stile, caratteri, moduli WebAssembly e altri ancora.
È possibile includere riferimenti ad alcune di queste risorse direttamente nel codice HTML, ma spesso sono accoppiati logicamente a componenti riutilizzabili. Ad esempio, un foglio di stile per un menu a discesa personalizzato collegato alla relativa parte JavaScript, immagini di icone collegate a un componente della barra degli strumenti o un modulo WebAssembly collegato al relativo collegamento JavaScript. In questi casi, è più pratico fare riferimento alle risorse direttamente dai relativi moduli JavaScript e caricarle dinamicamente quando (o se) viene caricato il componente corrispondente.
Tuttavia, la maggior parte dei progetti di grandi dimensioni dispone di sistemi di compilazione che eseguono ulteriori ottimizzazioni e riorganizzazione dei contenuti, ad esempio il raggruppamento e la minimizzazione. Non possono eseguire il codice e prevedere quale sarà il risultato dell'esecuzione, né possono esaminare ogni possibile stringa letterale in JavaScript e fare supposizioni sul fatto che si tratti o meno di un URL di risorsa. Quindi, come puoi fare in modo che "vedano" gli asset dinamici caricati dai componenti JavaScript e li includano nella compilazione?
Importazioni personalizzate nei pacchetti
Un approccio comune è riutilizzare la sintassi di importazione statica. In alcuni bundler il formato potrebbe essere rilevato automaticamente in base all'estensione del file, mentre altri consentono ai plug-in di utilizzare uno schema URL personalizzato, come nell'esempio seguente:
// 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);
Quando un plug-in del bundler trova un'importazione con un'estensione riconosciuta o uno schema personalizzato esplicito (asset-url:
e js-url:
nell'esempio precedente), aggiunge l'asset a cui si fa riferimento al grafo di compilazione, lo copia nella destinazione finale, esegue le ottimizzazioni applicabili al tipo di asset e restituisce l'URL finale da utilizzare in fase di esecuzione.
I vantaggi di questo approccio: il riutilizzo della sintassi di importazione di JavaScript garantisce che tutti gli URL siano statici e relativi al file corrente, il che semplifica la ricerca di queste dipendenze per il sistema di compilazione.
Tuttavia, presenta uno svantaggio significativo: questo codice non può funzionare direttamente nel browser, in quanto il browser non sa come gestire questi schemi o estensioni di importazione personalizzati. Questo potrebbe andare bene se controlli tutto il codice e ti affidi comunque a un bundler per lo sviluppo, ma è sempre più comune utilizzare i moduli JavaScript direttamente nel browser, almeno durante lo sviluppo, per ridurre le difficoltà. Chi lavora a una piccola demo potrebbe non aver bisogno di un bundler, nemmeno in produzione.
Pattern universale per browser e bundler
Se stai lavorando a un componente riutilizzabile, vorrai che funzioni in entrambi gli ambienti, che venga utilizzato direttamente nel browser o precompilato all'interno di un'app più grande. La maggior parte dei bundler moderni lo consente accettando il seguente pattern nei moduli JavaScript:
new URL('./relative-path', import.meta.url)
Questo pattern può essere rilevato in modo statico dagli strumenti, quasi come se fosse una sintassi speciale, ma è un'espressione JavaScript valida che funziona anche direttamente nel browser.
Quando utilizzi questo pattern, l'esempio riportato sopra può essere riscritto come:
// 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));
Come funziona? Analizziamo la questione. Il costruttore new URL(...)
prende un URL relativo come primo argomento e lo risolve in base a un URL assoluto fornito come secondo argomento. Nel nostro caso, il secondo argomento è import.meta.url
che fornisce l'URL del modulo JavaScript corrente, pertanto il primo argomento può essere qualsiasi percorso relativo.
Presenta compromessi simili all'importazione dinamica. Sebbene sia possibile utilizzare import(...)
con espressioni arbitrarie come import(someUrl)
, i bundler applicano un trattamento speciale a un pattern con URL statico import('./some-static-url.js')
per pre-elaborare una dipendenza nota in fase di compilazione, ma suddividerla in un proprio chunk che viene caricato in modo dinamico.
Analogamente, puoi utilizzare new URL(...)
con espressioni arbitrarie come new URL(relativeUrl, customAbsoluteBase)
, ma il pattern new URL('...', import.meta.url)
è un segnale chiaro per i bundler di eseguire la preelaborazione e includere una dipendenza insieme al codice JavaScript principale.
URL relativi ambigui
Potresti chiederti perché i pacchetti non riescono a rilevare altri pattern comuni, ad esempio fetch('./module.wasm')
senza i wrapper new URL
.
Il motivo è che, a differenza delle istruzioni di importazione, le eventuali richieste dinamiche vengono risolte in base al documento stesso e non al file JavaScript corrente. Supponiamo che tu abbia la seguente struttura:
index.html
:
html <script src="src/main.js" type="module"></script>
src/
main.js
module.wasm
Se vuoi caricare module.wasm
da main.js
, potrebbe essere allettante utilizzare un percorso relativo come fetch('./module.wasm')
.
Tuttavia, fetch
non conosce l'URL del file JavaScript in cui viene eseguito, ma risolve gli URL in base al documento. Di conseguenza, fetch('./module.wasm')
finirebbe per provare a caricare http://example.com/module.wasm
anziché http://example.com/src/module.wasm
e non andrebbe a buon fine (o, peggio, caricherebbe silenziosamente una risorsa diversa da quella prevista).
Inserendo l'URL relativo in new URL('...', import.meta.url)
puoi evitare questo problema e garantire che qualsiasi URL fornito venga risolto in base all'URL del modulo JavaScript corrente (import.meta.url
) prima di essere trasmesso a eventuali caricatori.
Sostituisci fetch('./module.wasm')
con fetch(new URL('./module.wasm', import.meta.url))
e il modulo WebAssembly previsto verrà caricato correttamente. Inoltre, i bundler avranno un modo per trovare questi percorsi relativi anche durante il tempo di compilazione.
Supporto degli strumenti
Bundler
I seguenti pacchetti già supportano lo schema new URL
:
- Webpack v5
- Aggregazione (ottenuta tramite plug-in: @web/rollup-plugin-import-meta-assets per gli asset generici e @surma/rollup-plugin-off-main-thread per i worker in modo specifico).
- Parcel v2 (beta)
- Vite
WebAssembly
Quando lavori con WebAssembly, in genere non carichi il modulo Wasm manualmente, ma importi il collegamento JavaScript emesso dalla toolchain. Le seguenti toolchain possono emettere il pattern new URL(...)
descritto sotto il cofano.
C/C++ tramite Emscripten
Quando utilizzi Emscripten, puoi chiedergli di emettere il codice JavaScript come modulo ES6 anziché come script normale tramite una delle seguenti opzioni:
$ 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
Quando utilizzi questa opzione, l'output utilizzerà il pattern new URL(..., import.meta.url)
sotto il cofano, in modo che i bundler possano trovare automaticamente il file Wasm associato.
Puoi utilizzare questa opzione anche con i thread WebAssembly aggiungendo un flag -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
In questo caso, il web worker generato verrà incluso nello stesso modo e sarà rilevabile anche da bundler e browser.
Rust tramite wasm-pack / wasm-bindgen
Anche wasm-pack, la toolchain Rust principale per WebAssembly, ha diverse modalità di output.
Per impostazione predefinita, emette un modulo JavaScript che si basa sulla proposta di integrazione di ESM WebAssembly. Al momento in cui scriviamo, questa proposta è ancora sperimentale e l'output funzionerà solo se aggregato con Webpack.
In alternativa, puoi chiedere a wasm-pack di emettere un modulo ES6 compatibile con i browser tramite --target web
:
$ wasm-pack build --target web
L'output utilizzerà il pattern new URL(..., import.meta.url)
descritto e il file Wasm verrà rilevato automaticamente anche dai bundler.
Se vuoi utilizzare i thread WebAssembly con Rust, la situazione è un po' più complicata. Per saperne di più, consulta la sezione corrispondente della guida.
In breve, non puoi utilizzare API thread arbitrarie, ma se utilizzi Rayon, puoi combinarlo con l'adattatore wasm-bindgen-rayon in modo che possa generare worker sul web. Il codice JavaScript utilizzato da wasm-bindgen-rayon include anche il pattern new URL(...)
sotto il cofano, pertanto i worker saranno rilevabili e inclusi anche dai bundler.
Funzionalità future
import.meta.resolve
Una chiamata import.meta.resolve(...)
dedicata è un potenziale miglioramento futuro. Consentirebbe di risolvere gli specificatori rispetto al modulo corrente in modo più semplice, senza parametri aggiuntivi:
new URL('...', import.meta.url)
await import.meta.resolve('...')
Inoltre, si integrerebbe meglio con le mappe di importazione e i risolutori personalizzati, in quanto utilizzerebbe lo stesso sistema di risoluzione dei moduli di import
. Sarebbe un segnale più forte anche per i bundler, in quanto si tratta di una sintassi statica che non dipende da API di runtime come URL
.
import.meta.resolve
è già implementato come esperimento in Node.js, ma ci sono ancora alcune domande irrisolte su come dovrebbe funzionare sul web.
Importa asserzioni
Le asserzioni di importazione sono una nuova funzionalità che consente di importare tipi diversi dai moduli ECMAScript. Per il momento sono limitati a JSON:
foo.json:
{ "answer": 42 }
main.mjs:
import json from './foo.json' assert { type: 'json' };
console.log(json.answer); // 42
Potrebbero essere utilizzati anche dai bundler e sostituire i casi d'uso attualmente coperti dal pattern new URL
, ma i tipi nelle asserzioni di importazione vengono aggiunti caso per caso. Per il momento coprono solo JSON, ma a breve saranno disponibili i moduli CSS. Tuttavia, altri tipi di asset richiederanno ancora una soluzione più generica.
Consulta la spiegazione della funzionalità v8.dev per scoprire di più su questa funzionalità.
Conclusione
Come puoi vedere, esistono vari modi per includere risorse non JavaScript sul web, ma presentano vari svantaggi e non funzionano su varie toolchain. Le proposte future potrebbero consentirci di importare asset di questo tipo con una sintassi specializzata, ma non siamo ancora arrivati a questo punto.
Fino ad allora, il pattern new URL(..., import.meta.url)
è la soluzione più promettente che funziona già in browser, vari bundler e toolchain WebAssembly.