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

Другие вопросы по тегам