qiankun源码深挖
使用qiankun
也有一段时间了,但是在使用的过程中,总会遇到各种问题,面对这些问题最终虽然解决了,但是总感觉心里面不踏实,想要去看看qiankun
的源码,加深一些理解,学习一下qiankun
内部的实现原理。断断续续读了好几周,主要的目的是为了弄清楚qiankun
、主应用、子应用、single-spa
这四个部分的生命周期的执行顺序以及触发的条件。
生命周期
这里给出qiankun
、主应用、子应用、single-spa
的生命周期的总图。
主应用和子应用的生命周期
在主应用中注册子应用的时候,可以传入五个生命周期函数,如下
-
beforeLoad
-Lifecycle | Array<Lifecycle>
- 可选 -
beforeMount
-Lifecycle | Array<Lifecycle>
- 可选 -
afterMount
-Lifecycle | Array<Lifecycle>
- 可选 -
beforeUnmount
-Lifecycle | Array<Lifecycle>
- 可选 -
afterUnmount
-Lifecycle | Array<Lifecycle>
- 可选
registerMicroApps(apps, {
beforeLoad: app => console.log('before load', app.name),
beforeMount: [
app => console.log('before mount', app.name),
],
afterMount: app => console.log('after mount', app.name),
beforeUnmount: app => console.log('before unmount', app.name),
afterUnmount: app => console.log('after unmount', app.name)
});
依照子应用的接入规范,子应用必须暴露三个生命周期,如下
export async function bootstrap() {
console.log('app bootstraped');
}
export async function mount(props) {
console.log('app mount', props);
render(props);
}
export async function unmount() {
console.log('app unmount');
instance.$destroy();
instance = null;
router = null;
}
qiankun的生命周期
qiankun
主要是利用beforeLoad
、beforeMount
、beforeUnmount
这三个生命周期添加和删除全局变量。
//qiankun-master\src\addons\engineFlag.ts
export default function getAddOn(global: Window): FrameworkLifeCycles<any> {
return {
async beforeLoad() {
// eslint-disable-next-line no-param-reassign
global.__POWERED_BY_QIANKUN__ = true;
},
async beforeMount() {
// eslint-disable-next-line no-param-reassign
global.__POWERED_BY_QIANKUN__ = true;
},
async beforeUnmount() {
// eslint-disable-next-line no-param-reassign
delete global.__POWERED_BY_QIANKUN__;
},
};
}
//qiankun-master\src\addons\runtimePublicPath.ts
export default function getAddOn(global: Window, publicPath = '/'): FrameworkLifeCycles<any> {
let hasMountedOnce = false;
return {
async beforeLoad() {
// eslint-disable-next-line no-param-reassign
global.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ = publicPath;
},
async beforeMount() {
if (hasMountedOnce) {
// eslint-disable-next-line no-param-reassign
global.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ = publicPath;
}
},
async beforeUnmount() {
if (rawPublicPath === undefined) {
// eslint-disable-next-line no-param-reassign
delete global.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
} else {
// eslint-disable-next-line no-param-reassign
global.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ = rawPublicPath;
}
hasMountedOnce = true;
},
};
}
以上是qiankun
、主应用、子应用分别可以使用的生命周期函数,但是这里还无法知道这些生命周期函数是何时何地执行,有谁进行调用,这是下面章节的主要内容,核心就是single-spa
。
生命周期的执行机制
首先需要明确的是qiankun
实际上是对single-spa
的进一步封装,使其更加容易开箱即用。
qiankun
我们关注qiankun
暴露的registerMicroApps
方法,
//qiankun-master\src\apis.ts
export function registerMicroApps<T extends object = {}>(
apps: Array<RegistrableApp<T>>,
lifeCycles?: FrameworkLifeCycles<T>,
) {
//过滤出还没有注册的应用
const unregisteredApps = apps.filter(app => !microApps.some(registeredApp => registeredApp.name === app.name));
microApps = [...microApps, ...unregisteredApps];
//对于每一个没有注册的应用调用qiankun的API进行注册
unregisteredApps.forEach(app => {
const { name, activeRule, loader = noop, props, ...appConfig } = app;
registerApplication({
name,
app: async () => {
loader(true);
//这就是为了控制整个流程,可以看到frameworkStartedDefer.promise在start方法的最后才会 resolve,所以在start调用后,这边才会继续执行
await frameworkStartedDefer.promise;
//这里需要关注的是loadApp这个方法,该方法主要就是完成qiankun与主应用和子应用的生命周期的整合, 最终返回整理后的生命周期数组,并且是按照执行顺序排列的
const { mount, ...otherMicroAppConfigs } = await loadApp(
{ name, props, ...appConfig },
frameworkConfiguration, //这个参数在start函数中被赋值
lifeCycles,
);
return {
mount: [async () => loader(true), ...toArray(mount), async () => loader(false)],
...otherMicroAppConfigs,
};
},
activeWhen: activeRule,
customProps: props,
});
});
}
由于loadApp
这个方法代码过长,这边就不完全贴出来,主要的执行过程用一个流程图给出来,
详细的代码大家可以自已前去阅读,这里简单给出函数的返回值,其中有部分代码省略,只关注主要的执行代码,
//qiankun-master\src\loader.ts
const parcelConfig: ParcelConfigObject = {
name: appInstanceId,
//子应用暴露的生命周期
bootstrap,
//这里是一个数组,里面有qiankun执行的一些逻辑
mount: [
//该函数是判断是否为单例模式,如果是单例模式就必须等待前面的应用先完成卸载操作。由于整个mount数组后面再执行的时候都是阻塞式的,所以这里如果返回的是一个promise,那么后续的函数只有等待该函数resolve之后才能继续执行。
async () => {
if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
return prevAppUnmountedDeferred.promise;
}
return undefined;
},
// 该函数确保每次应用加载前容器 dom 结构已经设置完毕
async () => {
element = element || createElement(appContent, strictStyleIsolation);
render({ element, loading: true }, 'mounting');
},
//该函数是执行beforeMount生命周期数组
async () => execHooksChain(toArray(beforeMount), app, global),
//该函数是挂载沙盒
mountSandbox,
//该函数是执行子应用暴露的mount生命周期函数
async props => mount({ ...props, container: containerGetter(), setGlobalState, onGlobalStateChange }),
//该函数是执行afterMount生命周期数组
async () => execHooksChain(toArray(afterMount), app, global),
//该函数是检测若是单例模式,那么就需要给prevAppUnmountedDeferred赋予一个promise,该promise会在unmount的时候resolve,并且在一些需要判断单例模式下的一些执行时需要使用该变量
async () => {
if (await validateSingularMode(singular, app)) {
prevAppUnmountedDeferred = new Deferred<void>();
}
}
],
unmount: [
//该函数是执行beforeUnmount生命周期数组
async () => execHooksChain(toArray(beforeUnmount), app, global),
//该函数是执行子应用暴露的unmount生命周期函数
async props => unmount生命周期函数({ ...props, container: containerGetter() }),
//该函数是卸载沙盒
unmountSandbox,
//该函数是执行afterUnmount生命周期数组
async () => execHooksChain(toArray(afterUnmount), app, global),
//该函数是卸载子应用的dom并且去除全局状态监听
async () => {
render({ element: null, loading: false }, 'unmounted');
offGlobalStateChange(appInstanceId);
// for gc,为了垃圾回收,引用计数为零可以回收
element = null;
},
//该函数是将prevAppUnmountedDeferred进行resolve,防止单例模式下影响其他流程的正常运转
async () => {
if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
prevAppUnmountedDeferred.resolve();
}
},
],
};
single-spa
qiankun
在调用single-spa
的registerApplication
方法后,就是将自己以及主应用、子应用的相关生命周期托管给了single-spa
来进行触发。
我们看single-spa
的registerApplication
方法,
//single-spa-master\src\applications\apps.js
export function registerApplication(
appNameOrConfig,
appOrLoadApp,
activeWhen,
customProps
) {
//这里做一些参数化校验以及规整
const registration = sanitizeArguments(
appNameOrConfig,
appOrLoadApp,
activeWhen,
customProps
);
//检查该app是否已经注册过了
if (getAppNames().indexOf(registration.name) !== -1)
throw Error(
formatErrorMessage(
21,
__DEV__ &&
`There is already an app registered with name ${registration.name}`,
registration.name
)
);
//这里的apps是single-spa维护的变量,用来存储所有的应用
apps.push(
assign(
{
loadErrorTime: null,
status: NOT_LOADED,
parcels: {},
devtools: {
overlays: {
options: {},
selectors: [],
},
},
},
registration
)
);
if (isInBrowser) {
ensureJQuerySupport();
//这个方法是single-spa的核心方法,类似一个状态机,完成应用不同状态的流转
reroute();
}
}
到这里,基本上所有应用都已全部托管给single-spa,而single-spa后续对于每一个应用的处理,状态的变化,事件的触发都基本通过reroute
这个方法完成,所以这个方法也是我们关注的一个重点。
single-spa的核心方法reroute()
首先需要从整体上看,reroute()这个方法基本是有三个地方会调用,
reroute的调用点.jpg这三个调用点可以大致分为显示调用(微应用注册、start),隐式调用(路有变化)。有路由的变化是随时可能发生,更多的随机性,更多的情况需要考虑,所以先梳理当路由变化时,reroute方法是如何被触发的。
路由变化触发reroute
这里的代码逻辑比较细碎,并且比较长,有兴趣的可以去下载源码找到代码位置为,
single-spa-master\src\navigation\navigation-events.js
自行阅读一下。
我这边阅读之后画了一个基础的流程图,大概的说明single-spa对于路由变化的处理方式,
路由变化时触发reroute.jpg这里面主要需要注意的就是single-spa是通过监听hashchange 和popstate 两个事件来判断路由发生变化,但是由于window.history.pushState 和window.history.replaceState 并没有触发popstate 事件,所以碎玉这两个函数需要加强,
//single-spa-master\src\navigation\navigation-events.js
window.history.pushState = patchedUpdateState(
window.history.pushState,
"pushState"
);
window.history.replaceState = patchedUpdateState(
window.history.replaceState,
"replaceState"
);
在patchedUpdateState 方法中完成两个任务
(1)正确完成URl的变化
(2)调用reroute方法,并且new一个popstate 事件传递过去,这么做的主要目的是能够正确的执行用户的事件监听。
这里single-spa之所以能够拿到用户的监听回调,是因为其重写了window.addEventListener ,并在其中判断是否是监听的hashchange 和popstate 这两个事件,如果是,则会缓冲到一个回调函数的数组之中。
//single-spa-master\src\navigation\navigation-events.js
const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
window.addEventListener = function (eventName, fn) {
// 如果用户监听的是 hashchange 和 popstate 事件,并且这个监听器此前未加入事件监听列表
// 那这个事件是有可能引发应用变更的,需要加入 capturedEventListeners 中
// 直接 return 掉,说明 hashchange 和 popstate 事件并没有马上执行
// 而是在执行完 reroute 逻辑之后在执行
if (typeof fn === "function") {
if (
routingEventsListeningTo.indexOf(eventName) >= 0 &&
!find(capturedEventListeners[eventName], (listener) => listener === fn)
) {
capturedEventListeners[eventName].push(fn);
return;
}
}
return originalAddEventListener.apply(this, arguments);
};
弄清楚路由变化的时候,single-spa的主要操作之后,下面就是要摸清楚reroute函数具体是做了哪些操作。
执行reroute(pendingPromises = [], eventArguments)
在这个方法中最主干的逻辑是,
//single-spa-master\src\navigation\reroute.js
export function reroute(pendingPromises = [], eventArguments) {
//如果正在执行上一个路由变化的操作,则将该事件缓存到peopleWaitingOnAppChange数组中,该事件大概率是用 户的监听事件,被single-spa劫持了。
if (appChangeUnderway) {
return new Promise((resolve, reject) => {
peopleWaitingOnAppChange.push({
resolve,
reject,
eventArguments,
});
});
}
//依据当前变化后的URl判断目前所有应用的状态即将发生的变化
const {
appsToUnload,
appsToUnmount,
appsToLoad,
appsToMount,
} = getAppChanges();
let appsThatChanged;
//判断是否已经执行start函数,如果是,则进一步执行每一个app的生命周期,如果没有,则只load应用。
if (isStarted()) {
appChangeUnderway = true;
appsThatChanged = appsToUnload.concat(
appsToLoad,
appsToUnmount,
appsToMount
);
return performAppChanges();
} else {
appsThatChanged = appsToLoad;
return loadApps();
}
}
依据上面的主要执行过程,画了一个基本的流程图,大概说明了reroute方法对于应用状态的变化以及single-spa提供的各个公共事件的派发机制。
reroute的执行过程.jpgsingle-spa在执行过程中派发的几个全局事件,应用可以进行监听,这其实也算是single-spa的一个生命周期,主要的执行过程为,
single-spa派发的事件.jpg每一个应用自身的状态都会随着URl的不断改变而变化,基本由下面的几个阶段组成,
应用状态的几个阶段.jpg小结
源码还有很多的细节可以去看,我这边只是大致梳理了一下qiankun生命周期的执行过程,基本就是将生命周期的逻辑封装好,托管给single-spa去执行。
reroute 流程作为 single-spa 的核心流程,充当了一个应用状态机的角色,控制了应用的生命周期的流转和事件分发。qiankun 就是利用了这一特性,将应用交给 single-spa 管理,自己实现应用的加载方法(loadApp)和生命周期
参考
万字长文+图文并茂+全面解析微前端框架 qiankun 源码 - qiankun 篇
还有一些就不全罗列出来了。。眼睛酸,多休息。欢迎大家多多吐槽。