Встраивание модулей ECMAScript в HTML
Я экспериментировал с новой поддержкой модуля ECMAScript, которая недавно была добавлена в браузеры. Приятно, наконец, возможность напрямую и чисто импортировать скрипты из JavaScript.
/example.html 🔍
<script type="module">
import {example} from '/example.js';
example();
</script>
/example.js
export function example() {
document.body.appendChild(document.createTextNode("hello"));
};
Однако, это только позволяет мне импортировать модули, которые определяются отдельными файлами JavaScript external. Обычно я предпочитаю встроить некоторые скрипты, используемые для первоначального рендеринга, поэтому их запросы не блокируют остальную часть страницы. С традиционной неофициально-структурированной библиотекой это может выглядеть так:
/inline-traditional.html 🔍
<body>
<script>
var example = {};
example.example = function() {
document.body.appendChild(document.createTextNode("hello"));
};
</script>
<script>
example.example();
</script>
Однако, наивно встраивание файлов модулей, очевидно, не будет работать, поскольку он удалит имя файла, используемое для идентификации модуля для других модулей. Нагрузка HTTP/2-сервера может быть каноническим способом обработки этой ситуации, но она по-прежнему не является вариантом во всех средах.
Можно ли выполнить эквивалентное преобразование с модулями ECMAScript?
Есть ли способ для <script type="module">
импортировать модуль, экспортированный другим в том же документе?
Я предполагаю, что это может сработать, разрешив script указать путь к файлу и вести себя так, как если бы он уже был загружен или вытолкнут с пути.
/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>
Или, может быть, совершенно другая эталонная схема, например, используется для локальных ссылок 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>
Но ни одна из этих гипотез действительно не работает, и я не видел альтернативы, которая делает.
Ответы
Ответ 1
Взлом вместе Наш собственный import from '#id'
Экспорт/импорт между встроенными скриптами не поддерживается, но это было весело, чтобы взломать реализацию для моих документов. Code-golfed до небольшого блока, я использую его вот так:
<script type="module" data-info="https://stackoverflow.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>
Ответ 2
Это возможно с работниками службы.
Поскольку рабочий сервис должен быть установлен до того, как он сможет обрабатывать страницу, для этого требуется отдельная страница для инициализации работника, чтобы избежать проблемы с курицей/яйцом - или страница может перезагрузиться, когда рабочий готов.
Вот пример, который должен быть работоспособен в браузерах, поддерживающих собственные ES-модули и async..await
(а именно 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;
}
Script тела кэшируются (также может использоваться собственный Cache
) и возвращаться для соответствующих запросов модуля.
Обеспокоенность
-
Этот подход уступает положению, созданному и размещенному с помощью инструмента для комплектации, такого как Webpack или Rollup, с точки зрения производительности, гибкости, надежности и поддержки браузера, особенно если блокировка одновременных запросов является основной задачей.
/li > -
Встроенные скрипты увеличивают использование полосы пропускания, этого естественно избежать, когда сценарии загружаются один раз и кэшируются браузером.
-
Встроенные скрипты не являются модульными и противоречат концепции модулей ES (если только они не генерируются из реальных модулей по серверному шаблону).
-
Инициализация рабочего пользователя должна выполняться на отдельной странице, чтобы избежать ненужных запросов.
-
Решение ограничивается одной страницей и не учитывает <base>
.
-
Регулярное выражение используется только для демонстрационных целей. При использовании, как в примере выше , он позволяет выполнять произвольный JS-код, который доступен на странице. Вместо этого следует использовать проверенную библиотеку, например parse5
(это приведет к накладным расходам на производительность и, тем не менее, могут возникнуть проблемы с безопасностью). Никогда не используйте регулярные выражения для разбора DOM.
Ответ 3
Я не считаю, что это возможно.
Для встроенных скриптов вы придерживаетесь одного из более традиционных способов модуляции кода, например, пространство имен, которое вы продемонстрировали с использованием объектных литералов.
С webpack вы можете сделать расщепление кода, которое вы могли бы использовать, чтобы захватить очень минимальный кусок кода при загрузке страницы, а затем постепенно захватывать по мере необходимости. Преимущество Webpack также заключается в том, что вы можете использовать синтаксис модуля (плюс тонну других улучшений ES201X) в виде дополнительных окружений, которые только Chrome Canary.