前言
「乾坤」是蚂蚁开源的一款微前端框架,其友好的接入体验,为社区带来巨大价值。
在这友好的背后,动用了一些小魔法。在绝大多数场景里,这些魔法能为开发者,大幅降低接入成本。但在某些场景下,却会带来一些困惑与风险。
现象
在使用「乾坤」进行「子应用」接入时,考虑改造成本,通常我们选择页面方式接入。逻辑如下:
// @main.js
registerMicroApps([
{
name: 'sub app',
entry: '//localhost:8081/sub/index.html',
container: '#app',
activeRule: '/#/sub',
},
]);
「子应用」这里,为了方便演示,我选择「非 webpack 构建的微应用」。直接使用官网代码,大致如下:
// @sub/index.js
const render = (props) => {
props.container.querySelector('#app').innerHTML = 'sub project';
return Promise.resolve();
};
window['purehtml'] = {
bootstrap: () => {
return Promise.resolve();
},
mount: (props) => {
return render(props);
},
unmount: () => {
return Promise.resolve();
},
};
然后发现,已经可以跑起来了。一个字,爽。
但随后,大家会不会有个困惑,因为我们知道,无论是「构建类的子应用」还是「非构建类的子应用」本质都是通过 UMD 的方式导出,挂一个全局对象,被「基座应用」使用。
但在文档中,并没有强调过,这个全局对象的名称,在「基座应用」使用与「子应用」抛出时,要保持一致。
对于 name 的建议,只是「微应用的名称,微应用之间必须确保唯一」
就像上述实例中,「基座应用」使用的是 sub app, 而「子应用」使用的是 purehtml。
魔法
带着这个疑问,去查找相关源码。找到相关逻辑如下:
// @https://github.com/umijs/qiankun/blob/becb7ada7763e471b6f770f297b9d37ff270b818/src/loader.ts#L206C1-L239C1
function getLifecyclesFromExports(
...
) {
...
// fallback to sandbox latest set property if it had
if (globalLatestSetProp) {
const lifecycles = (<any>global)[globalLatestSetProp];
if (validateExportLifecycle(lifecycles)) {
return lifecycles;
}
}
...
// fallback to global variable who named with ${appName} while module exports not found
const globalVariableExports = (global as any)[appName];
if (validateExportLifecycle(globalVariableExports)) {
return globalVariableExports;
}
throw new QiankunError(`You need to export lifecycle functions in ${appName} entry`);
}
可见,在载入子模块时,它有一定的策略。
- 先检测,最后一次设置
window的属性(示例中的purehtml)。如果满足导出规则,则直接导出。 - 再检测,主应用指定的应用名称(示例中的
sub app)。如果满足导出规则,则直接导出。 - 都没找到,则报错
这样一看,大体明白了。由于第一条策略的存在,即使两边全局对象名称不一致,也可以加载成功。
看到这里,大家是否会有这样的担忧,如果因为某次不注意,在后置逻辑中,又有新的对于 window 的设置操作,这样不就有问题了吗?
再后置加上如下代码,测试下:
...
window['purehtml'] = {};
...
window['utils'] = {}
跑起来看下,果然报错了。
风险
那么在实际业务场景中,这类风险大吗?我们这里分「非构建类子应用」与「构建类子应用」来看。
非构建类子应用
概率
这类应用由于没有构建流程,抛出全局对象的时机是自身控制的,因此,后置有相关全局设置逻辑的概率较高。
子应用独立开发时是否容易发现
基于上述示例,如果 window.utils 被「子应用」依赖,且立即执行。那么,调试时就能很容易发现问题。
否则,也较难发现。
构建类子应用
概率
由于有构建流程制约,UMD 的赋值操作大多数都在最后,因此出问题概率较低。
但不排除在模版尾部新增脚本的可能。比如:
// @public/index.html
<html>
<head>
<script defer src="app.bundle.js"></script>
</head>
...
<body>
</body>
<script src="utils.js"></script>
</html>
由于 app 的脚本构建后,默认会插入在 head 的尾部,因此也会出现问题。
子应用独立开发时是否容易发现
独立开发时,即使 utils.js 被 app.bundle.js 依赖,且相关方法要被立即执行,也能表现正常。
因为构建时,引入 app.bundle.js ,会默认加上 defer 属性。因此,它一直会在最后执行。所以,在独立开发时,此类问题较难被察觉。
而「基座应用」接入拉取资源时,并不会识别 defer 属性,因此会出现问题。
尾言
该加载策略的设计,让接入体验有大幅提升,但也会在少量场景下,因其过于隐晦而造成困扰。
因此推荐在使用时,尽量保持「基座应用」与「子应用」的全局名称一致,可以较有效地避免该问题的出现。
好了,就写这么多了。如果文中有错误,欢迎指正,感谢阅读。
最后,还是感谢「乾坤」的开源,让大家收益。