Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

分享小技巧:实现在浏览器中import内联JS模块 #1142

Open
moegirlwangge opened this issue Mar 4, 2022 · 1 comment
Open

分享小技巧:实现在浏览器中import内联JS模块 #1142

moegirlwangge opened this issue Mar 4, 2022 · 1 comment

Comments

@moegirlwangge
Copy link
Member

https://mp.weixin.qq.com/s?__biz=Mzg2ODQ1OTExOA==&mid=2247496539&idx=1&sn=b0972823804fb179c094d1f95ab13d3f

@github-actions
Copy link

github-actions bot commented Mar 4, 2022

分享小技巧:实现在浏览器中import内联JS模块 by 字节前端 ByteFE

现代浏览器支持了ES Modules[1],也就是浏览器原生支持的 JavaScript 模块化方案。虽然考虑兼容性,我们还很少能够把 ES Modules 用于生产环境,但是在开发、测试、学习的场景中,ES Modules 发挥了越来越大的作用,比如构建工具Vite[2],就利用 ES Modules 来快速提供开发调试环境。React 和 Vue 框架的学习中,也都可以利用 ES Modules 不用安装本地构建工具,直接在浏览器上体验这些现代框架。

不过 ES Modules 有个局限性,就是它在浏览器里能够 import 指定 URL 的模块化 JS 代码,但是不能 import 自身 HTML 文件里的模块,比如:

<script type="module">
import {createApp} from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';
</script>

我们没有办法做到下面这种:

<script type="module" id="foo">
export default {foo'foo'};
</script>
<script type="module" id="bar">
import foo from '#foo'// 我想在这里引用上面的script标签里export的对象
</script>

但是如果能实现这种 inline-import,其实还挺有用的,这就意味着我们可以在像 CodePen 这样简单的 Playground 环境中使用多个 JavaScript 模块,而不用把它们先发布成在线的 JS 文件再 import。

不过要实现 inline-import,也不是那么容易。

思路上,我们可以借助Blob[3]对象来实现,Blob 对象有一些神奇的能力,我在前端冷知识系列中分享过一篇文章《超好用的 Blob 对象!》[4],有兴趣的同学可以去看一下。

言归正传,我们可以实现一个函数,将一段 JavaScript 文本创建成 Blob 对象,并返回 Blob 对象的 URL。

