背景
我们都知道 qiankun 是基于 single-spa 开发的,今天我们来扒一扒 single-spa 到底做了哪些事。
微前端架构
一般来说,微前端需要解决的问题分为两大类:
- 应用的加载与切换:路由问题、应用入口、应用加载
- 应用的隔离与通信:js隔离、css样式隔离、应用间通信
single-spa 则很好地解决了 路由问题、应用入口 两个问题,但并没有解决应用加载问题,而是将该问题暴露出来由使用者实现(一般可以用 system.js 或原生 script 标签来实现);qiankun在此基础上封装了一个应用加载方案,即 import-html-entry (可以看我写的这篇 微前端:qiankun的依赖import-html-entry的作用),并给出了js隔离、css样式隔离和应用间通信三个问题的解决方案,同时提供了预加载功能。
原理
上面我们知道了,single-spa 则很好地解决了 路由问题、应用入口 两个问题,那么接下来我们就详细的看看其中的原理。
路由问题
single-spa是通过监听hashChange和popState这两个原生事件来检测路由变化的,它会根据路由的变化来加载对应的应用,相关的代码可以在single-spa的 src/navigation/navigation-events.js 中找到:
...
// 139行
if (isInBrowser) {
// We will trigger an app change for any routing events.
window.addEventListener("hashchange", urlReroute);
window.addEventListener("popstate", urlReroute);
...
// 174行,劫持pushState和replaceState
window.history.pushState = patchedUpdateState(
window.history.pushState,
"pushState"
);
window.history.replaceState = patchedUpdateState(
window.history.replaceState,
"replaceState"
);
function urlReroute() {
reroute([], arguments);
}
export function reroute(pendingPromises = [], eventArguments) {
...
// getAppChanges会根据路由改变应用的状态,状态包含4类
// 待清除、待卸载、待加载、待挂载
const {
appsToUnload,
appsToUnmount,
appsToLoad,
appsToMount,
} = getAppChanges();
...
// 如果应用已启动,则调用performAppChanges加载和挂载应用
// 否则,只加载未加载的应用
if (isStarted()) {
appChangeUnderway = true;
appsThatChanged = appsToUnload.concat(
appsToLoad,
appsToUnmount,
appsToMount
);
return performAppChanges();
} else {
appsThatChanged = appsToLoad;
return loadApps();
}
...
function performAppChanges() {
return Promise.resolve().then(() => {
// 1. 派发应用更新前的自定义事件
// 2. 执行应用暴露出的生命周期函数
// appsToUnload -> unload生命周期钩子
// appsToLoad -> 执行加载方法
// appsToUnmount -> 卸载应用,并执行对应生命周期钩子
// appsToMount -> 尝试引导和挂载应用
})
}
...
}
export function start(opts) {
started = true;
if (opts && opts.urlRerouteOnly) {
setUrlRerouteOnly(opts.urlRerouteOnly);
}
if (isInBrowser) {
reroute();
}
}
- 根据传入的参数activeWhen判断哪个应用需要加载,哪个应用需要卸载或清除,并将其push到对应的数组
- 如果应用已经启动,则进行应用加载或切换。针对应用的不同状态,直接执行应用自身暴露出的生命周期钩子函数即可。
- 如果应用未启动,则只去下载appsToLoad中的应用。
总的来看,当路由发生变化时,hashChange或popState会触发,这时single-spa会监听到,并触发urlReroute;接着它会调用reroute,该函数正确设置各个应用的状态后,直接通过调用应用所暴露出的生命周期钩子函数即可。当某个应用被推送到appsToMount后,它的mount函数会被调用,该应用就会被挂载;而推送到appsToUnmount中的应用则会调用其unmount钩子进行卸载。
应用入口
single-spa采用的是协议入口,即只要实现了single-spa的入口协议规范,它就是可加载的应用。single-spa的规范要求应用入口必须暴露出以下三个生命周期钩子函数,且必须返回Promise,以保证single-spa可以注册回调函数:
- bootstrap
- mount
- unmount
bootstrap用于应用引导,基座应用会在子应用挂载前调用它。
mount用于应用挂载,就是一般应用中用于渲染的逻辑,即上述的new Vue语句。
unmount用于应用卸载,我们可以在这里调用实例的destroy方法手动卸载应用,或清除某些内存占用等。
let instance = null;
let router = null;
/**
* 渲染函数
* 两种情况:主应用生命周期钩子中运行 / 微应用单独启动时运行
*/
function render() {
// 在 render 中创建 VueRouter,可以保证在卸载微应用时,移除 location 事件监听,防止事件污染
router = new VueRouter({
// 运行在主应用中时,添加路由命名空间 /vue
base: window.__POWERED_BY_QIANKUN__ ? "/vue" : "/",
mode: "history",
routes,
});
// 挂载应用
instance = new Vue({
router,
render: (h) => h(App),
}).$mount("#app");
}
// 独立运行时,直接挂载应用
if (!window.__POWERED_BY_QIANKUN__) {
render()
}
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log("VueMicroApp bootstraped");
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
console.log("VueMicroApp mount", props);
render(props);
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount() {
console.log("VueMicroApp unmount");
instance.$destroy();
instance = null;
router = null;
}
应用加载
那么,我们传一下上面两部分内容,解决了路由问题、应用入口(什么样的应用可以被加载,没有说如何加载),应用是如何进行加载的。single-spa并没有提供自己的解决方案,而是将它开放出来,由开发者提供。
刚刚说了,qiankun 提供了 import-html-entry 来进行加载,那么脱离 qiankun 普通的方式是怎么样的,这里举例用 system.js 来加载
<script type="systemjs-importmap">
{
"imports": {
"app1": "http://localhost:8080/app1.js",
"app2": "http://localhost:8081/app2.js",
"single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js"
}
}
</script>
... // system.js的相关依赖文件
<script>
(function(){
// 加载single-spa
System.import('single-spa').then((res)=>{
var singleSpa = res;
// 注册子应用
singleSpa.registerApplication('app1',
() => System.import('app1'),
location => location.hash.startsWith(`#/app1`);
);
singleSpa.registerApplication('app2',
() => System.import('app2'),
location => location.hash.startsWith(`#/app2`);
);
// 启动single-spa
singleSpa.start();
})
})()
</script>
我们在调用singleSpa.registerApplication注册应用时提供的第二个参数就是加载这个子应用的方法。single-spa会调用这个函数,下载子应用代码并分别调用其bootstrap和mount方法进行引导和挂载。
总结
以上就是single-spa的核心原理,从上面的介绍中不难看出,single-spa只是负责把应用加载到一个页面中,至于应用能否协同工作,是很难保证的。而qiankun所要解决的,就是协同工作的问题。
参考资料
官网
https://zhuanlan.zhihu.com/p/378346507
https://blog.csdn.net/qq_41694291/article/details/113842872