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

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

Open
closertb opened this issue Feb 28, 2021 · 0 comments
Open

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

closertb opened this issue Feb 28, 2021 · 0 comments

Comments

@closertb
Copy link
Owner

closertb commented Feb 28, 2021

背景

最近一直在做一个技术改进:微前端中子应用采用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 id="load">加载</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的导入流程
image

总结

元宵也过完了,就以这一篇解(water)密(wen)开启我的2021 技术之旅吧。元宵节快乐,离5.1 还剩61天,坚持。。。

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