Встраивание модулей 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.