Saiba como importar e agrupar vários tipos de recursos do JavaScript.
Suponha que você esteja trabalhando em um app da Web. Nesse caso, é provável que você precise lidar não apenas com módulos JavaScript, mas também com todos os tipos de outros recursos, como workers da Web (que também são JavaScript, mas não fazem parte do gráfico de módulo regular), imagens, folhas de estilo, fontes, módulos WebAssembly e outros.
É possível incluir referências a alguns desses recursos diretamente no HTML, mas muitas vezes eles são acoplados logicamente a componentes reutilizáveis. Por exemplo, uma folha de estilo para um menu suspenso personalizado vinculado à parte JavaScript, imagens de ícones vinculadas a um componente da barra de ferramentas ou um módulo WebAssembly vinculado à cola JavaScript. Nesses casos, é mais conveniente fazer referência aos recursos diretamente dos módulos JavaScript e carregá-los dinamicamente quando (ou se) o componente correspondente for carregado.
No entanto, a maioria dos projetos grandes tem sistemas de build que executam otimizações e reorganizações adicionais de conteúdo, por exemplo, agrupamento e minificação. Eles não podem executar o código e prever o resultado da execução, nem percorrer todos os literais de string possíveis em JavaScript e fazer suposições sobre se é um URL de recurso ou não. Como fazer com que eles "vejam" esses recursos dinâmicos carregados por componentes JavaScript e os incluam no build?
Importações personalizadas em bundlers
Uma abordagem comum é reutilizar a sintaxe de importação estática. Em alguns bundlers, o formato pode ser detectado automaticamente pela extensão do arquivo, enquanto outros permitem que os plug-ins usem um esquema de URL personalizado, como no exemplo a seguir:
// 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 um plug-in de bundler encontra uma importação com uma extensão reconhecida ou um esquema personalizado explícito (asset-url:
e js-url:
no exemplo acima), ele adiciona o recurso referenciado ao gráfico de build, o copia para o destino final, realiza otimizações aplicáveis ao tipo do recurso e retorna o URL final a ser usado durante a execução.
Os benefícios dessa abordagem: a reutilização da sintaxe de importação do JavaScript garante que todos os URLs sejam estáticos e relativos ao arquivo atual, o que facilita a localização dessas dependências para o sistema de build.
No entanto, ele tem uma desvantagem significativa: esse código não pode funcionar diretamente no navegador, porque ele não sabe como lidar com esses esquemas ou extensões de importação personalizados. Isso pode ser aceitável se você controla todo o código e depende de um bundler para o desenvolvimento. No entanto, é cada vez mais comum usar módulos JavaScript diretamente no navegador, pelo menos durante o desenvolvimento, para reduzir a fricção. Alguém que esteja trabalhando em uma pequena demonstração pode nem precisar de um agrupador, mesmo na produção.
Padrão universal para navegadores e agrupadores
Se você estiver trabalhando em um componente reutilizável, ele precisa funcionar em qualquer ambiente, seja usado diretamente no navegador ou pré-criado como parte de um app maior. A maioria dos bundlers modernos permite isso aceitando o seguinte padrão em módulos JavaScript:
new URL('./relative-path', import.meta.url)
Esse padrão pode ser detectado de forma estática pelas ferramentas, quase como se fosse uma sintaxe especial, mas é uma expressão JavaScript válida que também funciona diretamente no navegador.
Ao usar esse padrão, o exemplo acima pode ser reescrito como:
// 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));
Como funciona? Vamos explicar melhor. O construtor new URL(...)
recebe um URL relativo como primeiro argumento e o resolve em relação a um URL absoluto fornecido como o segundo argumento. No nosso caso, o segundo argumento é import.meta.url
, que fornece o URL do módulo JavaScript atual. Portanto, o primeiro argumento pode ser qualquer caminho relativo a ele.
Ela tem compensações semelhantes à importação dinâmica. Embora seja possível usar import(...)
com expressões arbitrárias, como import(someUrl)
, os agrupadores dão um tratamento especial a um padrão com URL estático import('./some-static-url.js')
como uma forma de pré-processar uma dependência conhecida no momento da compilação, mas dividi-la em seu próprio bloco carregado dinamicamente.
Da mesma forma, é possível usar new URL(...)
com expressões arbitrárias, como new URL(relativeUrl, customAbsoluteBase)
. No entanto, o padrão new URL('...', import.meta.url)
é um sinal claro para que os bundlers processem e incluam uma dependência com o JavaScript principal.
URLs relativos ambíguos
Você pode estar se perguntando por que os agrupadores não podem detectar outros padrões comuns, por exemplo, fetch('./module.wasm')
sem os wrappers new URL
.
Isso acontece porque, ao contrário das instruções de importação, todas as solicitações dinâmicas são resolvidas em relação ao documento, e não ao arquivo JavaScript atual. Digamos que você tenha a seguinte estrutura:
index.html
:
html <script src="src/main.js" type="module"></script>
src/
main.js
module.wasm
Se você quiser carregar module.wasm
de main.js
, pode ser tentador usar um caminho relativo como fetch('./module.wasm')
.
No entanto, fetch
não conhece o URL do arquivo JavaScript em que é executado. Em vez disso, ele resolve URLs em relação ao documento. Como resultado, fetch('./module.wasm')
tentaria carregar http://example.com/module.wasm
em vez do http://example.com/src/module.wasm
pretendido e falharia (ou, pior, carregaria silenciosamente um recurso diferente do pretendido).
Ao agrupar o URL relativo em new URL('...', import.meta.url)
, você pode evitar esse problema e garantir que qualquer URL fornecido seja resolvido em relação ao URL do módulo JavaScript atual (import.meta.url
) antes de ser transmitido para qualquer carregador.
Substitua fetch('./module.wasm')
por fetch(new URL('./module.wasm', import.meta.url))
e ele vai carregar o módulo WebAssembly esperado, além de dar aos bundlers uma maneira de encontrar esses caminhos relativos durante o build.
Suporte a ferramentas
Empacotadores
Os seguintes pacotes já oferecem suporte ao esquema new URL
:
- Webpack v5
- Reunificação (conseguida com plug-ins: @web/rollup-plugin-import-meta-assets para recursos genéricos e @surma/rollup-plugin-off-main-thread para workers especificamente).
- Parcel v2 (Beta)
- Vite
WebAssembly
Ao trabalhar com o WebAssembly, geralmente você não carrega o módulo Wasm manualmente, mas importa a cola JavaScript emitida pelo conjunto de ferramentas. As toolchains a seguir podem emitir o padrão new URL(...)
descrito para você.
C/C++ usando o Emscripten
Ao usar o Emscripten, é possível solicitar que ele emita cola JavaScript como um módulo ES6 em vez de um script normal usando uma das seguintes opções:
$ 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
Ao usar essa opção, a saída vai usar o padrão new URL(..., import.meta.url)
, para que os bundlers possam encontrar o arquivo Wasm associado automaticamente.
Também é possível usar essa opção com linhas de execução do WebAssembly adicionando uma 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
Nesse caso, o Web Worker gerado será incluído da mesma maneira e também poderá ser descoberto por agrupadores e navegadores.
Rust usando wasm-pack / wasm-bindgen
O wasm-pack, o conjunto de ferramentas Rust principal para WebAssembly, também tem vários modos de saída.
Por padrão, ele vai emitir um módulo JavaScript que depende da proposta de integração do ESM do WebAssembly. No momento da escrita, essa proposta ainda é experimental, e a saída só vai funcionar quando agrupada com o Webpack.
Em vez disso, peça para o wasm-pack emitir um módulo ES6 compatível com o navegador usando --target web
:
$ wasm-pack build --target web
A saída vai usar o padrão new URL(..., import.meta.url)
descrito, e o arquivo Wasm também será descoberto automaticamente pelos bundlers.
Se você quiser usar linhas de execução do WebAssembly com Rust, a história é um pouco mais complicada. Confira a seção correspondente do guia para saber mais.
A versão curta é que não é possível usar APIs de linha de execução arbitrárias, mas, se você usar o Rayon, poderá combiná-lo com o adaptador wasm-bindgen-rayon para gerar workers na Web. A cola JavaScript usada pelo wasm-bindgen-rayon também inclui o padrão new URL(...)
, e os workers também podem ser descobertos e incluídos pelos bundlers.
Recursos futuros
import.meta.resolve
Uma chamada import.meta.resolve(...)
dedicada é uma possível melhoria futura. Isso permitiria resolver especificadores em relação ao módulo atual de maneira mais direta, sem parâmetros extras:
new URL('...', import.meta.url)
await import.meta.resolve('...')
Ele também se integraria melhor aos mapas de importação e aos solucionadores personalizados, já que passaria pelo mesmo sistema de resolução de módulos que import
. Ele também seria um indicador mais forte para os agrupadores, já que é uma sintaxe estática que não depende de APIs de execução, como URL
.
O import.meta.resolve
já foi implementado como um experimento no Node.js, mas ainda há algumas perguntas sem resposta sobre como ele funciona na Web.
Importar declarações
As importações de atribuição são um novo recurso que permite importar tipos diferentes dos módulos ECMAScript. Por enquanto, elas são limitadas a JSON:
foo.json:
{ "answer": 42 }
main.mjs:
import json from './foo.json' assert { type: 'json' };
console.log(json.answer); // 42
Elas também podem ser usadas por agrupadores e substituir os casos de uso atualmente cobertos pelo padrão new URL
, mas os tipos em importações são adicionados caso a caso. Por enquanto, eles abrangem apenas JSON, com módulos CSS em breve, mas outros tipos de recursos ainda vão precisar de uma solução mais genérica.
Confira a explicação do recurso v8.dev para saber mais.
Conclusão
Como você pode ver, há várias maneiras de incluir recursos que não são JavaScript na Web, mas eles têm várias desvantagens e não funcionam em várias cadeias de ferramentas. Futuras propostas podem permitir a importação desses recursos com sintaxe especializada, mas ainda não chegamos a esse ponto.
Até lá, o padrão new URL(..., import.meta.url)
é a solução mais promissora que já funciona em navegadores, vários bundlers e toolchains do WebAssembly.