qiankun对比single-spa
qiankun基于single-spa,新增了很多特性。
- 预先加载的功能,利用空闲时间加载其他应用,使用import-html-entry包
- 沙箱功能(创建一个sandbox,让脚本执行在sandbox里面),css沙箱(影子dom,scopedcss(加前缀)))
- 获取导出的接入协议(在沙箱中执行的),进行扩展(比如设置全局变量。)
源码浅读:
// index.ts
export { loadMicroApp, registerMicroApps, start } from './apis';
export { initGlobalState } from './globalState';
export { getCurrentRunningApp as __internalGetCurrentRunningApp } from './sandbox';
export * from './errorHandler';
export * from './effects';
export * from './interfaces';
export { prefetchImmediately as prefetchApps } from './prefetch';
qiankun提供的registerMicroApps,注册应用的方法,实际上也是基于singe-spa的registerApplication方法
export function registerMicroApps<T extends ObjectType>(
apps: Array<RegistrableApp<T>>, // 本次要注册的应用
lifeCycles?: FrameworkLifeCycles<T>, //自己编写的生命周期
) {
// Each app only needs to be registered once
// 拿到没有注册过的应用,name属性用来区分不同应用
const unregisteredApps = apps.filter((app) => !microApps.some((registeredApp) => registeredApp.name === app.name));
// 保存全部应用
microApps = [...microApps, ...unregisteredApps];
// 循环注册未注册的应用。
unregisteredApps.forEach((app) => {
const { name, activeRule, loader = noop, props, ...appConfig } = app;
// single-spa的注册应用方法。(路由劫持)
registerApplication({
name,
app: async () => {
loader(true);
// 等待调用start方法后才会执行
await frameworkStartedDefer.promise;
const { mount, ...otherMicroAppConfigs } = (
// (loadApp())()
await loadApp({ name, props, ...appConfig }, frameworkConfiguration, lifeCycles)
)();
return {
// 返回接入协议
mount: [async () => loader(true), ...toArray(mount), async () => loader(false)],
...otherMicroAppConfigs,
};
},
activeWhen: activeRule,
customProps: props,
});
});
}
如上(single-spa路由拦截,匹配acriveRule,执行app方法)。其次看start方法
/**
* qiankun对比single-spa。多了预加载,和沙箱功能
*/
export function start(opts: FrameworkConfiguration = {}) {
frameworkConfiguration = { prefetch: true, singular: true, sandbox: true, ...opts }; //默认参数,预加载,单例,沙盒
const { prefetch, urlRerouteOnly = defaultUrlRerouteOnly, ...importEntryOpts } = frameworkConfiguration;
if (prefetch) {
// 预拉取
doPrefetchStrategy(microApps, prefetch, importEntryOpts);
}
// 对沙箱做降级处理 有的沙箱不支持proxy
frameworkConfiguration = autoDowngradeForLowVersionBrowser(frameworkConfiguration);
// single-spa开启应用
startSingleSpa({ urlRerouteOnly });
started = true;
frameworkStartedDefer.resolve();
}
start方法中有一个预加载的功能,就是利用浏览器空闲时间去请求其他子应用的资源。doPrefetchStartegy最终调用
function prefetchAfterFirstMounted(apps: AppMetadata[], opts?: ImportEntryOpts): void {
// 第一个应用mount之后触发
window.addEventListener('single-spa:first-mount', function listener() {
const notLoadedApps = apps.filter((app) => getAppStatus(app.name) === NOT_LOADED);
if (process.env.NODE_ENV === 'development') {
const mountedApps = getMountedApps();
console.log(`[qiankun] prefetch starting after ${mountedApps} mounted...`, notLoadedApps);
}
notLoadedApps.forEach(({ entry }) => prefetch(entry, opts));
window.removeEventListener('single-spa:first-mount', listener);
});
}
function prefetch(entry: Entry, opts?: ImportEntryOpts): void {
if (!navigator.onLine || isSlowNetwork) {
// Don't prefetch if in a slow network or offline
return;
}
requestIdleCallback(async () => {
// import-html-entry包,获取url的脚本和样式等数据,加载html,注释掉css和js
const { getExternalScripts, getExternalStyleSheets } = await importEntry(entry, opts);
requestIdleCallback(getExternalStyleSheets);
requestIdleCallback(getExternalScripts);
});
}
该方法主要在监听第一个应用mouint之后,调用prefetch方法去预拉资源。而prefetch方法主要是利用requestIdLeCallback和import-html-entry包去获取资源。
最后start方法调取single-spa的
// single-spa开启应用
startSingleSpa({ urlRerouteOnly });
startSingleSpa方法,开启应用。
接着就是路由匹配的时候加载子应用。主要看
// single-spa的注册应用方法。(路由劫持)
registerApplication({
name,
app: async () => {
loader(true);
// 等待调用start方法后才会执行
await frameworkStartedDefer.promise;
const { mount, ...otherMicroAppConfigs } = (
// (loadApp())()
await loadApp({ name, props, ...appConfig }, frameworkConfiguration, lifeCycles)
)();
return {
// 返回接入协议
mount: [async () => loader(true), ...toArray(mount), async () => loader(false)],
...otherMicroAppConfigs,
};
},
activeWhen: activeRule,
customProps: props,
});
loadApp方法
先看整个方法
export async function loadApp<T extends ObjectType>(
app: LoadableApp<T>,
configuration: FrameworkConfiguration = {},
lifeCycles?: FrameworkLifeCycles<T>,
): Promise<ParcelConfigObjectGetter> {
const { entry, name: appName } = app;
const appInstanceId = genAppInstanceIdByName(appName); //给当前加载的应用起一个名字
const markName = `[qiankun] App ${appInstanceId} Loading`;
if (process.env.NODE_ENV === 'development') {
performanceMark(markName);
}
const {
singular = false,
sandbox = true,
excludeAssetFilter,
globalContext = window,
...importEntryOpts
} = configuration;
// get the entry html content and script executor 使用import-html-entry拉资源,获取html文件和脚本的执行器,html的脚本会注视掉,然后转成execScripts,然后放在沙箱执行
const { template, execScripts, assetPublicPath, getExternalScripts } = await importEntry(entry, importEntryOpts);
// trigger external scripts loading to make sure all assets are ready before execScripts calling
await getExternalScripts(); //执行额外的脚本,确保真正的脚本执行之前加载完
// as single-spa load and bootstrap new app parallel with other apps unmounting
// (see https://github.com/CanopyTax/single-spa/blob/master/src/navigation/reroute.js#L74)
// we need wait to load the app until all apps are finishing unmount in singular mode
if (await validateSingularMode(singular, app)) {
//单例模式下,要保证之前的应用被卸载
await (prevAppUnmountedDeferred && prevAppUnmountedDeferred.promise);
}
// 获取文件内容,将html转为qiankun特定的html
const appContent = getDefaultTplWrapper(appInstanceId, sandbox)(template);
// shadowDom加载css的方式
const strictStyleIsolation = typeof sandbox === 'object' && !!sandbox.strictStyleIsolation;
if (process.env.NODE_ENV === 'development' && strictStyleIsolation) {
console.warn(
"[qiankun] strictStyleIsolation configuration will be removed in 3.0, pls don't depend on it or use experimentalStyleIsolation instead!",
);
}
// 作用域css的处理(配置)
const scopedCSS = isEnableScopedCSS(sandbox);
// 创建一个div,根据样式是作用域样式还是严格样式隔离,分别作对应的处理.
let initialAppWrapperElement: HTMLElement | null = createElement(
appContent,
strictStyleIsolation,
scopedCSS,
appInstanceId,
);
const initialContainer = 'container' in app ? app.container : undefined; //设置子应用的容器container
const legacyRender = 'render' in app ? app.render : undefined;
const render = getRender(appInstanceId, appContent, legacyRender);
// 第一次加载设置应用可见区域 dom 结构
// 确保每次应用加载前容器 dom 结构已经设置完毕
// 将创建的div设置到container里面
render({ element: initialAppWrapperElement, loading: true, container: initialContainer }, 'loading');
// 获取外层的div或者ShadowRoot
const initialAppWrapperGetter = getAppWrapperGetter(
appInstanceId,
!!legacyRender,
strictStyleIsolation,
scopedCSS,
() => initialAppWrapperElement,
);
let global = globalContext; //默认是window
let mountSandbox = () => Promise.resolve();
let unmountSandbox = () => Promise.resolve();
const useLooseSandbox = typeof sandbox === 'object' && !!sandbox.loose; // 快照沙箱
// enable speedy mode by default
const speedySandbox = typeof sandbox === 'object' ? sandbox.speedy !== false : true; //proxy 沙箱
let sandboxContainer;
if (sandbox) {
// 创建沙箱容器
sandboxContainer = createSandboxContainer(
appInstanceId,
// FIXME should use a strict sandbox logic while remount, see https://github.com/umijs/qiankun/issues/518
initialAppWrapperGetter,
scopedCSS,
useLooseSandbox,
excludeAssetFilter,
global,
speedySandbox,
);
// 用沙箱的代理对象作为接下来使用的全局对象,否则就是快照
global = sandboxContainer.instance.proxy as typeof window;
mountSandbox = sandboxContainer.mount;
unmountSandbox = sandboxContainer.unmount;
}
// 获取一些全局对象,默认给global(假的window,给子应用的)设置一些全局变量,比如global.__POWERED_BY_QIANKUN_等
const {
beforeUnmount = [],
afterUnmount = [],
afterMount = [],
beforeMount = [],
beforeLoad = [],
} = mergeWith({}, getAddOns(global, assetPublicPath), lifeCycles, (v1, v2) => concat(v1 ?? [], v2 ?? []));
await execHooksChain(toArray(beforeLoad), app, global); //执行beforLoad函数,设置全局变量
// get the lifecycle hooks from module exports 执行子应用真正的脚本
// 根据指定的沙箱环境执行脚本
const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox, {
scopedGlobalVariables: speedySandbox ? cachedGlobals : [],
});
// 获取到子应用export出来的生命周期函数
const { bootstrap, mount, unmount, update } = getLifecyclesFromExports(
scriptExports,
appName,
global,
sandboxContainer?.instance?.latestSetProp,
);
const { onGlobalStateChange, setGlobalState, offGlobalStateChange }: Record<string, CallableFunction> =
getMicroAppStateActions(appInstanceId);
// FIXME temporary way
const syncAppWrapperElement2Sandbox = (element: HTMLElement | null) => (initialAppWrapperElement = element);
const parcelConfigGetter: ParcelConfigObjectGetter = (remountContainer = initialContainer) => {
let appWrapperElement: HTMLElement | null;
let appWrapperGetter: ReturnType<typeof getAppWrapperGetter>;
// 最终返回的config
const parcelConfig: ParcelConfigObject = {
name: appInstanceId,
bootstrap,
mount: [
async () => {
if (process.env.NODE_ENV === 'development') {
const marks = performanceGetEntriesByName(markName, 'mark');
// mark length is zero means the app is remounting
if (marks && !marks.length) {
performanceMark(markName);
}
}
},
async () => {
// 单例模式保证之前应用的卸载
if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
return prevAppUnmountedDeferred.promise;
}
return undefined;
},
// initial wrapper element before app mount/remount
async () => {
appWrapperElement = initialAppWrapperElement;
appWrapperGetter = getAppWrapperGetter(
appInstanceId,
!!legacyRender,
strictStyleIsolation,
scopedCSS,
() => appWrapperElement,
);
},
// 添加 mount hook, 确保每次应用加载前容器 dom 结构已经设置完毕
async () => {
const useNewContainer = remountContainer !== initialContainer;
if (useNewContainer || !appWrapperElement) {
// element will be destroyed after unmounted, we need to recreate it if it not exist
// or we try to remount into a new container
appWrapperElement = createElement(appContent, strictStyleIsolation, scopedCSS, appInstanceId);
syncAppWrapperElement2Sandbox(appWrapperElement);
}
render({ element: appWrapperElement, loading: true, container: remountContainer }, 'mounting');
},
mountSandbox, //沙箱挂载
// exec the chain after rendering to keep the behavior with beforeLoad
async () => execHooksChain(toArray(beforeMount), app, global),
async (props) => mount({ ...props, container: appWrapperGetter(), setGlobalState, onGlobalStateChange }),//执行子应用的mount函数
// finish loading after app mounted
async () => render({ element: appWrapperElement, loading: false, container: remountContainer }, 'mounted'),
async () => execHooksChain(toArray(afterMount), app, global),
// initialize the unmount defer after app mounted and resolve the defer after it unmounted
async () => {
if (await validateSingularMode(singular, app)) {
prevAppUnmountedDeferred = new Deferred<void>();
}
},
async () => {
if (process.env.NODE_ENV === 'development') {
const measureName = `[qiankun] App ${appInstanceId} Loading Consuming`;
performanceMeasure(measureName, markName);
}
},
],
unmount: [
async () => execHooksChain(toArray(beforeUnmount), app, global),
async (props) => unmount({ ...props, container: appWrapperGetter() }),
unmountSandbox,
async () => execHooksChain(toArray(afterUnmount), app, global),
async () => {
render({ element: null, loading: false, container: remountContainer }, 'unmounted');
offGlobalStateChange(appInstanceId);
// for gc
appWrapperElement = null;
syncAppWrapperElement2Sandbox(appWrapperElement);
},
async () => {
if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
prevAppUnmountedDeferred.resolve();
}
},
],
};
if (typeof update === 'function') {
parcelConfig.update = update;
}
return parcelConfig;
};
return parcelConfigGetter;
}
从头往下:
- 先给子应用取名字,然后使用import-html-entry拉取子应用的资源。然后先执行额外的脚本,比如cdn,然后获取文件内容,就是将子应用的html文件转为特定的html,比如
<head>转为<qiakun-head>
然后将style和script注释掉,因为css的话会特别处理,还有脚本需要在沙箱里面执行。
const { entry, name: appName } = app;
const appInstanceId = genAppInstanceIdByName(appName); //给当前加载的应用起一个名字
const markName = `[qiankun] App ${appInstanceId} Loading`;
if (process.env.NODE_ENV === 'development') {
performanceMark(markName);
}
const {
singular = false,
sandbox = true,
excludeAssetFilter,
globalContext = window,
...importEntryOpts
} = configuration;
// get the entry html content and script executor 使用import-html-entry拉资源,获取html文件和脚本的执行器,html的脚本会注视掉,然后转成execScripts,然后放在沙箱执行
const { template, execScripts, assetPublicPath, getExternalScripts } = await importEntry(entry, importEntryOpts);
// trigger external scripts loading to make sure all assets are ready before execScripts calling
await getExternalScripts(); //执行额外的脚本,确保真正的脚本执行之前加载完
// as single-spa load and bootstrap new app parallel with other apps unmounting
// (see https://github.com/CanopyTax/single-spa/blob/master/src/navigation/reroute.js#L74)
// we need wait to load the app until all apps are finishing unmount in singular mode
if (await validateSingularMode(singular, app)) {
//单例模式下,要保证之前的应用被卸载
await (prevAppUnmountedDeferred && prevAppUnmountedDeferred.promise);
}
// 获取文件内容,将html转为qiankun特定的html
const appContent = getDefaultTplWrapper(appInstanceId, sandbox)(template);
- 接着处理css(shadowdom或者css作用域),
// shadowDom加载css的方式
const strictStyleIsolation = typeof sandbox === 'object' && !!sandbox.strictStyleIsolation;
if (process.env.NODE_ENV === 'development' && strictStyleIsolation) {
console.warn(
"[qiankun] strictStyleIsolation configuration will be removed in 3.0, pls don't depend on it or use experimentalStyleIsolation instead!",
);
}
// 作用域css的处理(配置)
const scopedCSS = isEnableScopedCSS(sandbox);
// 创建一个div,根据样式是作用域样式还是严格样式隔离,分别作对应的处理.
let initialAppWrapperElement: HTMLElement | null = createElement(
appContent,
strictStyleIsolation,
scopedCSS,
appInstanceId,
);
const initialContainer = 'container' in app ? app.container : undefined; //设置子应用的容器container
const legacyRender = 'render' in app ? app.render : undefined;
const render = getRender(appInstanceId, appContent, legacyRender);
// 第一次加载设置应用可见区域 dom 结构
// 确保每次应用加载前容器 dom 结构已经设置完毕
// 将创建的div设置到container里面
render({ element: initialAppWrapperElement, loading: true, container: initialContainer }, 'loading');
qiankun支持start({sandbox:true|{strictStyleIsolation?: boolean, experimentalStyleIsolation?: boolean}})
如上,如果sandbox.strictStyleIsolation开启,那就是严格css,这样会用shadowDom来加载css(qiankun3.0会移除),如果不是sandbox.strictStyleIsolation,那么就会判断是不是sandbox.experimentalStyleIsolation,也就是scopedCSS(作用域css,给子应用所有的css加前缀)
然后会创建一个element,将子应用文件内容注入到div里面去。(会根据用户配置处理css)
/**
* 创建div,将获取到的转化后的html加载进去,使用attachShadow沙箱模式隔离环境。
* 再判断css的处理逻辑
*/
function createElement(
appContent: string,
strictStyleIsolation: boolean,
scopedCSS: boolean,
appInstanceId: string,
): HTMLElement {
const containerElement = document.createElement('div');
containerElement.innerHTML = appContent;
// appContent always wrapped with a singular div
const appElement = containerElement.firstChild as HTMLElement;
if (strictStyleIsolation) {
// 如果是严格的样式隔离,就使用影子dom
if (!supportShadowDOM) {
console.warn(
'[qiankun]: As current browser not support shadow dom, your strictStyleIsolation configuration will be ignored!',
);
} else {
const { innerHTML } = appElement;
appElement.innerHTML = '';
let shadow: ShadowRoot;
if (appElement.attachShadow) {
shadow = appElement.attachShadow({ mode: 'open' });
} else {
// createShadowRoot was proposed in initial spec, which has then been deprecated
shadow = (appElement as any).createShadowRoot();
}
shadow.innerHTML = innerHTML;
}
}
//对于作用域的css,就是拿到style标签里面的css,增加css前缀(对于link加载的css而言,也会转化)
if (scopedCSS) {
// 重写css属性名
const attr = appElement.getAttribute(css.QiankunCSSRewriteAttr);
if (!attr) {
appElement.setAttribute(css.QiankunCSSRewriteAttr, appInstanceId);
}
const styleNodes = appElement.querySelectorAll('style') || [];
forEach(styleNodes, (stylesheetElement: HTMLStyleElement) => {
// 给每个样式都加上前缀名
css.process(appElement!, stylesheetElement, appInstanceId);
});
}
return appElement;
}
如上,如果是严格css,那么就使用影子dom,此时影子dom跟外部的样式互不影响。如果是作用域css,那么给所有的css增加前缀,对于link加载的css也会转化。
接着调用render({ element: initialAppWrapperElement, loading: true, container: initialContainer }, ‘loading’);
将创建好的div,注入到container里面(用户配置,#app1),确保每次加载前,dom结构已经设置完毕。
- 接着创建沙箱
// 获取外层的div或者ShadowRoot
const initialAppWrapperGetter = getAppWrapperGetter(
appInstanceId,
!!legacyRender,
strictStyleIsolation,
scopedCSS,
() => initialAppWrapperElement,
);
let global = globalContext; //默认是window
let mountSandbox = () => Promise.resolve();
let unmountSandbox = () => Promise.resolve();
const useLooseSandbox = typeof sandbox === 'object' && !!sandbox.loose; // 降级proxy沙箱
// enable speedy mode by default
const speedySandbox = typeof sandbox === 'object' ? sandbox.speedy !== false : true; //proxy 沙箱
let sandboxContainer;
if (sandbox) {
// 创建沙箱容器
sandboxContainer = createSandboxContainer(
appInstanceId,
// FIXME should use a strict sandbox logic while remount, see https://github.com/umijs/qiankun/issues/518
initialAppWrapperGetter,
scopedCSS,
useLooseSandbox,
excludeAssetFilter,
global,
speedySandbox,
);
// 用沙箱的代理对象作为接下来使用的全局对象,否则就是快照
global = sandboxContainer.instance.proxy as typeof window;
mountSandbox = sandboxContainer.mount;
unmountSandbox = sandboxContainer.unmount;
}
判断是否使用降级proxy沙箱还是正常的proxy沙箱。接着调用createSandboxContainer创建沙箱。
export function createSandboxContainer(
appName: string,
elementGetter: () => HTMLElement | ShadowRoot,
scopedCSS: boolean,
useLooseSandbox?: boolean,
excludeAssetFilter?: (url: string) => boolean,
globalContext?: typeof window,
speedySandBox?: boolean,
) {
let sandbox: SandBox;
if (window.Proxy) {
sandbox = useLooseSandbox
? new LegacySandbox(appName, globalContext) //为了兼容性 singular 模式下依旧使用该沙箱
: new ProxySandbox(appName, globalContext, { speedy: !!speedySandBox }); //proxy 沙箱
} else {
sandbox = new SnapshotSandbox(appName);//快照沙箱
}
// some side effect could be invoked while bootstrapping, such as dynamic stylesheet injection with style-loader, especially during the development phase
const bootstrappingFreers = patchAtBootstrapping(
appName,
elementGetter,
sandbox,
scopedCSS,
excludeAssetFilter,
speedySandBox,
);
// mounting freers are one-off and should be re-init at every mounting time
let mountingFreers: Freer[] = [];
let sideEffectsRebuilders: Rebuilder[] = [];
return {
instance: sandbox,
/**
* 沙箱被 mount
* 可能是从 bootstrap 状态进入的 mount
* 也可能是从 unmount 之后再次唤醒进入 mount
*/
async mount() {
/* ------------------------------------------ 因为有上下文依赖(window),以下代码执行顺序不能变 ------------------------------------------ */
/* ------------------------------------------ 1. 启动/恢复 沙箱------------------------------------------ */
sandbox.active();
const sideEffectsRebuildersAtBootstrapping = sideEffectsRebuilders.slice(0, bootstrappingFreers.length);
const sideEffectsRebuildersAtMounting = sideEffectsRebuilders.slice(bootstrappingFreers.length);
// must rebuild the side effects which added at bootstrapping firstly to recovery to nature state
if (sideEffectsRebuildersAtBootstrapping.length) {
sideEffectsRebuildersAtBootstrapping.forEach((rebuild) => rebuild());
}
/* ------------------------------------------ 2. 开启全局变量补丁 ------------------------------------------*/
// render 沙箱启动时开始劫持各类全局监听,尽量不要在应用初始化阶段有 事件监听/定时器 等副作用
mountingFreers = patchAtMounting(appName, elementGetter, sandbox, scopedCSS, excludeAssetFilter, speedySandBox);
/* ------------------------------------------ 3. 重置一些初始化时的副作用 ------------------------------------------*/
// 存在 rebuilder 则表明有些副作用需要重建
if (sideEffectsRebuildersAtMounting.length) {
sideEffectsRebuildersAtMounting.forEach((rebuild) => rebuild());
}
// clean up rebuilders
sideEffectsRebuilders = [];
},
/**
* 恢复 global 状态,使其能回到应用加载之前的状态
*/
async unmount() {
// record the rebuilders of window side effects (event listeners or timers)
// note that the frees of mounting phase are one-off as it will be re-init at next mounting
sideEffectsRebuilders = [...bootstrappingFreers, ...mountingFreers].map((free) => free());
sandbox.inactive();
},
};
}
判断使用快照沙箱还是proxy沙箱,然后返回一个包含mount和unmount等的对象,这里需要执行对应时期的一些函数。
沙箱分两种类型,app环境沙箱和render沙箱。
app环境沙箱应用初始化过之后,应用会在什么样的上下文环境运行。每个应用的环境沙箱只会初始化一次,因为子应用只会触发一次 bootstrap 。
render沙箱是子应用在app mount之前开始生成的沙箱,每次子应用切换过后,render沙箱都会重新初始化。
- 创建沙箱之后,就需要设置全局变量,然后执行子应用的js脚本。
// 获取一些全局对象,默认给global(假的window,给子应用的)设置一些全局变量,比如global.__POWERED_BY_QIANKUN_等
const {
beforeUnmount = [],
afterUnmount = [],
afterMount = [],
beforeMount = [],
beforeLoad = [],
} = mergeWith({}, getAddOns(global, assetPublicPath), lifeCycles, (v1, v2) => concat(v1 ?? [], v2 ?? []));
await execHooksChain(toArray(beforeLoad), app, global); //执行beforLoad函数,设置全局变量
// get the lifecycle hooks from module exports 执行子应用真正的脚本
// 根据指定的沙箱环境执行脚本
const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox, {
scopedGlobalVariables: speedySandbox ? cachedGlobals : [],
});
// 获取到子应用export出来的生命周期函数
const { bootstrap, mount, unmount, update } = getLifecyclesFromExports(
scriptExports,
appName,
global,
sandboxContainer?.instance?.latestSetProp,
);
// qiankun3.0即将舍弃
const { onGlobalStateChange, setGlobalState, offGlobalStateChange }: Record<string, CallableFunction> =
getMicroAppStateActions(appInstanceId);
使用mergeWidth收集每个时期应该做的事情。如getAddOns(global, assetPublicPath),assetPublicPath在import-html-entry获取目标资源的时候就已经获取到了。
import type { FrameworkLifeCycles } from '../interfaces';
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__;
},
};
}
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;
},
};
}
设置子应用的__INJECTED_PUBLIC_PATH_BY_QIANKUN__属性。这也可以理解子应用可以获取到qiankun特定的全局属性。
接着执行beforeLoad函数,设置全局变量。
// 根据指定的沙箱环境执行脚本
const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox, {
scopedGlobalVariables: speedySandbox ? cachedGlobals : [],
});
根据指定的沙箱环境,执行子应用真正的脚本(这时css和extra脚本已经执行完毕)execScripts是import-html-entry的包
// 获取到子应用export出来的生命周期函数
const { bootstrap, mount, unmount, update } = getLifecyclesFromExports(
scriptExports,
appName,
global,
sandboxContainer?.instance?.latestSetProp,
);
通过他的返回,可以获取到子应用入口文件export出来的生命周期函数。
- 接着是最后一步,因为qiankun是基于single-spa的,所以相当于在应用跟single-sap中间拦截了一层,多执行了一些东西,然后将对应的接入协议(bootsrtap,mount,unmount)继续交个sinle-spa处理。
const parcelConfigGetter: ParcelConfigObjectGetter = (remountContainer = initialContainer) => {
let appWrapperElement: HTMLElement | null;
let appWrapperGetter: ReturnType<typeof getAppWrapperGetter>;
// 最终返回的config
const parcelConfig: ParcelConfigObject = {
name: appInstanceId,
bootstrap,
mount: [
async () => {
if (process.env.NODE_ENV === 'development') {
const marks = performanceGetEntriesByName(markName, 'mark');
// mark length is zero means the app is remounting
if (marks && !marks.length) {
performanceMark(markName);
}
}
},
async () => {
// 单例模式保证之前应用的卸载
if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
return prevAppUnmountedDeferred.promise;
}
return undefined;
},
// initial wrapper element before app mount/remount
async () => {
appWrapperElement = initialAppWrapperElement;
appWrapperGetter = getAppWrapperGetter(
appInstanceId,
!!legacyRender,
strictStyleIsolation,
scopedCSS,
() => appWrapperElement,
);
},
// 添加 mount hook, 确保每次应用加载前容器 dom 结构已经设置完毕
async () => {
const useNewContainer = remountContainer !== initialContainer;
if (useNewContainer || !appWrapperElement) {
// element will be destroyed after unmounted, we need to recreate it if it not exist
// or we try to remount into a new container
appWrapperElement = createElement(appContent, strictStyleIsolation, scopedCSS, appInstanceId);
syncAppWrapperElement2Sandbox(appWrapperElement);
}
render({ element: appWrapperElement, loading: true, container: remountContainer }, 'mounting');
},
mountSandbox, //沙箱挂载
// exec the chain after rendering to keep the behavior with beforeLoad
async () => execHooksChain(toArray(beforeMount), app, global),
async (props) => mount({ ...props, container: appWrapperGetter(), setGlobalState, onGlobalStateChange }),//执行子应用的mount函数
// finish loading after app mounted
async () => render({ element: appWrapperElement, loading: false, container: remountContainer }, 'mounted'),
async () => execHooksChain(toArray(afterMount), app, global),
// initialize the unmount defer after app mounted and resolve the defer after it unmounted
async () => {
if (await validateSingularMode(singular, app)) {
prevAppUnmountedDeferred = new Deferred<void>();
}
},
async () => {
if (process.env.NODE_ENV === 'development') {
const measureName = `[qiankun] App ${appInstanceId} Loading Consuming`;
performanceMeasure(measureName, markName);
}
},
],
unmount: [
async () => execHooksChain(toArray(beforeUnmount), app, global),
async (props) => unmount({ ...props, container: appWrapperGetter() }),
unmountSandbox,
async () => execHooksChain(toArray(afterUnmount), app, global),
async () => {
render({ element: null, loading: false, container: remountContainer }, 'unmounted');
offGlobalStateChange(appInstanceId);
// for gc
appWrapperElement = null;
syncAppWrapperElement2Sandbox(appWrapperElement);
},
async () => {
if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
prevAppUnmountedDeferred.resolve();
}
},
],
};
if (typeof update === 'function') {
parcelConfig.update = update;
}
return parcelConfig;
};
最终返回的parcelConfig,里面就包含single-spa需要的接入协议。如上,moutn和unmount都执行了除子应用的之外,还执行了很多其他逻辑。
如mount函数:
mount: [
// 1 单例模式保证之前应用的卸载
async () => {
if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
return prevAppUnmountedDeferred.promise;
}
return undefined;
},
// initial wrapper element before app mount/remount
// 2 初始化容器在app mount之前
async () => {
appWrapperElement = initialAppWrapperElement;
appWrapperGetter = getAppWrapperGetter(
appInstanceId,
!!legacyRender,
strictStyleIsolation,
scopedCSS,
() => appWrapperElement,
);
},
// 3 添加 mount hook, 确保每次应用加载前容器 dom 结构已经设置完毕
async () => {
const useNewContainer = remountContainer !== initialContainer;
if (useNewContainer || !appWrapperElement) {
// element will be destroyed after unmounted, we need to recreate it if it not exist
// or we try to remount into a new container
appWrapperElement = createElement(appContent, strictStyleIsolation, scopedCSS, appInstanceId);
syncAppWrapperElement2Sandbox(appWrapperElement);
}
render({ element: appWrapperElement, loading: true, container: remountContainer }, 'mounting');
},
// 4沙箱挂载
mountSandbox,
// exec the chain after rendering to keep the behavior with beforeLoad
// 5 执行对应的beofrMount函数, 设置全局变量
async () => execHooksChain(toArray(beforeMount), app, global),
// 6 执行子应用的mount函数
async (props) => mount({ ...props, container: appWrapperGetter(), setGlobalState, onGlobalStateChange }),
// finish loading after app mounted
async () => render({ element: appWrapperElement, loading: false, container: remountContainer }, 'mounted'),
// 7 执行对应的afterMount函数
async () => execHooksChain(toArray(afterMount), app, global),
// initialize the unmount defer after app mounted and resolve the defer after it unmounted
async () => {
if (await validateSingularMode(singular, app)) {
prevAppUnmountedDeferred = new Deferred<void>();
}
},
async () => {
if (process.env.NODE_ENV === 'development') {
const measureName = `[qiankun] App ${appInstanceId} Loading Consuming`;
performanceMeasure(measureName, markName);
}
},
],
最终将返回的parcelConfig交给single-spa处理。single-spa拦截路由匹配到路径的时候,就会执行该子应用对应的函数,也就会执行上述的一些逻辑。
未完待续…