function getBlobURL(module{
  const jsCode = module.innerHTML;
  const blob = new Blob([jsCode], {type'text/javascript'});
  const blobURL = URL.createObjectURL(blob);
  return blobURL;
}

接着我们实现一个 inlineImport 函数:

// https://github.com/WICG/import-maps
const map = {imports: {}, scopes: {}};

window.inlineImport = async (moduleID) => {
  const {imports} = map;
  let blobURL = null;
  if(moduleID in imports) blobURL = imports[moduleID];
  else {
    const module = document.querySelector(`script[type="inline-module"]${moduleID}`);
    if(module) {
      blobURL = getBlobURL(module);
      imports[moduleID] = blobURL;
    }
  }
  if(blobURL) {
    const result = await import(blobURL);
    return result;
  }
  return null;
};

上面这段代码不复杂,结合 getBlobURL,其核心就是从标签<script type="inline-module">中获取 JavaScript 代码字符串然后生成 blobURL,并且将它缓存在 map 对象里,这样下次如果再 import,就直接从 map 缓存中取。取出的 blobURL,通过 ES Modules 原生的动态 import 方法加载。有了 inlineImport 函数之后,我们就可以这样用:

<script type="inline-module" id="foo">
  const foo = 'bar';
  export default {foo};
</script>
<script src="https://unpkg.com/inline-module/index.js"></script>
<script type="module">
  const foo = (await inlineImport('#foo')).default;
  console.log(foo); // {foo: 'bar'}
</script>

这样实现可以解决大部分问题,但是用起来还是不爽,因为这样只能动态 import。事实上,我们希望也能够以静态的方式 import,比如const foo = (await inlineImport('#foo')).default;可以写成import foo from '#foo';

实际上这个也是可以实现的,要用到现代浏览器的另一个特性,importmap。

importmap 本来是为了解决 ES Modules 引入模块的别名问题,比如我们觉得下面的代码写得不爽,因为 import 的 URL 太长了。

<script type="module">
import {createApp} from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';
</script>

可以改成:

<script type="importmap">
  {
    "imports": {
      "vue""https://unpkg.com/vue@3/dist/vue.esm-browser.js"
    }
  }
</script>
<script type="module">
import {createApp} from 'vue';
</script>

也就是在前面加一个<scirpt type="importmap">给要 import 的模块 URL 加一个别名就行了。

不过要注意,importmap 使用有限制,首先页面上只能有一个type="importmap"的 Script 标签,多个是不支持的,另外 importmap 的位置要在所有<script type="module">的元素出现之前。

那么,我们接着就可以利用生成 importmap 的思路来实现静态的 inline-import 了:

const currentScript = document.currentScript || document.querySelector('script');

function setup({
  const modules = document.querySelectorAll('script[type="inline-module"]');
  const importMap = {};
  [...modules].forEach((module) => {
    const {id} = module;
    if(id) {
      importMap[`#${id}`] = getBlobURL(module);
    }
  });
  const importMapEl = document.querySelector('script[type="importmap"]');
  if(importMapEl) {
    // map = JSON.parse(mapEl.innerHTML);
    throw new Error('Cannot setup after importmap is set. Use <script type="inline-module-importmap"> instead.');
  }

  const externalMapEl = document.querySelector('script[type="inline-module-importmap"]');
  if(externalMapEl) {
    const externalMap = JSON.parse(externalMapEl.textContent);
    Object.assign(map.imports, externalMap.imports);
    Object.assign(map.scopes, externalMap.scopes);
  }

  Object.assign(map.imports, importMap);

  const mapEl = document.createElement('script');
  mapEl.setAttribute('type''importmap');
  mapEl.textContent = JSON.stringify(map);
  currentScript.after(mapEl);
}

if(currentScript.hasAttribute('setup')) {
  setup();
}

这个函数的内容看起来稍微多一些,主要是处理 importmap 的规则,如果页面上已经有 importmap 标签,就不能再创建 importmap 了,要抛出异常,另外用户确实需要自己创建 importmap,我们可以让用户用<script type="inline-module-import">代替,然后我们自己合并 JSON 数据,也就是代码逻辑里 externalMapEl 的这部分。最后,最核心的部分就是前面得到模块的 BlobURL,然后针对 id 和 BlobURL 生成 importMap,最终将 importMap 挂载到 HTML 文档中。

有了这个 setup 方法之后,我们已经可以用静态的 import 了,我在代码的最后,如果 script 标签上设置 setup 属性,那么就自动运行setup()

这样我们就可以这么写:

<script type="inline-module" id="foo">
  const foo = 'bar';
  export default {foo};
</script>
<script src="https://unpkg.com/inline-module/index.js" setup></script>
<script type="module">
  import foo from '#foo';
  console.log(foo); // {foo: 'bar'}
</script>

或者要用到自定义的 importmap 的时候可以这么写:

<script type="inline-module-importmap">
  {
    "imports": {
      "vue""https://unpkg.com/vue@3/dist/vue.esm-browser.js"
    }
  }
</script>
<script type="inline-module" id="foo">
const foo = 'bar';
export default foo;
</script>
<script src="https://unpkg.com/inline-module/index.js" setup></script>
<script type="module">
  import foo from '#foo'
  console.log(foo);
  import {createApp} from 'vue';
  console.log(createApp);
</script>

只是需要注意的是,<script src="https://unpkg.com/inline-module/index.js" setup></script>这段必须出现在所有的type="inline-module"的 script 标签之后,所有type="module"的 script 标签之前。这样,我们就可以愉快地使用 inline-module 啦~有需要使用的同学,可以直接使用稀土掘金开源的 GitHub 仓库代码:github.com/xitu/inline…[5]有任何问题欢迎反馈~

参考资料

[1]

ES Modules: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules

[2]

Vite: https://vitejs.dev/

[3]

Blob: https://developer.mozilla.org/zh-CN/docs/Web/API/Blob

[4]

《超好用的Blob对象!》: https://github.com/akira-cn/FE_You_dont_know/issues/12

[5]

github.com/xitu/inline…: https://github.com/xitu/inline-module


@github-actions github-actions bot changed the title archive_request 分享小技巧:实现在浏览器中import内联JS模块 Mar 4, 2022
@moegirlwangge moegirlwangge transferred this issue from another repository Mar 4, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant