差点被 SystemJs 惊掉了下巴 解密模块加载黑魔法
最近一直在做一个技术改进:微前端中子应用采用 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"); } }; });
Demo 我稍微改了一下,把 triton.js 从主动动态加载,改成点击按钮后再动态加载,只是为了加载过程更明显。点击按钮后,界面和元素长下面这样:
发现没? triton.js
没有被加载到 html 中,但这个 JS 的内容确实是已经执行了,洋气不洋气, 惊不惊喜?!!!难道 script 真的可以不加入到 html 就能执行?
但再仔细搜索,发现是有 script 请求下载记录的:
黑魔法解密
如果你想要快速知道答案,你可以在 network 直接点击 script 加载的触发节点:
顺着点开,你会发现黑魔法不过是一个戏法:
先把 script 加载到 html 中,加载完成后,再将这个 script 从 html 中移除,看起让人不明觉厉。
浅入 SystemJs
为什么要做这种骚操作(卸磨杀驴)?留在那貌似也没有什么问题。
这种操作也不是不可以,因为 script 标签加载完成就会马上执行,除非加上了 defer
标识,或者采用了 preload 或者 prefetch 标签来预加载。一旦 script 标签中的内容被执行,其有用或者需要再次被调用的部分,就会以引用的方式存在内存中,这时 script 中的内容确实就是个摆设,重绘重排都没用,只有重新加载才会触发执行。
简单了解一下 SystemJs 的原理:
当我们引入 <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 模块以外,还支持 amd
和 umd
模块,但其依赖扩展 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 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论