最近一直在做一个技术改进:微前端中子应用采用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这个模块化方案也是火了一把,以前我是不知道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中移除,看起让人不明觉厉。
为什么要做这种骚操作(卸磨杀驴)?留在那貌似也没有什么问题。
这种操作也不是不可以,因为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注入,原理还是比较易懂。但引入这个扩展前,还是有一些坑,我踩过:
global.System.constructor.prototype
;externals
,但因为amd扩展的引入,这些global依赖就变成了SystemJs导入,应用会加载失效,所以有一种投机的加载方式就是: 待其他js script导入完成后,再执行extras/amd
;以上只是SystemJs 浏览器相关的一些比较核心的流程,很多细节性的处理我也没深究,应该差不了多少。
元宵也过完了,就以这一篇解(water)密(wen)开启我的2021 技术之旅吧。元宵节快乐,离5.1 还剩61天,坚持。。。