主应用调用
1. registerMicroApps 注册微应用
2. start 启动
这两个api,微应用在入口提供生命周期回调函数
简要流程
qiankun的底层还是调用了single-spa,它额外做了以下处理:
- 样式隔离
- JS沙箱
- 资源预加载
- HTML Entry接入方式
- 应用间通信
主要API
一、 registerMicroApps(apps, lifeCycles)
- 全局变量microApps用来存储所有已注册的微应用
- 从apps中找出未注册的微应用unregisteredApps
- 将unregisteredApps并入microApps
- 遍历unregisteredApps,调用single-spa的registerApplication 以下app会在触发微应用activeRule时执行
app: async () => {
// 调用app.loader(loading 状态发生变化时会调用的方法。)
loader(true);
// 确保先启动了再执行这里(关注start api的末尾)
await frameworkStartedDefer.promise;
const { mount, ...otherMicroAppConfigs } = (
// frameworkConfiguration:start 方法执行时设置的配置对象
await loadApp({ name, props, ...appConfig }, frameworkConfiguration, lifeCycles)
)();
return {
mount: [async () => loader(true), ...toArray(mount), async () => loader(false)],
...otherMicroAppConfigs,
};
},
以上loadApp加载微应用
- 获取微应用的入口 html 内容和脚本执行器(调用import-html-entry的importEntry)
const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);
以上即qiankun在single-spa基础上的优化:HTML Entry接入方式
解决single-spa需要自己提供加载微应用函数,需要将微应用打包成一个js文件,这样对微应用的修改成本太高,不易维护。
- single-spa 的限制,加载、初始化和卸载不能同时进行,必须等卸载完成以后才可以进行加载,这个 promise 会在微应用卸载完成后被 resolve
if (await validateSingularMode(singular, app)) {
await (prevAppUnmountedDeferred && prevAppUnmountedDeferred.promise);
}
- 用一个容器元素包裹微应用入口 html 模版, -> appContent
`<div id="`__qiankun_microapp_wrapper_for_${snakeCase(id)}__`" data-name="${name}" data-version="${version}">${tpl}</div>`
- 根据start参数选项中配置的sandbox:
- strictStyleIsolation:是否开启严格的样式隔离模式。这种模式下 qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响。
- experimentalStyleIsolation: 设置为 true 时,qiankun 会改写子应用所添加的样式,为所有样式规则增加一个特殊的选择器规则来限定其影响范围
将appContent转换成dom节点initialAppWrapperElement
const appContent = getDefaultTplWrapper(appInstanceId, appName)(template);
const strictStyleIsolation = typeof sandbox === 'object' && !!sandbox.strictStyleIsolation;
const scopedCSS = isEnableScopedCSS(sandbox);
let initialAppWrapperElement: HTMLElement | null = createElement(
appContent,
strictStyleIsolation,
scopedCSS,
appName,
);
以上即qiankun在single-spa基础上的优化:样式隔离
有两种方式
shadow DOM: Web components 的一个重要属性是封装——可以将标记结构、样式和行为隐藏起来,并与页面上的其他代码相隔离,保证不同的部分不会混在一起,可使代码更加干净、整洁。其中,Shadow DOM 接口是关键所在,它可以将一个隐藏的、独立的 DOM 附加到一个元素上。
- 获取渲染函数
const render = getRender(appName, appContent, legacyRender);
- 执行render函数 调用app.render(后续版本会删除)或者将container清除后插入element
render({ element: initialAppWrapperElement, loading: true, container: initialContainer }, 'loading');
- 生成一个获取微应用dom(div id="
__qiankun_microapp_wrapper_for_${snakeCase(id)}__")的方法
const initialAppWrapperGetter = getAppWrapperGetter(
appName,
appInstanceId,
!!legacyRender,
strictStyleIsolation,
scopedCSS,
() => initialAppWrapperElement,
);
- 沙箱
let global = globalContext;
let mountSandbox = () => Promise.resolve();
let unmountSandbox = () => Promise.resolve();
const useLooseSandbox = typeof sandbox === 'object' && !!sandbox.loose;
let sandboxContainer;
if (sandbox) {
sandboxContainer = createSandboxContainer(
appName,
// FIXME should use a strict sandbox logic while remount, see https://github.com/umijs/qiankun/issues/518
initialAppWrapperGetter,
scopedCSS,
useLooseSandbox,
excludeAssetFilter,
global,
);
// 用沙箱的代理对象作为接下来使用的全局对象
global = sandboxContainer.instance.proxy as typeof window;
mountSandbox = sandboxContainer.mount;
unmountSandbox = sandboxContainer.unmount;
}
三种模式:
- 不支持Proxy: SnapshotSandbox
- 支持Proxy单例:LegacySandbox
- 支持Proxy多例:ProxySandbox
- 合并生命周期
const {
beforeUnmount = [],
afterUnmount = [],
afterMount = [],
beforeMount = [],
beforeLoad = [],
} = mergeWith({}, getAddOns(global, assetPublicPath), lifeCycles, (v1, v2) => concat(v1 ?? [], v2 ?? []));
- 执行beforeLoad
await execHooksChain(toArray(beforeLoad), app, global);
- 获取微应用入口导出的生命周期函数
const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox);
const { bootstrap, mount, unmount, update } = getLifecyclesFromExports(
scriptExports,
appName,
global,
sandboxContainer?.instance?.latestSetProp,
);
- 给微应用注册通信方法并返回通信方法,然后会将通信方法通过 props 注入到微应用
const { onGlobalStateChange, setGlobalState, offGlobalStateChange }: Record<string, CallableFunction> =
getMicroAppStateActions(appInstanceId);
通信:设置全局对象globalState,调用initGlobalState可以对其进行初始化,微应用可以通过以上三个方法监听state修改,修改state,移除监听。
- 返回mount等single-spa的registerApplication的app需要return的promise<生命周期集合>
// FIXME temporary way
const syncAppWrapperElement2Sandbox = (element: HTMLElement | null) => (initialAppWrapperElement = element);
const parcelConfigGetter: ParcelConfigObjectGetter = (remountContainer = initialContainer) => {
let appWrapperElement: HTMLElement | null;
let appWrapperGetter: ReturnType<typeof getAppWrapperGetter>;
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 () => { // 单例模式需要等微应用卸载完成以后才能执行挂载任务,promise 会在微应用卸载完以后 resolve
if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
return prevAppUnmountedDeferred.promise;
}
return undefined;
},
// initial wrapper element before app mount/remount
async () => {
appWrapperElement = initialAppWrapperElement;
appWrapperGetter = getAppWrapperGetter(
appName,
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, appName);
syncAppWrapperElement2Sandbox(appWrapperElement);
}
render({ element: appWrapperElement, loading: true, container: remountContainer }, 'mounting');
},
mountSandbox, // 运行时沙箱导出的 mount
// exec the chain after rendering to keep the behavior with beforeLoad
async () => execHooksChain(toArray(beforeMount), app, global),
// 向微应用的 mount 生命周期函数传递参数,比如微应用中使用的 props.onGlobalStateChange 方法
async (props) => mount({ ...props, container: appWrapperGetter(), setGlobalState, onGlobalStateChange }),
// 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;
};
二、start(opts)
- 设置默认值,解析opts
frameworkConfiguration = { prefetch: true, singular: true, sandbox: true, ...opts };
const {
prefetch,
sandbox,
singular,
urlRerouteOnly = defaultUrlRerouteOnly,
...importEntryOpts
} = frameworkConfiguration;
- 预加载
if (prefetch) {
doPrefetchStrategy(microApps, prefetch, importEntryOpts);
}
- prefetch为数组,监听: single-spa:first-mount,预加载数组中的微应用
prefetchAfterFirstMounted(appsName2Apps(prefetchStrategy as string[]), importEntryOpts);
- prefetch为函数,执行该函数,在预加载时机,加载首屏应用,次屏应用
(async () => {
// critical rendering apps would be prefetch as earlier as possible
const { criticalAppNames = [], minorAppsName = [] } = await prefetchStrategy(apps);
prefetchImmediately(appsName2Apps(criticalAppNames), importEntryOpts);
prefetchAfterFirstMounted(appsName2Apps(minorAppsName), importEntryOpts);
})();
- 其他-true
prefetchAfterFirstMounted(apps, importEntryOpts);
- 其他-all
prefetchImmediately(apps, importEntryOpts);
- 降级,不支持Proxy特性的低版本浏览器强制使用单例模式
frameworkConfiguration = autoDowngradeForLowVersionBrowser(frameworkConfiguration);
- 执行single-spa的start
startSingleSpa({ urlRerouteOnly });
- 全局设置标识值started,用于在手动调用loadMicroApp时判断是否已启动
started = true;
- 全局设置,用于在执行single-spa的app方法时先等待start执行完再loadApp
frameworkStartedDefer.resolve();
调用的singl-spa API
registerApplication(appNameOrConfig, appOrLoadApp, activeWhen, customProps)
- 解析、验证参数
const registration = sanitizeArguments(
appNameOrConfig,
appOrLoadApp,
activeWhen,
customProps
);
- apps为全局变量,存储已注册的微应用。如果当前应用已注册则抛出错误
if (getAppNames().indexOf(registration.name) !== -1)
throw Error(
formatErrorMessage(
21,
__DEV__ &&
`There is already an app registered with name ${registration.name}`,
registration.name
)
);
- 将当前应用存入apps,状态为NOT_LOADED
apps.push(
assign(
{
loadErrorTime: null,
status: NOT_LOADED,
parcels: {},
devtools: {
overlays: {
options: {},
selectors: [],
},
},
},
registration
)
);
- 确保兼容jQuery,添加补丁;执行主要逻辑reroute
if (isInBrowser) {
ensureJQuerySupport();
reroute();
}
以上reroute执行场景:
- 注册微应用
- 启动
- 路由变化
- 等待切换
if (appChangeUnderway) {
return new Promise((resolve, reject) => {
peopleWaitingOnAppChange.push({
resolve,
reject,
eventArguments,
});
});
}
- 根据状态将apps整理归类到以下4个数组中
const {
appsToUnload,
appsToUnmount,
appsToLoad,
appsToMount,
} = getAppChanges();
- 如果已经执行过start方法,则performAppChanges
if (isStarted()) {
appChangeUnderway = true;
appsThatChanged = appsToUnload.concat(
appsToLoad,
appsToUnmount,
appsToMount
);
return performAppChanges();
}
- 如果没有执行过start方法,则loadApps
else {
appsThatChanged = appsToLoad;
return loadApps();
}
以上loadApps:加载微应用
重要逻辑: toLoadPromise: 执行registerApplication的第二个参数app,将得到的生命周期函数注入每个微应用,此时状态是 NOT_BOOTSTRAPPED
-》 callAllEventListeners:监听浏览器路由事件
return Promise.resolve().then(() => {
const loadPromises = appsToLoad.map(toLoadPromise);
return (
Promise.all(loadPromises)
.then(callAllEventListeners)
// there are no mounted apps, before start() is called, so we always return []
.then(() => [])
.catch((err) => {
callAllEventListeners();
throw err;
})
);
});
以上performAppChanges:执行改变
- 触发状态变更事件
window.dispatchEvent(
new CustomEvent(
appsThatChanged.length === 0
? "single-spa:before-no-app-change"
: "single-spa:before-app-change",
getCustomEventDetail(true)
)
);
window.dispatchEvent(
new CustomEvent(
"single-spa:before-routing-event",
getCustomEventDetail(true, { cancelNavigation })
)
);
- 针对导航取消的处理
finishUpAndReturn: appChangeUnderway置为false,继续reroute peopleWaitingOnAppChange
navigateToUrl: 跳转到oldUrl
if (navigationIsCanceled) {
window.dispatchEvent(
new CustomEvent(
"single-spa:before-mount-routing-event",
getCustomEventDetail(true)
)
);
finishUpAndReturn();
navigateToUrl(oldUrl);
return;
}
- 移除
const unloadPromises = appsToUnload.map(toUnloadPromise);
const unmountUnloadPromises = appsToUnmount
.map(toUnmountPromise)
.map((unmountPromise) => unmountPromise.then(toUnloadPromise));
- 卸载
const allUnmountPromises = unmountUnloadPromises.concat(unloadPromises);
const unmountAllPromise = Promise.all(allUnmountPromises);
unmountAllPromise.then(() => {
window.dispatchEvent(
new CustomEvent(
"single-spa:before-mount-routing-event",
getCustomEventDetail(true)
)
);
});
- 加载
const loadThenMountPromises = appsToLoad.map((app) => {
return toLoadPromise(app).then((app) =>
tryToBootstrapAndMount(app, unmountAllPromise)
);
});
- 挂载
const mountPromises = appsToMount
.filter((appToMount) => appsToLoad.indexOf(appToMount) < 0)
.map((appToMount) => {
return tryToBootstrapAndMount(appToMount, unmountAllPromise);
});
start(opts)
started = true;
if (opts && opts.urlRerouteOnly) {
setUrlRerouteOnly(opts.urlRerouteOnly);
}
if (isInBrowser) {
reroute();
}