Встраивание модулей 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. Я обычно предпочитаю встроить некоторые сценарии, используемые для начального рендеринга, чтобы их запросы не блокировали остальную часть страницы. С традиционной неформальной библиотекой это может выглядеть так:
/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">
импортировать модуль, экспортированный другим в том же документе?
Я полагаю, что это может работать, позволяя сценарию указать путь к файлу и вести себя так, как будто он уже был загружен или удален из пути.
/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>
Но ни одна из этих гипотез на самом деле не работает, и я не видел альтернативы, которая бы работала.
5 ответов
Взламывая вместе наши собственные import from '#id'
Экспорт / импорт между встроенными сценариями изначально не поддерживается, но было забавным занятием собрать реализацию для моих документов. Код-гольф до небольшого блока, я использую его так:
<script type="module" data-info="https://stackru.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>
Вместо <script type="module">
нам нужно определить элементы нашего скрипта, используя пользовательский тип, такой как <script type="inline-module">
, Это препятствует тому, чтобы браузер сам пытался выполнить их содержимое, оставляя их нам для обработки. Скрипт (полная версия ниже) находит все inline-module
элементы сценария в документе и преобразует их в обычные элементы модуля сценария с нужным нам поведением.
Встроенные сценарии не могут быть импортированы напрямую друг от друга, поэтому нам нужно предоставить сценарии импортируемые URL-адреса. Мы генерируем blob:
URL для каждого из них, содержащий их код, и установите src
Атрибут для запуска с этого URL вместо запуска inline. blob:
URL-адреса действуют как обычные URL-адреса с сервера, поэтому их можно импортировать из других модулей. Каждый раз, когда мы видим inline-module
пытается импортировать из '#example'
, где example
это идентификатор inline-module
мы изменили, мы модифицируем этот импорт, чтобы импортировать из blob:
URL вместо Это поддерживает единовременное выполнение и дедупликацию ссылок, которые должны иметь модули.
<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>
Выполнение элементов сценария модуля всегда откладывается до тех пор, пока документ не будет проанализирован, поэтому нам не нужно беспокоиться о том, чтобы поддерживать способ, которым традиционные элементы сценария могут изменять документ, пока он еще анализируется.
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);
}
Предупреждения: эта простая реализация не обрабатывает элементы сценария, добавленные после анализа исходного документа, и не позволяет импортировать элементы сценария из других элементов сценария, которые появляются после них в документе. Если у вас есть оба module
а также inline-module
Элементы скрипта в документе, их относительный порядок выполнения может быть неправильным. Преобразование исходного кода выполняется с использованием грубого регулярного выражения, которое не будет обрабатывать некоторые крайние случаи, такие как точки в идентификаторах.
Это возможно с работниками сферы обслуживания.
Так как сервисный работник должен быть установлен до того, как он сможет обработать страницу, для этого требуется отдельная страница, чтобы инициализировать работника, чтобы избежать проблемы с курицей или яйцом - или страницу можно перезагрузить, когда работник будет готов.
Вот пример, который должен работать в браузерах, которые поддерживают собственные модули 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;
}
Тела скриптов кэшируются Cache
также может использоваться) и возвращаться для соответствующих запросов модуля.
Обеспокоенность
Этот подход уступает приложению, созданному и разбитому на части с помощью такого инструмента, как Webpack или Rollup, с точки зрения производительности, гибкости, надежности и поддержки браузера - особенно, если блокирование одновременных запросов является основной задачей.
Встроенные сценарии увеличивают использование полосы пропускания, этого естественно избегать, когда сценарии загружаются один раз и кэшируются браузером.
Встроенные скрипты не являются модульными и противоречат концепции модулей ES (если они не генерируются из реальных модулей с помощью серверного шаблона).
Инициализация работника службы должна выполняться на отдельной странице, чтобы избежать ненужных запросов.
Решение ограничено одной страницей и не занимает
<base>
в учетную запись.Регулярное выражение используется только в демонстрационных целях. При использовании, как в примере выше, он позволяет выполнять произвольный код JS, который доступен на странице. Проверенная библиотека, как
parse5
следует использовать вместо этого (это приведет к снижению производительности, и все же могут возникнуть проблемы с безопасностью). Никогда не используйте регулярные выражения для анализа DOM.
Я изменил ответ Джереми с помощью этой статьи, чтобы предотвратить выполнение скриптов.
<script data-info="https://stackoverflow.com/a/43834063">
// awsome guy on [data-info] wrote 90% of this but I added the mutation/module-type part
let l,e,t='script',p=/(from\s+|import\s+)['"](#[\w\-]+)['"]/g,x='textContent',d=document,s,o;
let evls = event => (
event.target.type === 'javascript/blocked',
event.preventDefault(),
event.target.removeEventListener( 'beforescriptexecute', evls ) )
;(new MutationObserver( mutations =>
mutations.forEach( ({ addedNodes }) =>
addedNodes.forEach( node =>
( node.nodeType === 1 && node.matches( t+'[module-type=inline]' )
&& (
node.type = 'javascript/blocked',
node.addEventListener( 'beforescriptexecute', evls ),
o = node,
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
) ) )))
.observe( document.documentElement, {
childList: true,
subtree: true
} )
// for(o of d.querySelectorAll(t+'[module-type=inline]'))
// 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>
Я надеюсь, что это решит проблему добавления динамических сценариев (с использованием MutationObserver), vs-code без подсветки синтаксиса (сохранение type = module), и я полагаю, что с помощью того же MutationObserver можно будет выполнять сценарии после добавления импортированных идентификаторов. в ДОМ.
Скажите, пожалуйста, есть ли здесь проблемы!
Я не верю, что это возможно.
Для встроенных сценариев вы придерживаетесь одного из более традиционных способов модуляции кода, таких как пространство имен, которое вы продемонстрировали, используя объектные литералы.
С помощью веб-пакета вы можете выполнять разбиение кода, которое можно использовать для получения минимального фрагмента кода при загрузке страницы, а затем постепенно увеличивать остальное по мере необходимости. Webpack также имеет то преимущество, что позволяет использовать синтаксис модуля (плюс кучу других улучшений ES201X) в большем количестве сред, чем просто Chrome Canary.
Мы можем использовать blob и importmap для импорта встроенных скриптов.
https://github.com/xitu/inline-модуль