差点被 SystemJs 惊掉了下巴 解密模块加载黑魔法

发布于 2023-08-18 21:59:08 字数 8853 浏览 84 评论 0

最近一直在做一个技术改进:微前端中子应用采用 umd 方式分包构建,取代现有的 systemJs 方式构建,解决子应用稍微复杂一点后构建资源过大造成应用加载缓慢的问题。

依赖 umd 分包,就需要依赖 webpackJsonp 的全局变量通信,这个技改方案最后成功了,但这个过程让我对 SystemJs 有了新的认识。准确点说它差一点就成功忽悠住了我,幸好 18 岁的我保留了足够的好奇心,没有被表面现象懵逼。

根深蒂固的认知

作为一个工作 6 年的前端,虽然离牛逼还有成都地铁六号线那么远的距离,但自认为自己基础还是扎实。在我的认知里,所有的浏览器 JS 代码运行,都离不开 script 标签的引入,比如:

1. 内联 script

<script>
  console.log('I am inline script');
</script>

2. 远程脚本加载

<script src="http://localhost:5001/run.js"></script>

3. Es6 module

和前面一致,只是多一个 type="module" 标识

4. 动态 import()

/* hello.js */

// Default export
export default () => {
  console.log('Hi from the default export!');
};

// Named export ``
export const sayHi = (user) => {
  console.log('Hi from the named export!', user);
};
<script type="module">
  import('./hello.js')
    .then((module) => {
      module.default();
      // → 'Hi from the default export!'
      module.doStuff('doddle');
      // → 'Hi from the named export!, doddle'
    });
</script>

但这个语法支持的浏览器很少,还只是一个提案,chrome 也只有高版本做了支持。所以在业务开发中使用 webpack 打包,都对这个语法做了 polyfill,其原理还是利用了 script 加载与 webpackJsonp.push 劫持做的发布订阅来实现,具体原理在去年我一篇流水账中有提到: webpack 打包的代码怎么在浏览器跑起来的?看不懂算我输

差点刷新我认知的 SystemJs

这两年微前端的兴起,让 SystemJs 这个模块化方案也是火了一把,以前我是不知道 webpack 的 libraryTarget 配置还有 system 这一说的: webpack 之 libraryTarget 设置

SystemJS 是一个插件化的,基于标准的模块加载器。它提供了一个工作流,可以将为浏览器中编写的原始 ES6 模块代码转换为 System.register 模块格式,以在不支持原始 ES6 模块的旧版浏览器中运行,几乎可以达到运行原始 ES 模块的速度,同时支持 顶层 await ,动态导入,循环引用和实时绑定,import.meta.url,模块类型,导入映射,完整性和内容安全策略,并且在旧版浏览器中可兼容 IE11。

SystemJs 还没有概念的,可以跑一下官方 demo 感受一下它的 黑魔法systemjs-examples

SystemJs 看起牛逼在哪呢?以 demo 库的示例 dynamic-import 为例:

<html lang="en-US">
  <head>content="IE=edge">
    <title>SystemJS Dynamic Import Example</title>
    <script type="systemjs-importmap">
      {
        "imports": {
          "neptune": "./neptune.js"
        }
      }
    </script>
    <!-- 启动即运行 neptune.js -->
    <script type="systemjs-module" src="import:neptune"></script>
    <!-- load SystemJS itself from CDN -->
    <script src="https://cdn.jsdelivr.net/npm/systemjs/dist/system.js"></script>
  </head>
  <body>
    <button>加载</button>
  </body>
</html>
// neptune.js
System.register([], function (_export, _context) {
  return {
    execute: function() {
      document.body.appendChild(Object.assign(document.createElement('p'), {
        textContent: 'Neptune is a planet that revolves around the Sun'
      }));

      // 点击按钮后 加载 triton.js
      document.querySelector('#load').addEventListener('click', () => {
        console.log('start debug');
        _context.import('./triton.js').then(function (triton) {
          console.log("Triton was discovered on", triton.discoveryDate);
        });
      });
    }
  };
});
// triton.js
System.register([], function (_export, _context) {
  return {
    execute: function() {
      document.body.appendChild(Object.assign(document.createElement('p'), {
        textContent: 'Triton is a moon that revolves around Neptune.'
      }));
      _export("discoveryDate", "Oct. 10, 1846");
    }
  };
});

20210223225507

Demo 我稍微改了一下,把 triton.js 从主动动态加载,改成点击按钮后再动态加载,只是为了加载过程更明显。点击按钮后,界面和元素长下面这样:

20210223225736

发现没? triton.js 没有被加载到 html 中,但这个 JS 的内容确实是已经执行了,洋气不洋气, 惊不惊喜?!!!难道 script 真的可以不加入到 html 就能执行?

但再仔细搜索,发现是有 script 请求下载记录的:

20210223231942

黑魔法解密

如果你想要快速知道答案,你可以在 network 直接点击 script 加载的触发节点:

20210223232541

顺着点开,你会发现黑魔法不过是一个戏法:

20210223232926

先把 script 加载到 html 中,加载完成后,再将这个 script 从 html 中移除,看起让人不明觉厉。
2021225-5617

浅入 SystemJs

为什么要做这种骚操作(卸磨杀驴)?留在那貌似也没有什么问题。

这种操作也不是不可以,因为 script 标签加载完成就会马上执行,除非加上了 defer 标识,或者采用了 preload 或者 prefetch 标签来预加载。一旦 script 标签中的内容被执行,其有用或者需要再次被调用的部分,就会以引用的方式存在内存中,这时 script 中的内容确实就是个摆设,重绘重排都没用,只有重新加载才会触发执行。

简单了解一下 SystemJs 的原理:

20210228230222

当我们引入 <script src="https://cdn.net//system.js"></script> 时,就会完成以上操作,简单来讲就是生成一个 System 实例,遍历 System 相关的 script 标签,做一下预处理。system-module 类的标签其实是唤起模块执行的一个入口,其实质是调用 System.import 方法。

与 System.import 相对应的,是 System.register,仔细看上面示例:

// _context 意指实例与 System
_context.import('./triton.js')
  .then(function (triton) {
    console.log("Triton was discovered on", triton.discoveryDate);
  });


System.register([/*依赖项*/], function (_export, _context) {
  return {
    execute: function() {
      document.body.appendChild(Object.assign(document.createElement('p'), {
        textContent: 'Triton is a moon that revolves around Neptune.'
      }));
      _export("discoveryDate", "Oct. 10, 1846");
    }
  };
});

当调用 import('./triton.js') 时,System 就会发起 triton.js 的 script 加载,当加载完成后,就会开始 System.register 的模块注册,这时只会注册模块为一个函数,并还不会执行,因为要检测模块是否还有依赖,如果有,就需要待依赖模块加载完后,再调用 execute 方法执行并导出。然后通知 import 方法,导出已收到,resolve 执行 then 中内容。

除了支持 SystemJs 模块以外,还支持 amdumd 模块,但其依赖扩展 extras/amd.js , 其原理就是在 window 上注入了 amd 模块依赖的 define 方法,然后这个方法会把 amd 转化成 register 注入,原理还是比较易懂。但引入这个扩展前,还是有一些坑,我踩过:

  • 扩展加入时机:只能是在 systemJs 加载执行完后,扩展才能接着执行,因为其依赖 global.System.constructor.prototype ;
  • 扰乱全局 umd 模块加载,如果你应用本身有一些 umd 模块,其加载方式是 global 加载(注册在 window 上),比较常见的就是 webpack 打包,为了减少包体积,我们用了 externals ,但因为 amd 扩展的引入,这些 global 依赖就变成了 SystemJs 导入,应用会加载失效,所以有一种投机的加载方式就是: 待其他 js script 导入完成后,再执行 extras/amd

以上只是 SystemJs 浏览器相关的一些比较核心的流程,很多细节性的处理我也没深究,应该差不了多少。

欧洲杯看完了,补一个 System.import 的导入流程

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

柒夜笙歌凉

暂无简介

文章
评论
361 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文