O cómo controlar el comportamiento de las importaciones de JavaScript.
<script>
<base>
import.meta.resolve()
Esta propuesta permite controlar qué URL se obtienen mediante declaraciones import
de JavaScript y expresiones import()
. Esto permite que funcionen los "especificadores de importación simples", como import moment from "moment"
.
El mecanismo para hacer esto es a través de un mapa de importación que se puede utilizar para controlar la resolución de los especificadores de módulo en general. Como ejemplo introductorio, considere el código
import moment from "moment" ;
import { partition } from "lodash" ;
Hoy en día, esto arroja, ya que dichos especificadores básicos están explícitamente reservados. Proporcionando al navegador el siguiente mapa de importación
< script type =" importmap " >
{
"imports" : {
"moment" : "/node_modules/moment/src/moment.js" ,
"lodash" : "/node_modules/lodash-es/lodash.js"
}
}
</ script >
lo anterior actuaría como si hubieras escrito
import moment from "/node_modules/moment/src/moment.js" ;
import { partition } from "/node_modules/lodash-es/lodash.js" ;
Para obtener más información sobre el nuevo valor "importmap"
para el atributo type=""
de <script>
, consulte la sección de instalación. Por ahora, nos concentraremos en la semántica del mapeo, posponiendo la discusión sobre la instalación.
Los desarrolladores web con experiencia con sistemas de módulos anteriores a ES2015, como CommonJS (ya sea en Node o empaquetados usando webpack/browserify para el navegador), están acostumbrados a poder importar módulos usando una sintaxis simple:
const $ = require ( "jquery" ) ;
const { pluck } = require ( "lodash" ) ;
Traducidos al lenguaje del sistema de módulos integrado de JavaScript, estos serían
import $ from "jquery" ;
import { pluck } from "lodash" ;
En dichos sistemas, estos especificadores de importación simples de "jquery"
o "lodash"
se asignan a nombres de archivos completos o URL. Más detalladamente, estos especificadores representan paquetes , generalmente distribuidos en npm; al especificar solo el nombre del paquete, implícitamente están solicitando el módulo principal de ese paquete.
El principal beneficio de este sistema es que permite una fácil coordinación en todo el ecosistema. Cualquiera puede escribir un módulo e incluir una declaración de importación usando el nombre conocido de un paquete, y dejar que el tiempo de ejecución de Node.js o sus herramientas de tiempo de compilación se encarguen de traducirlo a un archivo real en el disco (lo que incluye determinar las consideraciones de versiones).
Hoy en día, muchos desarrolladores web incluso utilizan la sintaxis del módulo nativo de JavaScript, pero la combinan con especificadores de importación simples, lo que hace que su código no pueda ejecutarse en la web sin modificaciones previas por aplicación. Nos gustaría resolver eso y llevar estos beneficios a la web.
Explicamos las características del mapa de importación a través de una serie de ejemplos.
Como se mencionó en la introducción,
{
"imports" : {
"moment" : " /node_modules/moment/src/moment.js " ,
"lodash" : " /node_modules/lodash-es/lodash.js "
}
}
proporciona soporte básico para especificadores de importación en código JavaScript:
import moment from "moment" ;
import ( "lodash" ) . then ( _ => ... ) ;
Tenga en cuenta que el lado derecho de la asignación (conocido como "dirección") debe comenzar con /
, ../
o ./
, o ser analizable como una URL absoluta, para identificar una URL. En el caso de direcciones similares a URL relativas, se resuelven en relación con la URL base del mapa de importación, es decir, la URL base de la página para mapas de importación en línea y la URL del recurso de mapa de importación para mapas de importación externos.
En particular, las URL relativas "desnudas" como node_modules/moment/src/moment.js
no funcionarán en estas posiciones, por ahora. Esto se hace como un valor predeterminado conservador, ya que en el futuro es posible que deseemos permitir múltiples mapas de importación, lo que podría cambiar el significado del lado derecho de manera que afecte especialmente a estos casos básicos.
Es común en el ecosistema JavaScript tener un paquete (en el sentido de npm) que contenga múltiples módulos u otros archivos. Para tales casos, queremos asignar un prefijo en el espacio del especificador de módulo a otro prefijo en el espacio de URL recuperable.
Los mapas de importación hacen esto dando un significado especial a las claves especificadoras que terminan con una barra diagonal. Así, un mapa como
{
"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/ "
}
}
permitiría no sólo importar los módulos principales como
import moment from "moment" ;
import _ from "lodash" ;
pero también módulos no principales, por ejemplo
import localeData from "moment/locale/zh-cn.js" ;
import fp from "lodash/fp.js" ;
También es común en el ecosistema Node.js importar archivos sin incluir la extensión. No podemos darnos el lujo de probar varias extensiones de archivo hasta que encontremos una buena coincidencia. Sin embargo, podemos emular algo similar usando un mapa de importación. Por ejemplo,
{
"imports" : {
"lodash" : " /node_modules/lodash-es/lodash.js " ,
"lodash/" : " /node_modules/lodash-es/ " ,
"lodash/fp" : " /node_modules/lodash-es/fp.js " ,
}
}
permitiría no solo import fp from "lodash/fp.js"
, sino que también permitiría import fp from "lodash/fp"
.
Aunque este ejemplo muestra cómo es posible permitir importaciones sin extensiones con mapas de importación, no es necesariamente deseable . Hacerlo infla el mapa de importación y hace que la interfaz del paquete sea menos simple, tanto para los humanos como para las herramientas.
Esta hinchazón es especialmente problemática si necesita permitir importaciones sin extensiones dentro de un paquete. En ese caso, necesitará una entrada de mapa de importación para cada archivo del paquete, no sólo los puntos de entrada de nivel superior. Por ejemplo, para permitir import "./fp"
desde el archivo /node_modules/lodash-es/lodash.js
, necesitaría una entrada de importación que asigne /node_modules/lodash-es/fp
a /node_modules/lodash-es/fp.js
. Ahora imagina repetir esto para cada archivo al que se hace referencia sin extensión.
Como tal, recomendamos precaución al emplear patrones como este en sus mapas de importación o módulos de escritura. Será más sencillo para el ecosistema si no dependemos de la importación de mapas para corregir las discrepancias relacionadas con las extensiones de archivos.
Como parte de permitir la reasignación general de especificadores, los mapas de importación permiten específicamente la reasignación de especificadores similares a URL, como "https://example.com/foo.mjs"
o "./bar.mjs"
. Un uso práctico para esto es mapear hashes, pero aquí demostramos algunos básicos para comunicar el concepto:
{
"imports" : {
"https://www.unpkg.com/vue/dist/vue.runtime.esm.js" : " /node_modules/vue/dist/vue.runtime.esm.js "
}
}
Esta reasignación garantiza que cualquier importación de la versión unpkg.com de Vue (al menos en esa URL) tome la del servidor local.
{
"imports" : {
"/app/helpers.mjs" : " /app/helpers/index.mjs "
}
}
Esta reasignación garantiza que cualquier importación similar a una URL que se resuelva en /app/helpers.mjs
, incluida, por ejemplo, una import "./helpers.mjs"
desde archivos dentro de /app/
, o una import "../helpers.mjs"
desde archivos dentro de /app/models
, en su lugar se resolverá en /app/helpers/index.mjs
. Probablemente esta no sea una buena idea; en lugar de crear una dirección indirecta que ofusque su código, debería simplemente actualizar sus archivos fuente para importar los archivos correctos. Pero es un ejemplo útil para demostrar las capacidades de importar mapas.
Esta reasignación también se puede realizar basándose en la coincidencia de prefijos, terminando la clave del especificador con una barra diagonal:
{
"imports" : {
"https://www.unpkg.com/vue/" : " /node_modules/vue/ "
}
}
Esta versión garantiza que las declaraciones de importación para los especificadores que comienzan con la subcadena "https://www.unpkg.com/vue/"
se asignarán a la URL correspondiente debajo de /node_modules/vue/
.
En general, el punto es que la reasignación funciona igual para importaciones tipo URL que para importaciones simples. Nuestros ejemplos anteriores cambiaron la resolución de especificadores como "lodash"
y, por lo tanto, cambiaron el significado de import "lodash"
. Aquí estamos cambiando la resolución de especificadores como "/app/helpers.mjs"
y, por lo tanto, cambiamos el significado de import "/app/helpers.mjs"
.
Tenga en cuenta que esta variante de barra diagonal final del mapeo de especificador tipo URL solo funciona si el especificador tipo URL tiene un esquema especial: por ejemplo, un mapeo de "data:text/": "/foo"
no afectará el significado de import "data:text/javascript,console.log('test')"
, pero en su lugar solo afectará import "data:text/"
.
Los archivos de script a menudo reciben un hash único en su nombre para mejorar la capacidad de almacenamiento en caché. Vea esta discusión general sobre la técnica, o esta discusión más centrada en JavaScript y paquetes web.
Con los gráficos de módulos, esta técnica puede resultar problemática:
Considere un gráfico de módulo simple, con app.mjs
dependiendo de dep.mjs
, que depende de sub-dep.mjs
. Normalmente, si actualiza o cambia sub-dep.mjs
, app.mjs
y dep.mjs
pueden permanecer en caché, lo que requiere solo transferir el nuevo sub-dep.mjs
a través de la red.
Ahora considere el mismo gráfico de módulo, utilizando nombres de archivos con hash para producción. Allí tenemos nuestro proceso de compilación generando app-8e0d62a03.mjs
, dep-16f9d819a.mjs
y sub-dep-7be2aa47f.mjs
a partir de los tres archivos originales.
Si actualizamos o cambiamos sub-dep.mjs
, nuestro proceso de compilación volverá a generar un nuevo nombre de archivo para la versión de producción, digamos sub-dep-5f47101dc.mjs
. Pero esto significa que debemos cambiar la declaración import
en la versión de producción de dep.mjs
. Eso cambia su contenido, lo que significa que la versión de producción de dep.mjs
necesita un nuevo nombre de archivo. Pero entonces esto significa que necesitamos actualizar la declaración import
en la versión de producción de app.mjs
...
Es decir, con gráficos de módulos y declaraciones import
que contienen archivos de script con nombres de archivos hash, las actualizaciones de cualquier parte del gráfico se vuelven virales para todas sus dependencias, perdiendo todos los beneficios de la capacidad de almacenamiento en caché.
Los mapas de importación proporcionan una salida a este dilema al desacoplar los especificadores de módulo que aparecen en las declaraciones import
de las URL del servidor. Por ejemplo, nuestro sitio podría comenzar con un mapa de importación como
{
"imports" : {
"/js/app.mjs" : " /js/app-8e0d62a03.mjs " ,
"/js/dep.mjs" : " /js/dep-16f9d819a.mjs " ,
"/js/sub-dep.mjs" : " /js/sub-dep-7be2aa47f.mjs "
}
}
y con declaraciones de importación que tienen el formato import "./sub-dep.mjs"
en lugar de import "./sub-dep-7be2aa47f.mjs"
. Ahora, si cambiamos sub-dep.mjs
, simplemente actualizamos nuestro mapa de importación:
{
"imports" : {
"/js/app.mjs" : " /js/app-8e0d62a03.mjs " ,
"/js/dep.mjs" : " /js/dep-16f9d819a.mjs " ,
"/js/sub-dep.mjs" : " /js/sub-dep-5f47101dc.mjs "
}
}
y deje la instrucción import "./sub-dep.mjs"
sola. Esto significa que el contenido de dep.mjs
no cambia y, por lo tanto, permanece en caché; lo mismo para app.mjs
.
<script>
Una nota importante sobre el uso de mapas de importación para cambiar el significado de los especificadores de importación es que no cambia el significado de las URL sin formato, como las que aparecen en <script src="">
o <link rel="modulepreload">
. Es decir, dado el ejemplo anterior, mientras
import "./app.mjs" ;
se reasignaría correctamente a su versión hash en navegadores que admitan mapas de importación,
< script type =" module " src =" ./app.mjs " > </ script >
no lo haría: en todas las clases de navegadores, intentaría recuperar app.mjs
directamente, lo que daría como resultado un 404. Lo que funcionaría , en navegadores que admitan importación de mapas, sería
< script type =" module " > import "./app.mjs" ; </ script >
A menudo se da el caso de que desee utilizar el mismo especificador de importación para hacer referencia a varias versiones de una única biblioteca, dependiendo de quién las importe. Esto encapsula las versiones de cada dependencia en uso y evita el infierno de dependencias (publicación de blog más larga).
Admitimos este caso de uso en la importación de mapas al permitirle cambiar el significado de un especificador dentro de un alcance determinado:
{
"imports" : {
"querystringify" : " /node_modules/querystringify/index.js "
},
"scopes" : {
"/node_modules/socksjs-client/" : {
"querystringify" : " /node_modules/socksjs-client/querystringify/index.js "
}
}
}
(Este ejemplo es uno de varios ejemplos reales de múltiples versiones por aplicación proporcionados por @zkat. ¡Gracias, @zkat!)
Con esta asignación, dentro de cualquier módulo cuyas URL comiencen con /node_modules/socksjs-client/
, el especificador "querystringify"
se referirá a /node_modules/socksjs-client/querystringify/index.js
. Mientras que, de lo contrario, la asignación de nivel superior garantizará que "querystringify"
haga referencia a /node_modules/querystringify/index.js
.
Tenga en cuenta que estar en un ámbito no cambia la forma en que se resuelve una dirección; Se sigue utilizando la URL base del mapa de importación, en lugar de, por ejemplo, el prefijo de la URL de alcance.
Los ámbitos se "heredan" entre sí de una manera intencionalmente simple, fusionándose pero anulándose a medida que avanzan. Por ejemplo, el siguiente mapa de importación:
{
"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 "
}
}
}
daría las siguientes resoluciones:
Especificador | referente | URL resultante |
---|---|---|
a | /scope1/foo.mjs | /a-1.mjs |
b | /scope1/foo.mjs | /b-1.mjs |
do | /scope1/foo.mjs | /c-1.mjs |
a | /scope2/foo.mjs | /a-2.mjs |
b | /scope2/foo.mjs | /b-1.mjs |
do | /scope2/foo.mjs | /c-1.mjs |
a | /scope2/scope3/foo.mjs | /a-2.mjs |
b | /scope2/scope3/foo.mjs | /b-3.mjs |
do | /scope2/scope3/foo.mjs | /c-1.mjs |
Puedes instalar un mapa de importación para tu aplicación usando un elemento <script>
, ya sea en línea o con un atributo src=""
:
< script type =" importmap " >
{
"imports" : { ... } ,
"scopes" : { ... }
}
</ script >
< script type =" importmap " src =" import-map.importmap " > </ script >
Cuando se utiliza el atributo src=""
, la respuesta HTTP resultante debe tener el tipo MIME application/importmap+json
. (¿Por qué no reutilizar application/json
? Hacerlo podría habilitar la omisión de CSP). Al igual que los scripts del módulo, la solicitud se realiza con CORS habilitado y la respuesta siempre se interpreta como UTF-8.
Debido a que afectan a todas las importaciones, todos los mapas de importación deben estar presentes y recuperarse correctamente antes de realizar cualquier resolución del módulo. Esto significa que la recuperación del gráfico del módulo está bloqueada al recuperar el mapa de importación.
Esto significa que se recomienda encarecidamente la forma en línea de importación de mapas para obtener el mejor rendimiento. Esto es similar a la mejor práctica de incorporar CSS crítico; Ambos tipos de recursos impiden que su aplicación realice un trabajo importante hasta que se procesen, por lo que introducir un segundo viaje de ida y vuelta de red (o incluso un viaje de ida y vuelta de caché de disco) es una mala idea. Si está decidido a utilizar mapas de importación externos, puede intentar mitigar esta penalización de ida y vuelta con tecnologías como HTTP/2 Push o intercambios HTTP incluidos.
Como otra consecuencia de cómo los mapas de importación afectan todas las importaciones, intentar agregar un nuevo <script type="importmap">
después de que se haya iniciado la obtención de cualquier gráfico de módulo es un error. El mapa de importación se ignorará y el elemento <script>
activará un evento error
.
Por ahora, sólo se permite un <script type="importmap">
en la página. Planeamos ampliar esto en el futuro, una vez que descubramos la semántica correcta para combinar múltiples mapas de importación. Consulte la discusión en los puntos 14, 137 y 167.
¿Qué hacemos en los trabajadores? ¿Probablemente new Worker(someURL, { type: "module", importMap: ... })
? ¿O debería configurarlo desde el interior del trabajador? ¿Deberían los trabajadores dedicados utilizar el mapa de su documento de control, ya sea de forma predeterminada o siempre? Discuta en el n.° 2.
Las reglas anteriores significan que puede generar mapas de importación dinámicamente, siempre que lo haga antes de realizar cualquier importación. Por ejemplo:
< 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 ejemplo más realista podría utilizar esta capacidad para ensamblar el mapa de importación en función de la detección de características:
< 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 >
Tenga en cuenta que (al igual que otros elementos <script>
) modificar el contenido de un <script type="importmap">
después de que ya esté insertado en el documento no funcionará. Es por eso que escribimos el ejemplo anterior ensamblando el contenido del mapa de importación antes de crear e insertar el <script type="importmap">
.
Los mapas de importación son una cuestión a nivel de aplicación, algo así como los trabajadores de servicios. (Más formalmente, serían mapas por módulo y, por lo tanto, por reino). No están destinados a ser compuestos, sino producidos por un ser humano o una herramienta con una visión holística de su aplicación web. Por ejemplo, no tendría sentido que una biblioteca incluyera un mapa de importación; las bibliotecas pueden simplemente hacer referencia a módulos por especificador y dejar que la aplicación decida a qué URL se asignan esos especificadores.
Esto, además de la simplicidad general, es en parte lo que motiva las restricciones anteriores en <script type="importmap">
.
Dado que el mapa de importación de una aplicación cambia el algoritmo de resolución para cada módulo en el mapa de módulos, no se ven afectados por si el texto fuente de un módulo proviene originalmente de una URL de origen cruzado. Si carga un módulo desde una CDN que utiliza especificadores de importación simples, necesitará saber de antemano qué especificadores de importación simples agrega ese módulo a su aplicación e incluirlos en el mapa de importación de su aplicación. (Es decir, necesita saber cuáles son todas las dependencias transitivas de su aplicación). Es importante que el control de qué URL se utilizan para cada paquete permanezca en el autor de la aplicación, para que pueda administrar de manera integral el control de versiones y el uso compartido de módulos.
La mayoría de los navegadores tienen un analizador HTML especulativo que intenta descubrir recursos declarados en el marcado HTML mientras el analizador HTML espera que se obtengan y ejecuten los scripts de bloqueo. Esto aún no está especificado, aunque hay esfuerzos en curso para hacerlo en whatwg/html#5959. Esta sección analiza algunas de las posibles interacciones que se deben tener en cuenta.
Primero, tenga en cuenta que, aunque hasta donde sabemos ningún navegador lo hace actualmente, sería posible que un analizador especulativo obtenga https://example.com/foo.mjs
en el siguiente ejemplo, mientras espera el script de bloqueo 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 manera similar, un navegador podría recuperar de forma especulativa https://example.com/foo.mjs
y https://example.com/bar.mjs
en el siguiente ejemplo, analizando el mapa de importación como parte del proceso de análisis especulativo:
<!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 >
Una interacción a tener en cuenta aquí es que los navegadores que analizan especulativamente módulos JS en línea, pero no admiten mapas de importación, probablemente especularían incorrectamente para este ejemplo: podrían buscar especulativamente https://other.example/bar.mjs
, en lugar del https://example.com/bar.mjs
al que está asignado.
En términos más generales, las especulaciones sobre importaciones basadas en mapas pueden estar sujetas al mismo tipo de errores que otras especulaciones. Por ejemplo, si el contenido de blocking-1.js
fuera
const el = document . createElement ( "base" ) ;
el . href = "/subdirectory/" ;
document . currentScript . after ( el ) ;
entonces la recuperación especulativa de https://example.com/foo.mjs
en el ejemplo del mapa sin importación se desperdiciaría, ya que cuando llegara el momento de realizar la evaluación real del módulo, volveríamos a calcular el especificador relativo. "./foo.mjs"
y darse cuenta de que lo que realmente se solicita es https://example.com/subdirectory/foo.mjs
.
De manera similar, para el caso del mapa de importación, si el contenido de blocking-2.js
fuera
document . write ( `<script type="importmap">
{
"imports": {
"foo": "./other-foo.mjs",
"https://other.example/bar.mjs": "./other-bar.mjs"
}
}
</script>` ) ;
entonces las recuperaciones especulativas de https://example.com/foo.mjs
y https://example.com/bar.mjs
se desperdiciarían, ya que el mapa de importación recién escrito estaría vigente en lugar del que se vio. en línea en el HTML.
<base>
Cuando el elemento <base>
está presente en el documento, todas las URL y especificadores similares a URL en el mapa de importación se convierten en URL absolutas utilizando el 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 el navegador admite el método de soporte (tipo) de HTMLScriptElement, HTMLScriptElement.supports('importmap')
debe devolver verdadero.
if ( HTMLScriptElement . supports && HTMLScriptElement . supports ( 'importmap' ) ) {
console . log ( 'Your browser supports import maps.' ) ;
}
A diferencia de Node.js, en el navegador no tenemos el lujo de contar con un sistema de archivos razonablemente rápido que podamos rastrear en busca de módulos. Por lo tanto, no podemos implementar directamente el algoritmo de resolución del módulo Node; requeriría realizar múltiples viajes de ida y vuelta al servidor para cada declaración import
, desperdiciando ancho de banda y tiempo a medida que continuamos obteniendo 404. Necesitamos asegurarnos de que cada declaración import
genere solo una solicitud HTTP; esto requiere cierta medida de cálculo previo.
Algunos han sugerido personalizar el algoritmo de resolución del módulo del navegador utilizando un enlace de JavaScript para interpretar cada especificador de módulo.
Desafortunadamente, esto es fatal para el rendimiento; entrar y salir de JavaScript para cada borde del gráfico de un módulo ralentiza drásticamente el inicio de la aplicación. (Las aplicaciones web típicas tienen del orden de miles de módulos, con entre 3 y 4 veces esa cantidad de declaraciones de importación). Puede imaginar varias mitigaciones, como restringir las llamadas a solo especificadores de importación básicos o exigir que el gancho acepte lotes de especificadores y devuelve lotes de URL, pero al final nada supera al cálculo previo.
Otro problema con esto es que es difícil imaginar un algoritmo de mapeo útil que un desarrollador web pueda escribir, incluso si tuviera este gancho. Node.js tiene uno, pero se basa en rastrear repetidamente el sistema de archivos y verificar si existen archivos; Nosotros, como se mencionó anteriormente, eso no es factible en la web. La única situación en la que un algoritmo general sería factible es si (a) nunca necesitó personalización por subgrafo, es decir, sólo existía una versión de cada módulo en su aplicación; (b) las herramientas lograron organizar sus módulos con anticipación de alguna manera uniforme y predecible, de modo que, por ejemplo, el algoritmo se convierta en "return /js/${specifier}.js
". Pero si de todos modos estamos en este mundo, una solución declarativa sería más sencilla.
Una solución que se utiliza hoy en día (por ejemplo, en la CDN unpkg a través de babel-plugin-unpkg) es reescribir todos los especificadores de importación básicos en sus URL absolutas apropiadas con anticipación, utilizando herramientas de compilación. Esto también se podría hacer en el momento de la instalación, de modo que cuando instale un paquete usando npm, reescriba automáticamente el contenido del paquete para usar URL absolutas o relativas en lugar de especificadores de importación simples.
El problema con este enfoque es que no funciona con import()
dinámico, ya que es imposible analizar estáticamente las cadenas pasadas a esa función. Podría inyectar una solución que, por ejemplo, cambie cada instancia de import(x)
a import(specifierToURL(x, import.meta.url))
, donde specifierToURL
es otra función generada por la herramienta de compilación. Pero al final esta es una abstracción con bastantes fugas, y la función specifierToURL
duplica en gran medida el trabajo de esta propuesta de todos modos.
A primera vista, los trabajadores de servicios parecen el lugar adecuado para realizar este tipo de traducción de recursos. Hemos hablado en el pasado sobre encontrar alguna manera de pasar el especificador junto con el evento de recuperación de un trabajador de servicio, permitiéndole así devolver una Response
apropiada.
Sin embargo, los trabajadores de servicio no están disponibles en la primera carga . Por lo tanto, en realidad no pueden ser parte de la infraestructura crítica utilizada para cargar módulos. Sólo se pueden utilizar como una mejora progresiva además de las recuperaciones que, de otro modo, generalmente funcionarían.
Si tiene aplicaciones simples sin necesidad de resolución de dependencias con alcance y tiene una herramienta de instalación de paquetes que le permite reescribir rutas en el disco dentro del paquete (a diferencia de las versiones actuales de npm), podría salirse con la suya con una asignación mucho más simple. Por ejemplo, si su herramienta de instalación creó un listado plano del formulario
node_modules_flattened/
lodash/
index.js
core.js
fp.js
moment/
index.js
html-to-dom/
index.js
entonces la única información que necesitas es
/node_modules_flattened/
)index.js
)Podría imaginarse un formato de configuración de importación de módulo que solo especificara estas cosas, o incluso solo algún subconjunto (si incluimos suposiciones para los demás).
Esta idea no funciona para aplicaciones más complejas que necesitan una resolución de alcance, por lo que creemos que la propuesta de mapa de importación completa es necesaria. Pero sigue siendo atractivo para aplicaciones simples, y nos preguntamos si hay una manera de hacer que la propuesta también tenga un modo fácil que no requiera enumerar todos los módulos, sino que dependa de convenciones y herramientas para garantizar que se necesite un mapeo mínimo. Discuta en el n.° 7.
Varias veces ha surgido que la gente desea proporcionar metadatos para cada módulo; por ejemplo, metadatos de integridad u opciones de recuperación. Aunque algunos han propuesto hacer esto con una declaración de importación, una consideración cuidadosa de las opciones lleva a preferir un archivo de manifiesto fuera de banda.
El mapa de importación podría ser ese archivo de manifiesto. Sin embargo, puede que no sea la mejor opción por varias razones:
Como se prevé actualmente, la mayoría de los módulos de una aplicación no tendrían entradas en el mapa de importación. El caso de uso principal es para módulos a los que necesita hacer referencia mediante especificadores simples, o módulos en los que necesita hacer algo complicado como polyfilling o virtualización. Si imagináramos que todos los módulos estuvieran en el mapa, no incluiríamos características convenientes como paquetes mediante barras diagonales.
Todos los metadatos propuestos hasta ahora son aplicables a cualquier tipo de recurso, no solo a los módulos de JavaScript. Probablemente una solución debería funcionar a un nivel más general.
Es natural que aparezcan varios <script type="importmap">
en una página, al igual que pueden aparecer varios <script>
de otros tipos. Nos gustaría habilitar esto en el futuro.
El mayor desafío aquí es decidir cómo se componen los múltiples mapas de importación. Es decir, dados dos mapas de importación que reasignan la misma URL, o dos definiciones de alcance que cubren el mismo espacio de prefijo de URL, ¿cuál debería ser el efecto en la página? El principal candidato actual es la resolución en cascada, que transforma los mapas de importación de especificador de importación → asignaciones de URL a una serie en cascada de especificador de importación → asignaciones de especificador de importación, y eventualmente toca fondo en un "especificador de importación recuperable" (esencialmente una URL).
Vea estos temas abiertos para más discusión.
Algunos casos de uso desean una forma de leer o manipular el mapa de importación de un reino desde un script, en lugar de insertar elementos <script type="importmap">
declarativos. Considérelo un "modelo de objetos de mapa de importación", similar al modelo de objetos CSS que permite manipular las reglas CSS generalmente declarativas de la página.
Los desafíos aquí giran en torno a cómo conciliar los mapas de importación declarativos con cualquier cambio programático, así como en qué momento del ciclo de vida de la página puede operar dicha API. En general, los diseños más simples son menos potentes y pueden cumplir con menos casos de uso.
Consulte estos temas abiertos para obtener más debates y casos de uso en los que una API programática podría ayudar.
import.meta.resolve()
La función import.meta.resolve(specifier)
propuesta permite que los scripts del módulo resuelvan especificadores de importación en URL en cualquier momento. Consulte whatwg/html#5572 para obtener más información. Esto está relacionado con la importación de mapas, ya que le permite resolver recursos "relativos al paquete", por ejemplo
const url = import . meta . resolve ( "somepackage/resource.json" ) ;
le daría la ubicación asignada apropiadamente de resource.json
dentro del espacio de nombres somepackage/
controlado por el mapa de importación de la página.
Varios miembros de la comunidad han estado trabajando en polyfills y herramientas relacionadas con la importación de mapas. Estos son los que conocemos:
package.json
y node_modules/
.package.json
.<script type="systemjs-importmap">
.¡No dudes en enviar una solicitud de extracción con más! Además, puede utilizar el número 146 en el rastreador de problemas para debatir sobre este espacio.
Este documento se originó a partir de una carrera de un día que involucró a @domenic, @hiroshige-g, @justinfagnani, @MylesBorins y @nyaxt. Desde entonces, @guybedford ha desempeñado un papel decisivo en la creación de prototipos y en impulsar el debate sobre esta propuesta.
¡Gracias también a todos los contribuyentes en el rastreador de problemas por su ayuda para desarrollar la propuesta!