Obtén información para importar y agrupar varios tipos de recursos desde JavaScript.
Supongamos que estás trabajando en una app web. En ese caso, es probable que debas lidiar no solo con módulos de JavaScript, sino también con todo tipo de otros recursos: Web Workers (que también son JavaScript, pero no forman parte del gráfico de módulos normal), imágenes, hojas de estilo, fuentes, módulos de WebAssembly y otros.
Es posible incluir referencias a algunos de esos recursos directamente en el código HTML, pero a menudo se vinculan de forma lógica con componentes reutilizables. Por ejemplo, un hoja de estilo para un menú desplegable personalizado vinculado a su parte de JavaScript, imágenes de íconos vinculadas a un componente de barra de herramientas o un módulo de WebAssembly vinculado a su elemento de unión de JavaScript. En esos casos, es más conveniente hacer referencia a los recursos directamente desde sus módulos de JavaScript y cargarlos de forma dinámica cuando (o si) se carga el componente correspondiente.
Sin embargo, la mayoría de los proyectos grandes tienen sistemas de compilación que realizan optimizaciones adicionales y reorganizan el contenido, por ejemplo, el empaquetado y la reducción. No pueden ejecutar el código ni predecir cuál será el resultado de la ejecución, ni pueden recorrer todas las cadenas literales posibles en JavaScript y adivinar si es una URL de recurso o no. Entonces, ¿cómo puedes hacer que “vean” esos recursos dinámicos cargados por los componentes de JavaScript y los incluyan en la compilación?
Importaciones personalizadas en empaquetadores
Un enfoque común es reutilizar la sintaxis de importación estática. En algunos empaquetadores, es posible que se detecte automáticamente el formato según la extensión del archivo, mientras que otros permiten que los complementos usen un esquema de URL personalizado, como en el siguiente ejemplo:
// 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);
Cuando un complemento del empaquetador encuentra una importación con una extensión que reconoce o un esquema personalizado explícito (asset-url:
y js-url:
en el ejemplo anterior), agrega el recurso al que se hace referencia al gráfico de compilación, lo copia en el destino final, realiza las optimizaciones aplicables para el tipo de recurso y muestra la URL final que se usará durante el tiempo de ejecución.
Los beneficios de este enfoque: La reutilización de la sintaxis de importación de JavaScript garantiza que todas las URLs sean estáticas y relativas al archivo actual, lo que facilita la ubicación de esas dependencias para el sistema de compilación.
Sin embargo, presenta una desventaja importante: este código no puede funcionar directamente en el navegador, ya que este no sabe cómo manejar esos esquemas o extensiones de importación personalizados. Esto puede estar bien si controlas todo el código y, de todos modos, dependes de un empaquetador para el desarrollo, pero cada vez es más común usar módulos de JavaScript directamente en el navegador, al menos durante el desarrollo, para reducir los inconvenientes. Es posible que alguien que trabaje en una demo pequeña ni siquiera necesite un empaquetador, incluso en producción.
Patrón universal para navegadores y empaquetadores
Si trabajas en un componente reutilizable, es recomendable que funcione en cualquiera de los entornos, ya sea que se use directamente en el navegador o se haya compilado previamente como parte de una app más grande. La mayoría de los agrupadores modernos permiten esto, ya que aceptan el siguiente patrón en los módulos de JavaScript:
new URL('./relative-path', import.meta.url)
Las herramientas pueden detectar este patrón de forma estática, casi como si fuera una sintaxis especial, aunque es una expresión de JavaScript válida que también funciona directamente en el navegador.
Cuando usas este patrón, el ejemplo anterior se puede reescribir de la siguiente manera:
// 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));
¿Cómo funciona? Vamos a dividirlo. El constructor new URL(...)
toma una URL relativa como primer argumento y la resuelve con una URL absoluta proporcionada como segundo argumento. En nuestro caso, el segundo argumento es import.meta.url
, que proporciona la URL del módulo de JavaScript actual, por lo que el primer argumento puede ser cualquier ruta relativa a él.
Tiene compensaciones similares a las de la importación dinámica. Si bien es posible usar import(...)
con expresiones arbitrarias como import(someUrl)
, los empaquetadores otorgan un tratamiento especial a un patrón con una URL estática import('./some-static-url.js')
como una forma de procesar previamente una dependencia conocida en el tiempo de compilación, pero la dividen en su propio fragmento que se carga de forma dinámica.
De manera similar, puedes usar new URL(...)
con expresiones arbitrarias como new URL(relativeUrl, customAbsoluteBase)
. Sin embargo, el patrón new URL('...', import.meta.url)
es una señal clara para que los empaquetadores procesen previamente y, además, incluyan una dependencia junto con el código JavaScript principal.
URLs relativas ambiguas
Quizás te preguntes por qué los agrupadores no pueden detectar otros patrones comunes, por ejemplo, fetch('./module.wasm')
sin los wrappers new URL
.
El motivo es que, a diferencia de las instrucciones de importación, cualquier solicitud dinámica se resuelve en relación con el documento en sí y no con el archivo JavaScript actual. Supongamos que tienes la siguiente estructura:
index.html
:
html <script src="src/main.js" type="module"></script>
src/
main.js
module.wasm
Si quieres cargar module.wasm
desde main.js
, podría ser tentador usar una ruta de acceso relativa como fetch('./module.wasm')
.
Sin embargo, fetch
no conoce la URL del archivo JavaScript en el que se ejecuta, sino que resuelve las URLs en relación con el documento. Como resultado, fetch('./module.wasm')
intentaría cargar http://example.com/module.wasm
en lugar de la http://example.com/src/module.wasm
prevista y fallaría (o, peor aún, cargaría de forma silenciosa un recurso diferente del que deseas).
Si unes la URL relativa en new URL('...', import.meta.url)
, puedes evitar este problema y garantizar que cualquier URL proporcionada se resuelva en relación con la URL del módulo de JavaScript actual (import.meta.url
) antes de que se pase a cualquier cargador.
Reemplaza fetch('./module.wasm')
por fetch(new URL('./module.wasm', import.meta.url))
para que cargue correctamente el módulo de WebAssembly esperado, y los agrupadores podrán encontrar esas rutas relativas también durante el tiempo de compilación.
Compatibilidad con herramientas
Empaquetadores
Los siguientes empaquetadores ya admiten el esquema new URL
:
- Webpack v5
- Rollup (se logra a través de complementos: @web/rollup-plugin-import-meta-assets para recursos genéricos y @surma/rollup-plugin-off-main-thread para trabajadores específicamente).
- Parcel v2 (beta)
- Vite
WebAssembly
Cuando trabajes con WebAssembly, por lo general, no cargarás el módulo Wasm de forma manual, sino que importarás el elemento de enlace de JavaScript que emite la cadena de herramientas. Las siguientes cadenas de herramientas pueden emitir el patrón new URL(...)
descrito de forma interna.
C/C++ a través de Emscripten
Cuando usas Emscripten, puedes pedirle que emita el código de enlace de JavaScript como un módulo ES6 en lugar de una secuencia de comandos normal a través de una de las siguientes opciones:
$ 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
Cuando uses esta opción, el resultado usará el patrón new URL(..., import.meta.url)
de forma interna, de modo que los empaquetadores puedan encontrar el archivo Wasm asociado automáticamente.
También puedes usar esta opción con subprocesos de WebAssembly si agregas una marca -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
En este caso, el Web Worker generado se incluirá de la misma manera y los compiladores y los navegadores también podrán encontrarlo.
Rust a través de wasm-pack o wasm-bindgen
wasm-pack, la cadena de herramientas principal de Rust para WebAssembly, también tiene varios modos de salida.
De forma predeterminada, emitirá un módulo de JavaScript que se basa en la propuesta de integración de ESM de WebAssembly. En el momento de escribir este artículo, esta propuesta aún es experimental, y el resultado solo funcionará cuando se combine con Webpack.
En su lugar, puedes pedirle a wasm-pack que emita un módulo ES6 compatible con el navegador a través de --target web
:
$ wasm-pack build --target web
El resultado usará el patrón new URL(..., import.meta.url)
descrito, y los empaquetadores también descubrirán automáticamente el archivo Wasm.
Si quieres usar subprocesos de WebAssembly con Rust, la historia es un poco más complicada. Consulta la sección correspondiente de la guía para obtener más información.
La versión corta es que no puedes usar APIs de subprocesos arbitrarias, pero si usas Rayon, puedes combinarlo con el adaptador wasm-bindgen-rayon para que pueda generar trabajadores en la Web. El elemento de unión de JavaScript que usa wasm-bindgen-rayon también incluye el patrón new URL(...)
en segundo plano, por lo que los agrupadores también podrán descubrir y, además, incluir los trabajadores.
Funciones futuras
import.meta.resolve
Una llamada import.meta.resolve(...)
dedicada es una posible mejora futura. Permitiría resolver los especificadores en relación con el módulo actual de una manera más directa, sin parámetros adicionales:
new URL('...', import.meta.url)
await import.meta.resolve('...')
También se integraría mejor con los mapas de importación y los solucionadores personalizados, ya que pasaría por el mismo sistema de resolución de módulos que import
. También sería un indicador más potente para los agrupadores, ya que es una sintaxis estática que no depende de APIs de entorno de ejecución como URL
.
import.meta.resolve
ya se implementó como un experimento en Node.js, pero todavía hay algunas preguntas sin resolver sobre cómo debería funcionar en la Web.
Importar aserciones
Las aserciones de importación son una función nueva que permite importar tipos distintos de módulos de ECMAScript. Por ahora, solo están limitados a JSON:
foo.json:
{ "answer": 42 }
main.mjs:
import json from './foo.json' assert { type: 'json' };
console.log(json.answer); // 42
Los agrupadores también pueden usarlos y reemplazar los casos de uso que actualmente cubre el patrón new URL
, pero los tipos en las aserciones de importación se agregan de forma individual. Por ahora, solo abarcan JSON, y los módulos CSS pronto estarán disponibles. Sin embargo, otros tipos de elementos requerirán una solución más genérica.
Consulta la explicación de la función de v8.dev para obtener más información.
Conclusión
Como puedes ver, existen varias formas de incluir recursos que no son de JavaScript en la Web, pero tienen varias desventajas y no funcionan en varias cadenas de herramientas. Es posible que las próximas propuestas nos permitan importar esos recursos con sintaxis especializada, pero aún no hemos llegado a ese punto.
Hasta entonces, el patrón new URL(..., import.meta.url)
es la solución más prometedora que ya funciona en navegadores, varios agrupadores y cadenas de herramientas de WebAssembly en la actualidad.