javascript-以HTML格式内联ECMAScript模块

我一直在尝试新的本机ECMAScript模块支持,该支持最近已添加到浏览器中。 最终能够直接从JavaScript干净地导入脚本是令人高兴的。

 /example.html 🔍 
<script type="module">
  import {example} from '/example.js';

  example();
</script>
 /inline-id.html 🔍 
export function example() {
  document.body.appendChild(document.createTextNode("hello"));
};

但是,这仅允许我导入由单独的外部JavaScript文件定义的模块。 我通常喜欢内联一些用于初始呈现的脚本,因此它们的请求不会阻塞页面的其余部分。 使用传统的非正式结构库,可能看起来像这样:

 /inline-id.html 🔍 
<body>
<script>
  var example = {};

  example.example = function() {
    document.body.appendChild(document.createTextNode("hello"));
  };
</script>
<script>
  example.example();
</script>

但是,天真的内联模块文件显然不起作用,因为它会将用于标识该模块的文件名删除为其他模块。 HTTP / 2服务器推送可能是处理这种情况的规范方法,但并非在所有环境中都是一种选择。

是否可以使用ECMAScript模块执行等效转换?  /inline-id.html 🔍 是否可以导入同一文档中另一个模块导出的模块?


我想这可以通过允许脚本指定文件路径来工作,并且表现得好像它已经从路径下载或推送一样。

 /inline-id.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>

但是这些假设都没有实际起作用,而且我还没有看到一种可行的选择。

3个解决方案
18 votes

共同破解我们自己的<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>

内联脚本之间的导出/导入本身不受支持,但是将文档的实现统一在一起是一个有趣的练习。 代码遍历到一个小块,我像这样使用它:

<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>

我们需要使用自定义类型(如inline-module)来定义脚本元素,而不是module。这可以防止浏览器自己尝试执行其内容,让我们来处理它们。 该脚本(下面的完整版本)在文档中找到所有blob:脚本元素,并将它们转换为具有所需行为的常规脚本模块元素。

内联脚本不能直接相互导入,因此我们需要为脚本提供可导入的URL。 我们为它们每个生成一个module URL,其中包含它们的代码,并将inline-module属性设置为从该URL运行而不是内联运行。 blob: URL的行为类似于来自服务器的正常URL,因此可以从其他模块中导入它们。 每次我们看到随后的inline-module尝试从'#example'导入时(其中example是我们已转换的inline-module的ID),我们都会将该导入修改为从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);
}

警告:此简单的实现无法处理在解析初始文档后添加的脚本元素,也不能允许脚本元素从文档中之后出现的其他脚本元素导入。 如果文档中同时具有moduleinline-module脚本元素,则它们的相对执行顺序可能不正确。 源代码转换是使用粗略的正则表达式执行的,该表达式不能处理某些边缘情况(例如ID中的句点)。

ON STRIKE - Jeremy Banks answered 2020-08-01T09:35:55Z
7 votes

服务人员可以做到这一点。

由于服务工作者应先安装,然后才能处理页面,因此这就需要有一个单独的页面来初始化工作者,以避免出现鸡/蛋问题-否则在工作者准备就绪时可以重新加载页面。

以下示例应该在支持本机ES模块和parse5(即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;
}

脚本主体被缓存(也可以使用本机parse5),并针对相应的模块请求返回。

顾虑

  • 这种方法在性能,灵活性,可靠性和浏览器支持方面不如使用Webpack或Rollup等捆绑工具构建和分块的应用程序-特别是在阻止并发请求是主要问题时。

  • 内联脚本会增加带宽使用量,当脚本一次加载并由浏览器缓存时,自然可以避免这种情况。

  • 内联脚本不是模块化的,并且与ES模块的概念相矛盾(除非它们是由服务器端模板从真实模块中生成的)。

  • 服务人员初始化应在单独的页面上执行,以避免不必要的请求。

  • 该解决方案仅限于单个页面,并且未考虑parse5

  • 正则表达式仅用于演示目的。 当像上面的示例中那样使用时,它将启用页面上可用的任意JS代码的执行。 应该改用经过验证的库,例如parse5(这将导致性能开销,但仍然可能存在安全隐患)。 切勿使用正则表达式解析DOM。

Estus Flask answered 2020-08-01T09:37:09Z
2 votes

我认为这是不可能的。

对于内联脚本,您只能使用一种较传统的代码模块化方法,例如使用对象文字演示的命名空间。

使用webpack,您可以进行代码拆分,您可以使用它来在页面加载时捕获非常少的代码块,然后根据需要增量地捕获其余代码。 Webpack的优势还在于,您可以在仅Chrome Canary的更多环境中使用模块语法(以及其他大量ES201X改进)。

bmceldowney answered 2020-08-01T09:37:38Z
translate from https://stackoverflow.com:/questions/43817297/inlining-ecmascript-modules-in-html