背景
最近一直在做一个技术改进:微前端中子应用采用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");
}
};
});
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的模块注册,这时只会注册模块为一个函数,然后onload事件触发,就会调用getRegister获取这个模块,但这并不会立即执行,因为要检测模块是否还有依赖,如果有,就需要待依赖模块加载完后,再调用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导入详细过程:
总结
元宵也过完了,就以这一篇解(water)密(wen)开启我的2021 技术之旅吧。元宵节快乐,离5.1 还剩61天,坚持。。。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。