在html中内联ecmascript模块

yizd12fk  于 2021-09-13  发布在  Java
关注(0)|答案(4)|浏览(317)

我一直在试验最近添加到浏览器中的新的本机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>

但这两种假设都不起作用,而且我还没有看到另一种可行的方法。

nnvyjq4y

nnvyjq4y1#

从“#id”拼凑我们自己的输入

内联脚本之间的导出/导入本机不受支持,但为我的文档拼凑一个实现是一个有趣的练习。代码向下延伸到一个小块,我这样使用它:

<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"> ,我们需要使用自定义类型定义脚本元素,如 <script type="inline-module"> . 这可以防止浏览器试图自行执行其内容,而将其留给我们来处理。脚本(下面的完整版本)将查找所有 inline-module 为文档中的元素编写脚本,并将它们转换为具有所需行为的常规脚本模块元素。
内联脚本不能直接从彼此导入,因此我们需要为脚本提供可导入的URL。我们生成一个 blob: 它们的url,包含它们的代码,并设置 src 属性从该url运行,而不是内联运行。这个 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);
}

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

pdkcd3nj

pdkcd3nj2#

这在服务人员中是可能的。
由于服务工作者应该在能够处理页面之前安装,因此需要有一个单独的页面来初始化工作者以避免鸡/蛋问题,或者在工作者准备好后可以重新加载页面。

范例

这里有一个演示,应该可以在支持本机es模块和 async..await (即铬):
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等捆绑工具构建和分块的应用程序,尤其是在主要关注阻止并发请求的情况下。
内联脚本增加了带宽使用。当脚本加载一次并由浏览器缓存时,自然可以避免这种情况。
内联脚本不是模块化的,并且与ecmascript模块的概念相矛盾(除非它们是由服务器端模板从实际模块生成的)。
服务工作者初始化应在单独的页面上执行,以避免不必要的请求。
该解决方案仅限于单个页面,不需要 <base> 考虑到。
正则表达式仅用于演示目的。在上面的示例中使用时,它支持执行页面上可用的任意javascript代码。经验证的类库 parse5 应改为使用(这将导致性能开销,而且可能存在安全问题)。永远不要使用正则表达式来解析dom。

u3r8eeie

u3r8eeie3#

我认为那是不可能的。
对于内联脚本,您必须使用一种更传统的模块化代码的方法,比如使用对象文本演示的名称空间。
使用webpack,您可以进行代码拆分,您可以使用它在页面加载时获取非常小的代码块,然后根据需要增量获取其余的代码块。webpack还有一个优势,即允许您在比chrome canary更多的环境中使用模块语法(以及大量其他es201x改进)。

okxuctiv

okxuctiv4#

我使用本文修改了jeremy的答案,以防止脚本执行

<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),而不是代码不语法突出显示(保留type=module),我设想,一旦导入的ID添加到dom中,使用相同的mutationobserver就可以执行脚本。
请告诉我这是否有问题!

相关问题