javascript - from - Incrustación de módulos ECMAScript en HTML
import javascript (3)
He estado experimentando con el nuevo soporte nativo del módulo ECMAScript que recientemente se ha agregado a los navegadores. Es agradable finalmente poder importar guiones directamente y limpiamente desde JavaScript.
/example.html 🔍
<script type="module">
import {example} from ''/example.js'';
example();
</script>
/example.js
export function example() {
document.body.appendChild(document.createTextNode("hello"));
};
Sin embargo, esto solo me permite importar módulos que están definidos por archivos JavaScript externos separados. Por lo general, prefiero alinear algunas secuencias de comandos utilizadas para la representación inicial, por lo que sus solicitudes no bloquean el resto de la página. Con una biblioteca tradicional con estructura informal, podría verse así:
/inline-traditional.html 🔍
<body>
<script>
var example = {};
example.example = function() {
document.body.appendChild(document.createTextNode("hello"));
};
</script>
<script>
example.example();
</script>
Sin embargo, ingenuamente los archivos de los módulos internos obviamente no funcionarán, ya que eliminarían el nombre de archivo utilizado para identificar el módulo a otros módulos. El empuje del servidor HTTP / 2 puede ser la manera canónica de manejar esta situación, pero todavía no es una opción en todos los entornos.
¿Es posible realizar una transformación equivalente con módulos ECMAScript? ¿Hay alguna forma de que un <script type="module">
importe un módulo exportado por otro en el mismo documento?
Imagino que esto podría funcionar al permitir que el script especifique una ruta de archivo y se comporte como si ya se hubiera descargado o empujado desde la ruta.
/inline-name.html 🔍
<script type="module" name="/example.js">
export function example() {
document.body.appendChild(document.createTextNode("hello"));
};
</script>
<script type="module">
import {example} from ''/example.js'';
example();
</script>
O tal vez por un esquema de referencia completamente diferente, como el que se usa para los refs locales de SVG:
/inline-id.html 🔍
<script type="module" id="example">
export function example() {
document.body.appendChild(document.createTextNode("hello"));
};
</script>
<script type="module">
import {example} from ''#example'';
example();
</script>
Pero ninguno de estos hipotéticos realmente funciona, y no he visto una alternativa que sí lo haga.
Hackear juntos nuestra propia import from ''#id''
Las exportaciones / importaciones entre scripts en línea no son compatibles de forma nativa, pero fue un ejercicio divertido para hackear una implementación para mis documentos. Codificado como golf en un bloque pequeño, lo uso así:
<script type="module" data-info="https://.com/a/43834063">let l,e,t
=''script'',p=/(from/s+|import/s+)[''"](#[/w/-]+)[''"]/g,x=''textContent'',d=document,
s,o;for(o of d.querySelectorAll(t+''[type=inline-module]''))l=d.createElement(t),o
.id?l.id=o.id:0,l.type=''module'',l[x]=o[x].replace(p,(u,a,z)=>(e=d.querySelector(
t+z+''[type=module][src]''))?a+`/* ${z} */''${e.src}''`:u),l.src=URL.createObjectURL
(new Blob([l[x]],{type:''application/java''+t})),o.replaceWith(l)//inline</script>
<script type="inline-module" id="utils">
let n = 1;
export const log = message => {
const output = document.createElement(''pre'');
output.textContent = `[${n++}] ${message}`;
document.body.appendChild(output);
};
</script>
<script type="inline-module" id="dogs">
import {log} from ''#utils'';
log("Exporting dog names.");
export const names = ["Kayla", "Bentley", "Gilligan"];
</script>
<script type="inline-module">
import {log} from ''#utils'';
import {names as dogNames} from ''#dogs'';
log(`Imported dog names: ${dogNames.join(", ")}.`);
</script>
En lugar de <script type="module">
, necesitamos definir nuestros elementos de script utilizando un tipo personalizado como <script type="inline-module">
. Esto evita que el navegador intente ejecutar sus propios contenidos, dejándolos para que los manejemos. El script (la versión completa a continuación) encuentra todos los elementos del script del inline-module
en el documento y los transforma en elementos del módulo de scripts regulares con el comportamiento que queremos.
Los scripts en línea no se pueden importar directamente entre sí, por lo que debemos otorgarles a los scripts URLs importables. blob:
un blob:
URL para cada uno de ellos, que contiene su código, y establecemos que el atributo src
ejecute desde esa URL en lugar de ejecutarse en línea. El blob:
URL actúan como URL normales del servidor, por lo que se pueden importar desde otros módulos. Cada vez que vemos un inline-module
posterior tratando de importar desde ''#example''
, donde el example
es el ID de un inline-module
que hemos transformado, modificamos esa importación para importar desde el blob:
URL en su lugar. Esto mantiene la deduplicación de ejecución y referencia única que los módulos deben tener.
<script type="module" id="dogs" src="blob:https://example.com/9dc17f20-04ab-44cd-906e">
import {log} from /* #utils */ ''blob:https://example.com/88fd6f1e-fdf4-4920-9a3b'';
log("Exporting dog names.");
export const names = ["Kayla", "Bentley", "Gilligan"];
</script>
La ejecución de los elementos del script del módulo siempre se pospone hasta después de que se haya analizado el documento, por lo que no debemos preocuparnos por intentar respaldar la forma en que los elementos del script tradicional pueden modificar el documento mientras se está analizando.
export {};
for (const original of document.querySelectorAll(''script[type=inline-module]'')) {
const replacement = document.createElement(''script'');
// Preserve the ID so the element can be selected for import.
if (original.id) {
replacement.id = original.id;
}
replacement.type = ''module'';
const transformedSource = original.textContent.replace(
// Find anything that looks like an import from ''#some-id''.
/(from/s+|import/s+)[''"](#[/w/-]+)[''"]/g,
(unmodified, action, selector) => {
// If we can find a suitable script with that id...
const refEl = document.querySelector(''script[type=module][src]'' + selector);
return refEl ?
// ..then update the import to use that script''s src URL instead.
`${action}/* ${selector} */ ''${refEl.src}''` :
unmodified;
});
// Include the updated code in the src attribute as a blob URL that can be re-imported.
replacement.src = URL.createObjectURL(
new Blob([transformedSource], {type: ''application/javascript''}));
// Insert the updated code inline, for debugging (it will be ignored).
replacement.textContent = transformedSource;
original.replaceWith(replacement);
}
Advertencias: esta implementación simple no maneja los elementos del script añadidos después de que el documento inicial ha sido analizado, ni permite que los elementos del script se importen desde otros elementos del script que aparecen después de ellos en el documento. Si tiene elementos de script de module
y module
inline-module
en un documento, su orden de ejecución relativa puede no ser correcta. La transformación del código fuente se realiza utilizando una expresión regular bruta que no manejará algunos casos extremos, como los períodos en ID.
Esto es posible con los trabajadores del servicio.
Dado que un trabajador de servicio debe instalarse antes de que pueda procesar una página, esto requiere tener una página separada para inicializar a un trabajador para evitar problemas de huevo / gallina, o una página puede volver a cargarse cuando un trabajador esté listo.
Aquí hay un ejemplo que se supone que es viable en los navegadores que admiten módulos ES nativos y async..await
(es decir, Chrome):
index.html
<html>
<head>
<script>
(async () => {
try {
const swInstalled = await navigator.serviceWorker.getRegistration(''./'');
await navigator.serviceWorker.register(''sw.js'', { scope: ''./'' })
if (!swInstalled) {
location.reload();
}
} catch (err) {
console.error(''Worker not registered'', err);
}
})();
</script>
</head>
<body>
World,
<script type="module" data-name="./example.js">
export function example() {
document.body.appendChild(document.createTextNode("hello"));
};
</script>
<script type="module">
import {example} from ''./example.js'';
example();
</script>
</body>
</html>
sw.js
self.addEventListener(''fetch'', e => {
// parsed pages
if (/^https:////run.plnkr.co///w+//$/.test(e.request.url)) {
e.respondWith(parseResponse(e.request));
// module files
} else if (cachedModules.has(e.request.url)) {
const moduleBody = cachedModules.get(e.request.url);
const response = new Response(moduleBody,
{ headers: new Headers({ ''Content-Type'' : ''text/javascript'' }) }
);
e.respondWith(response);
} else {
e.respondWith(fetch(e.request));
}
});
const cachedModules = new Map();
async function parseResponse(request) {
const response = await fetch(request);
if (!response.body)
return response;
const html = await response.text(); // HTML response can be modified further
const moduleRegex = /<script type="module" data-name="([/w./]+)">([/s/S]*?)<//script>/;
const moduleScripts = html.match(new RegExp(moduleRegex.source, ''g''))
.map(moduleScript => moduleScript.match(moduleRegex));
for (const [, moduleName, moduleBody] of moduleScripts) {
const moduleUrl = new URL(moduleName, request.url).href;
cachedModules.set(moduleUrl, moduleBody);
}
const parsedResponse = new Response(html, response);
return parsedResponse;
}
Los cuerpos de script se almacenan en caché (también se puede usar Cache
nativo) y se devuelven para las solicitudes de módulo respectivas.
Preocupaciones
El enfoque es inferior a la aplicación construida y fragmentada con la herramienta de agrupamiento como Webpack o Rollup en términos de rendimiento, flexibilidad, solidez y compatibilidad con navegadores, especialmente si el bloqueo de solicitudes simultáneas es la principal preocupación.
Los scripts en línea aumentan el uso del ancho de banda, esto se evita naturalmente cuando los scripts se cargan una vez y el navegador los almacena en caché.
Los scripts en línea no son modulares y contradicen el concepto de módulos ES (a menos que se generen a partir de módulos reales mediante una plantilla del lado del servidor).
La inicialización del trabajador de servicio debe realizarse en una página separada para evitar solicitudes innecesarias.
La solución está limitada a una sola página y no tiene en cuenta
<base>
.La expresión regular se usa solo con fines de demostración. Cuando se utiliza como en el ejemplo anterior , permite la ejecución de código JS arbitrario que está disponible en la página. En su
parse5
debe usar una biblioteca comprobada comoparse5
(esto generará una sobrecarga de rendimiento y, sin embargo, puede haber problemas de seguridad). Nunca use expresiones regulares para analizar el DOM .
No creo que eso sea posible.
Para los scripts en línea, está atrapado con una de las formas más tradicionales de modular el código, como el espacio de nombres que ha demostrado utilizando literales de objeto.
Con el paquete web puedes dividir el código que puedes utilizar para tomar un pedazo mínimo de código en la carga de la página y luego tomar el resto de manera incremental según sea necesario. Webpack también tiene la ventaja de que le permite usar la sintaxis del módulo (más una tonelada de otras mejoras de ES201X) en forma de más entornos que solo Chrome Canary.