使用qiankun
也有一段時(shí)間了球匕,但是在使用的過(guò)程中纹磺,總會(huì)遇到各種問(wèn)題,面對(duì)這些問(wèn)題最終雖然解決了亮曹,但是總感覺(jué)心里面不踏實(shí)橄杨,想要去看看qiankun
的源碼,加深一些理解乾忱,學(xué)習(xí)一下qiankun
內(nèi)部的實(shí)現(xiàn)原理讥珍。斷斷續(xù)續(xù)讀了好幾周,主要的目的是為了弄清楚qiankun
窄瘟、主應(yīng)用衷佃、子應(yīng)用、single-spa
這四個(gè)部分的生命周期的執(zhí)行順序以及觸發(fā)的條件蹄葱。
生命周期
這里給出qiankun
氏义、主應(yīng)用锄列、子應(yīng)用、single-spa
的生命周期的總圖惯悠。
主應(yīng)用和子應(yīng)用的生命周期
在主應(yīng)用中注冊(cè)子應(yīng)用的時(shí)候邻邮,可以傳入五個(gè)生命周期函數(shù),如下
-
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)
});
依照子應(yīng)用的接入規(guī)范克婶,子應(yīng)用必須暴露三個(gè)生命周期筒严,如下
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
這三個(gè)生命周期添加和刪除全局變量鸭蛙。
//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
、主應(yīng)用筋岛、子應(yīng)用分別可以使用的生命周期函數(shù)娶视,但是這里還無(wú)法知道這些生命周期函數(shù)是何時(shí)何地執(zhí)行,有誰(shuí)進(jìn)行調(diào)用睁宰,這是下面章節(jié)的主要內(nèi)容肪获,核心就是single-spa
。
生命周期的執(zhí)行機(jī)制
首先需要明確的是qiankun
實(shí)際上是對(duì)single-spa
的進(jìn)一步封裝柒傻,使其更加容易開(kāi)箱即用孝赫。
qiankun
我們關(guān)注qiankun
暴露的registerMicroApps
方法,
//qiankun-master\src\apis.ts
export function registerMicroApps<T extends object = {}>(
apps: Array<RegistrableApp<T>>,
lifeCycles?: FrameworkLifeCycles<T>,
) {
//過(guò)濾出還沒(méi)有注冊(cè)的應(yīng)用
const unregisteredApps = apps.filter(app => !microApps.some(registeredApp => registeredApp.name === app.name));
microApps = [...microApps, ...unregisteredApps];
//對(duì)于每一個(gè)沒(méi)有注冊(cè)的應(yīng)用調(diào)用qiankun的API進(jìn)行注冊(cè)
unregisteredApps.forEach(app => {
const { name, activeRule, loader = noop, props, ...appConfig } = app;
registerApplication({
name,
app: async () => {
loader(true);
//這就是為了控制整個(gè)流程诅愚,可以看到frameworkStartedDefer.promise在start方法的最后才會(huì) resolve寒锚,所以在start調(diào)用后,這邊才會(huì)繼續(xù)執(zhí)行
await frameworkStartedDefer.promise;
//這里需要關(guān)注的是loadApp這個(gè)方法违孝,該方法主要就是完成qiankun與主應(yīng)用和子應(yīng)用的生命周期的整合刹前, 最終返回整理后的生命周期數(shù)組,并且是按照?qǐng)?zhí)行順序排列的
const { mount, ...otherMicroAppConfigs } = await loadApp(
{ name, props, ...appConfig },
frameworkConfiguration, //這個(gè)參數(shù)在start函數(shù)中被賦值
lifeCycles,
);
return {
mount: [async () => loader(true), ...toArray(mount), async () => loader(false)],
...otherMicroAppConfigs,
};
},
activeWhen: activeRule,
customProps: props,
});
});
}
由于loadApp
這個(gè)方法代碼過(guò)長(zhǎng)雌桑,這邊就不完全貼出來(lái)喇喉,主要的執(zhí)行過(guò)程用一個(gè)流程圖給出來(lái),
詳細(xì)的代碼大家可以自已前去閱讀校坑,這里簡(jiǎn)單給出函數(shù)的返回值拣技,其中有部分代碼省略,只關(guān)注主要的執(zhí)行代碼耍目,
//qiankun-master\src\loader.ts
const parcelConfig: ParcelConfigObject = {
name: appInstanceId,
//子應(yīng)用暴露的生命周期
bootstrap,
//這里是一個(gè)數(shù)組膏斤,里面有qiankun執(zhí)行的一些邏輯
mount: [
//該函數(shù)是判斷是否為單例模式,如果是單例模式就必須等待前面的應(yīng)用先完成卸載操作邪驮。由于整個(gè)mount數(shù)組后面再執(zhí)行的時(shí)候都是阻塞式的莫辨,所以這里如果返回的是一個(gè)promise,那么后續(xù)的函數(shù)只有等待該函數(shù)resolve之后才能繼續(xù)執(zhí)行。
async () => {
if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
return prevAppUnmountedDeferred.promise;
}
return undefined;
},
// 該函數(shù)確保每次應(yīng)用加載前容器 dom 結(jié)構(gòu)已經(jīng)設(shè)置完畢
async () => {
element = element || createElement(appContent, strictStyleIsolation);
render({ element, loading: true }, 'mounting');
},
//該函數(shù)是執(zhí)行beforeMount生命周期數(shù)組
async () => execHooksChain(toArray(beforeMount), app, global),
//該函數(shù)是掛載沙盒
mountSandbox,
//該函數(shù)是執(zhí)行子應(yīng)用暴露的mount生命周期函數(shù)
async props => mount({ ...props, container: containerGetter(), setGlobalState, onGlobalStateChange }),
//該函數(shù)是執(zhí)行afterMount生命周期數(shù)組
async () => execHooksChain(toArray(afterMount), app, global),
//該函數(shù)是檢測(cè)若是單例模式沮榜,那么就需要給prevAppUnmountedDeferred賦予一個(gè)promise盘榨,該promise會(huì)在unmount的時(shí)候resolve,并且在一些需要判斷單例模式下的一些執(zhí)行時(shí)需要使用該變量
async () => {
if (await validateSingularMode(singular, app)) {
prevAppUnmountedDeferred = new Deferred<void>();
}
}
],
unmount: [
//該函數(shù)是執(zhí)行beforeUnmount生命周期數(shù)組
async () => execHooksChain(toArray(beforeUnmount), app, global),
//該函數(shù)是執(zhí)行子應(yīng)用暴露的unmount生命周期函數(shù)
async props => unmount生命周期函數(shù)({ ...props, container: containerGetter() }),
//該函數(shù)是卸載沙盒
unmountSandbox,
//該函數(shù)是執(zhí)行afterUnmount生命周期數(shù)組
async () => execHooksChain(toArray(afterUnmount), app, global),
//該函數(shù)是卸載子應(yīng)用的dom并且去除全局狀態(tài)監(jiān)聽(tīng)
async () => {
render({ element: null, loading: false }, 'unmounted');
offGlobalStateChange(appInstanceId);
// for gc蟆融,為了垃圾回收草巡,引用計(jì)數(shù)為零可以回收
element = null;
},
//該函數(shù)是將prevAppUnmountedDeferred進(jìn)行resolve,防止單例模式下影響其他流程的正常運(yùn)轉(zhuǎn)
async () => {
if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
prevAppUnmountedDeferred.resolve();
}
},
],
};
single-spa
qiankun
在調(diào)用single-spa
的registerApplication
方法后型酥,就是將自己以及主應(yīng)用山憨、子應(yīng)用的相關(guān)生命周期托管給了single-spa
來(lái)進(jìn)行觸發(fā)。
我們看single-spa
的registerApplication
方法冕末,
//single-spa-master\src\applications\apps.js
export function registerApplication(
appNameOrConfig,
appOrLoadApp,
activeWhen,
customProps
) {
//這里做一些參數(shù)化校驗(yàn)以及規(guī)整
const registration = sanitizeArguments(
appNameOrConfig,
appOrLoadApp,
activeWhen,
customProps
);
//檢查該app是否已經(jīng)注冊(cè)過(guò)了
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維護(hù)的變量萍歉,用來(lái)存儲(chǔ)所有的應(yīng)用
apps.push(
assign(
{
loadErrorTime: null,
status: NOT_LOADED,
parcels: {},
devtools: {
overlays: {
options: {},
selectors: [],
},
},
},
registration
)
);
if (isInBrowser) {
ensureJQuerySupport();
//這個(gè)方法是single-spa的核心方法,類(lèi)似一個(gè)狀態(tài)機(jī)档桃,完成應(yīng)用不同狀態(tài)的流轉(zhuǎn)
reroute();
}
}
到這里,基本上所有應(yīng)用都已全部托管給single-spa憔晒,而single-spa后續(xù)對(duì)于每一個(gè)應(yīng)用的處理藻肄,狀態(tài)的變化,事件的觸發(fā)都基本通過(guò)reroute
這個(gè)方法完成拒担,所以這個(gè)方法也是我們關(guān)注的一個(gè)重點(diǎn)嘹屯。
single-spa的核心方法reroute()
首先需要從整體上看,reroute()這個(gè)方法基本是有三個(gè)地方會(huì)調(diào)用从撼,
這三個(gè)調(diào)用點(diǎn)可以大致分為顯示調(diào)用(微應(yīng)用注冊(cè)州弟、start),隱式調(diào)用(路有變化)低零。有路由的變化是隨時(shí)可能發(fā)生婆翔,更多的隨機(jī)性,更多的情況需要考慮掏婶,所以先梳理當(dāng)路由變化時(shí)啃奴,reroute方法是如何被觸發(fā)的。
路由變化觸發(fā)reroute
這里的代碼邏輯比較細(xì)碎雄妥,并且比較長(zhǎng)最蕾,有興趣的可以去下載源碼找到代碼位置為,
single-spa-master\src\navigation\navigation-events.js
自行閱讀一下老厌。
我這邊閱讀之后畫(huà)了一個(gè)基礎(chǔ)的流程圖瘟则,大概的說(shuō)明single-spa對(duì)于路由變化的處理方式,
這里面主要需要注意的就是single-spa是通過(guò)監(jiān)聽(tīng)hashchange 和popstate 兩個(gè)事件來(lái)判斷路由發(fā)生變化枝秤,但是由于window.history.pushState 和window.history.replaceState 并沒(méi)有觸發(fā)popstate 事件醋拧,所以碎玉這兩個(gè)函數(shù)需要加強(qiáng),
//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 方法中完成兩個(gè)任務(wù)
(1)正確完成URl的變化
(2)調(diào)用reroute方法,并且new一個(gè)popstate 事件傳遞過(guò)去趁仙,這么做的主要目的是能夠正確的執(zhí)行用戶的事件監(jiān)聽(tīng)洪添。
這里single-spa之所以能夠拿到用戶的監(jiān)聽(tīng)回調(diào),是因?yàn)槠渲貙?xiě)了window.addEventListener 雀费,并在其中判斷是否是監(jiān)聽(tīng)的hashchange 和popstate 這兩個(gè)事件干奢,如果是,則會(huì)緩沖到一個(gè)回調(diào)函數(shù)的數(shù)組之中盏袄。
//single-spa-master\src\navigation\navigation-events.js
const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
window.addEventListener = function (eventName, fn) {
// 如果用戶監(jiān)聽(tīng)的是 hashchange 和 popstate 事件忿峻,并且這個(gè)監(jiān)聽(tīng)器此前未加入事件監(jiān)聽(tīng)列表
// 那這個(gè)事件是有可能引發(fā)應(yīng)用變更的,需要加入 capturedEventListeners 中
// 直接 return 掉辕羽,說(shuō)明 hashchange 和 popstate 事件并沒(méi)有馬上執(zhí)行
// 而是在執(zhí)行完 reroute 邏輯之后在執(zhí)行
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);
};
弄清楚路由變化的時(shí)候逛尚,single-spa的主要操作之后,下面就是要摸清楚reroute函數(shù)具體是做了哪些操作刁愿。
執(zhí)行reroute(pendingPromises = [], eventArguments)
在這個(gè)方法中最主干的邏輯是绰寞,
//single-spa-master\src\navigation\reroute.js
export function reroute(pendingPromises = [], eventArguments) {
//如果正在執(zhí)行上一個(gè)路由變化的操作,則將該事件緩存到peopleWaitingOnAppChange數(shù)組中铣口,該事件大概率是用 戶的監(jiān)聽(tīng)事件滤钱,被single-spa劫持了。
if (appChangeUnderway) {
return new Promise((resolve, reject) => {
peopleWaitingOnAppChange.push({
resolve,
reject,
eventArguments,
});
});
}
//依據(jù)當(dāng)前變化后的URl判斷目前所有應(yīng)用的狀態(tài)即將發(fā)生的變化
const {
appsToUnload,
appsToUnmount,
appsToLoad,
appsToMount,
} = getAppChanges();
let appsThatChanged;
//判斷是否已經(jīng)執(zhí)行start函數(shù)脑题,如果是件缸,則進(jìn)一步執(zhí)行每一個(gè)app的生命周期,如果沒(méi)有叔遂,則只load應(yīng)用他炊。
if (isStarted()) {
appChangeUnderway = true;
appsThatChanged = appsToUnload.concat(
appsToLoad,
appsToUnmount,
appsToMount
);
return performAppChanges();
} else {
appsThatChanged = appsToLoad;
return loadApps();
}
}
依據(jù)上面的主要執(zhí)行過(guò)程,畫(huà)了一個(gè)基本的流程圖已艰,大概說(shuō)明了reroute方法對(duì)于應(yīng)用狀態(tài)的變化以及single-spa提供的各個(gè)公共事件的派發(fā)機(jī)制痊末。
single-spa在執(zhí)行過(guò)程中派發(fā)的幾個(gè)全局事件,應(yīng)用可以進(jìn)行監(jiān)聽(tīng)旗芬,這其實(shí)也算是single-spa的一個(gè)生命周期舌胶,主要的執(zhí)行過(guò)程為,
每一個(gè)應(yīng)用自身的狀態(tài)都會(huì)隨著URl的不斷改變而變化疮丛,基本由下面的幾個(gè)階段組成幔嫂,
小結(jié)
源碼還有很多的細(xì)節(jié)可以去看,我這邊只是大致梳理了一下qiankun生命周期的執(zhí)行過(guò)程誊薄,基本就是將生命周期的邏輯封裝好履恩,托管給single-spa去執(zhí)行。
reroute 流程作為 single-spa 的核心流程呢蔫,充當(dāng)了一個(gè)應(yīng)用狀態(tài)機(jī)的角色切心,控制了應(yīng)用的生命周期的流轉(zhuǎn)和事件分發(fā)飒筑。qiankun 就是利用了這一特性,將應(yīng)用交給 single-spa 管理绽昏,自己實(shí)現(xiàn)應(yīng)用的加載方法(loadApp)和生命周期
參考
萬(wàn)字長(zhǎng)文+圖文并茂+全面解析微前端框架 qiankun 源碼 - qiankun 篇
還有一些就不全羅列出來(lái)了协屡。。眼睛酸全谤,多休息肤晓。歡迎大家多多吐槽。