官方实现
2024年03月21日
一、入口 src/index.ts
/**
* 在示例或者官网提到的所有 API 都在这里统一导出
*/
// 最关键的三个,手动加载微应用、基于路由配置、启动 qiankun
export { loadMicroApp, registerMicroApps, start } from './apis';
// 全局状态
export { initGlobalState } from './globalState';
// 全局的未捕获异常处理器
export * from './errorHandler';
// setDefaultMountApp 设置主应用启动后默认进入哪个微应用、runAfterFirstMounted 设置当第一个微应用挂载以后需要调用的一些方法
export * from './effects';
// 类型定义
export * from './interfaces';
// prefetch
export { prefetchImmediately as prefetchApps } from './prefetch';
二、registerMicroApps
/**
* 注册微应用,基于路由配置
* @param apps = [
* {
* name: 'react16',
* entry: '//localhost:7100',
* container: '#subapp-viewport',
* loader,
* activeRule: '/react16'
* },
* ...
* ]
* @param lifeCycles = { ...各个生命周期方法对象 }
*/
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];
// 注册每一个微应用
unregisteredApps.forEach(app => {
// 注册时提供的微应用基本信息
const { name, activeRule, loader = noop, props, ...appConfig } = app;
// 调用 single-spa 的 registerApplication 方法注册微应用
registerApplication({
// 微应用名称
name,
// 微应用的加载方法,Promise<生命周期方法组成的对象>
app: async () => {
// 加载微应用时主应用显示 loading 状态
loader(true);
// 这句可以忽略,目的是在 single-spa 执行这个加载方法时让出线程,让其它微应用的加载方法都开始执行
await frameworkStartedDefer.promise;
// 核心、精髓、难点所在,负责加载微应用,然后一大堆处理,返回 bootstrap、mount、unmount、update 这个几个生命周期
const { mount, ...otherMicroAppConfigs } = await loadApp(
// 微应用的配置信息
{ name, props, ...appConfig },
// start 方法执行时设置的配置对象
frameworkConfiguration,
// 注册微应用时提供的全局生命周期对象
lifeCycles,
);
return {
mount: [async () => loader(true), ...toArray(mount), async () => loader(false)],
...otherMicroAppConfigs,
};
},
// 微应用的激活条件
activeWhen: activeRule,
// 传递给微应用的 props
customProps: props,
});
});
}
三、start
/**
* 启动 qiankun
* @param opts start 方法的配置对象
*/
export function start(opts: FrameworkConfiguration = {}) {
// qiankun 框架默认开启预加载、单例模式、样式沙箱
frameworkConfiguration = { prefetch: true, singular: true, sandbox: true, ...opts };
// 从这里可以看出 start 方法支持的参数不止官网文档说的那些,比如 urlRerouteOnly,这个是 single-spa 的 start 方法支持的
const { prefetch, sandbox, singular, urlRerouteOnly, ...importEntryOpts } = frameworkConfiguration;
// 预加载
if (prefetch) {
// 执行预加载策略,参数分别为微应用列表、预加载策略、{ fetch、getPublicPath、getTemplate }
doPrefetchStrategy(microApps, prefetch, importEntryOpts);
}
// 样式沙箱
if (sandbox) {
if (!window.Proxy) {
console.warn('[qiankun] Miss window.Proxy, proxySandbox will degenerate into snapshotSandbox');
// 快照沙箱不支持非 singular 模式
if (!singular) {
console.error('[qiankun] singular is forced to be true when sandbox enable but proxySandbox unavailable');
// 如果开启沙箱,会强制使用单例模式
frameworkConfiguration.singular = true;
}
}
}
// 执行 single-spa 的 start 方法,启动 single-spa
startSingleSpa({ urlRerouteOnly });
frameworkStartedDefer.resolve();
}
四、预加载 - doPrefetchStrategy
/**
* 执行预加载策略,qiankun 支持四种
* @param apps 所有的微应用
* @param prefetchStrategy 预加载策略,四种 =》
* 1、true,第一个微应用挂载以后加载其它微应用的静态资源,利用的是 single-spa 提供的 single-spa:first-mount 事件来实现的
* 2、string[],微应用名称数组,在第一个微应用挂载以后加载指定的微应用的静态资源
* 3、all,主应用执行 start 以后就直接开始预加载所有微应用的静态资源
* 4、自定义函数,返回两个微应用组成的数组,一个是关键微应用组成的数组,需要马上就执行预加载的微应用,一个是普通的微应用组成的数组,在第一个微应用挂载以后预加载这些微应用的静态资源
* @param importEntryOpts = { fetch, getPublicPath, getTemplate }
*/
export function doPrefetchStrategy(
apps: AppMetadata[],
prefetchStrategy: PrefetchStrategy,
importEntryOpts?: ImportEntryOpts,
) {
// 定义函数,函数接收一个微应用名称组成的数组,然后从微应用列表中返回这些名称所对应的微应用,最后得到一个数组[{name, entry}, ...]
const appsName2Apps = (names: string[]): AppMetadata[] => apps.filter(app => names.includes(app.name));
if (Array.isArray(prefetchStrategy)) {
// 说明加载策略是一个数组,当第一个微应用挂载之后开始加载数组内由用户指定的微应用资源,数组内的每一项表示一个微应用的名称
prefetchAfterFirstMounted(appsName2Apps(prefetchStrategy as string[]), importEntryOpts);
} else if (isFunction(prefetchStrategy)) {
// 加载策略是一个自定义的函数,可完全自定义应用资源的加载时机(首屏应用、次屏应用)
(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);
})();
} else {
// 加载策略是默认的 true 或者 all
switch (prefetchStrategy) {
case true:
// 第一个微应用挂载之后开始加载其它微应用的静态资源
prefetchAfterFirstMounted(apps, importEntryOpts);
break;
case 'all':
// 在主应用执行 start 以后就开始加载所有微应用的静态资源
prefetchImmediately(apps, importEntryOpts);
break;
default:
break;
}
}
}
// 判断是否为弱网环境
const isSlowNetwork = navigator.connection
? navigator.connection.saveData ||
(navigator.connection.type !== 'wifi' &&
navigator.connection.type !== 'ethernet' &&
/(2|3)g/.test(navigator.connection.effectiveType))
: false;
/**
* prefetch assets, do nothing while in mobile network
* 预加载静态资源,在移动网络下什么都不做
* @param entry
* @param opts
*/
function prefetch(entry: Entry, opts?: ImportEntryOpts): void {
// 弱网环境下不执行预加载
if (!navigator.onLine || isSlowNetwork) {
// Don't prefetch if in a slow network or offline
return;
}
// 通过时间切片的方式去加载静态资源,在浏览器空闲时去执行回调函数,避免浏览器卡顿
requestIdleCallback(async () => {
// 得到加载静态资源的函数
const { getExternalScripts, getExternalStyleSheets } = await importEntry(entry, opts);
// 样式
requestIdleCallback(getExternalStyleSheets);
// js 脚本
requestIdleCallback(getExternalScripts);
});
}
/**
* 在第一个微应用挂载之后开始加载 apps 中指定的微应用的静态资源
* 通过监听 single-spa 提供的 single-spa:first-mount 事件来实现,该事件在第一个微应用挂载以后会被触发
* @param apps 需要被预加载静态资源的微应用列表,[{ name, entry }, ...]
* @param opts = { fetch , getPublicPath, getTemplate }
*/
function prefetchAfterFirstMounted(apps: AppMetadata[], opts?: ImportEntryOpts): void {
// 监听 single-spa:first-mount 事件
window.addEventListener('single-spa:first-mount', function listener() {
// 已挂载的微应用
const mountedApps = getMountedApps();
// 从预加载的微应用列表中过滤出未挂载的微应用
const notMountedApps = apps.filter(app => mountedApps.indexOf(app.name) === -1);
// 开发环境打印日志,已挂载的微应用和未挂载的微应用分别有哪些
if (process.env.NODE_ENV === 'development') {
console.log(`[qiankun] prefetch starting after ${mountedApps} mounted...`, notMountedApps);
}
// 循环加载微应用的静态资源
notMountedApps.forEach(({ entry }) => prefetch(entry, opts));
// 移除 single-spa:first-mount 事件
window.removeEventListener('single-spa:first-mount', listener);
});
}
/**
* 在执行 start 启动 qiankun 之后立即预加载所有微应用的静态资源
* @param apps 需要被预加载静态资源的微应用列表,[{ name, entry }, ...]
* @param opts = { fetch , getPublicPath, getTemplate }
*/
export function prefetchImmediately(apps: AppMetadata[], opts?: ImportEntryOpts): void {
// 开发环境打印日志
if (process.env.NODE_ENV === 'development') {
console.log('[qiankun] prefetch starting for apps...', apps);
}
// 加载所有微应用的静态资源
apps.forEach(({ entry }) => prefetch(entry, opts));
}
五、应用间通信 initGlobalState
// 触发全局监听,执行所有应用注册的回调函数
function emitGlobal(state: Record<string, any>, prevState: Record<string, any>) {
// 循环遍历,执行所有应用注册的回调函数
Object.keys(deps).forEach((id: string) => {
if (deps[id] instanceof Function) {
deps[id](cloneDeep(state), cloneDeep(prevState));
}
});
}
/**
* 定义全局状态,并返回通信方法,一般由主应用调用,微应用通过 props 获取通信方法。
* @param state 全局状态,{ key: value }
*/
export function initGlobalState(state: Record<string, any> = {}) {
if (state === globalState) {
console.warn('[qiankun] state has not changed!');
} else {
// 方法有可能被重复调用,将已有的全局状态克隆一份,为空则是第一次调用 initGlobalState 方法,不为空则非第一次次调用
const prevGlobalState = cloneDeep(globalState);
// 将传递的状态克隆一份赋值为 globalState
globalState = cloneDeep(state);
// 触发全局监听,当然在这个位置调用,正常情况下没啥反应,因为现在还没有应用注册回调函数
emitGlobal(globalState, prevGlobalState);
}
// 返回通信方法,参数表示应用 id,true 表示自己是主应用调用
return getMicroAppStateActions(`global-${+new Date()}`, true);
}
/**
* 返回通信方法
* @param id 应用 id
* @param isMaster 表明调用的应用是否为主应用,在主应用初始化全局状态时,initGlobalState 内部调用该方法时会传递 true,其它都为 false
*/
export function getMicroAppStateActions(id: string, isMaster?: boolean): MicroAppStateActions {
return {
/**
* 全局依赖监听,为指定应用(id = 应用id)注册回调函数
* 依赖数据结构为:
* {
* {id}: callback
* }
*
* @param callback 注册的回调函数
* @param fireImmediately 是否立即执行回调
*/
onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) {
// 回调函数必须为 function
if (!(callback instanceof Function)) {
console.error('[qiankun] callback must be function!');
return;
}
// 如 果回调函数已经存在,重复注册时给出覆盖提示信息
if (deps[id]) {
console.warn(`[qiankun] '${id}' global listener already exists before this, new listener will overwrite it.`);
}
// id 为一个应用 id,一个应用对应一个回调
deps[id] = callback;
// 克隆全局状态
const cloneState = cloneDeep(globalState);
// 如果需要,立即出发回调执行
if (fireImmediately) {
callback(cloneState, cloneState);
}
},
/**
* setGlobalState 更新 store 数据
*
* 1. 对新输入 state 的第一层属性做校验,如果是主应用则可以添加新的一级属性进来,也可以更新已存在的一级属性,
* 如果是微应用,则只能更新已存在的一级属性,不可以新增一级属性
* 2. 触发全局监听,执行所有应用注册的回调函数,以达到应用间通信的目的
*
* @param state 新的全局状态
*/
setGlobalState(state: Record<string, any> = {}) {
if (state === globalState) {
console.warn('[qiankun] state has not changed!');
return false;
}
// 记录旧的全局状态中被改变的 key
const changeKeys: string[] = [];
// 旧的全局状态
const prevGlobalState = cloneDeep(globalState);
globalState = cloneDeep(
// 循环遍历新状态中的所有 key
Object.keys(state).reduce((_globalState, changeKey) => {
if (isMaster || _globalState.hasOwnProperty(changeKey)) {
// 主应用 或者 旧的全局状态存在该 key 时才进来,说明只有主应用才可以新增属性,微应用只可以更新已存在的属性值,且不论主应用微应用只能更新一级属性
// 记录被改变的key
changeKeys.push(changeKey);
// 更新旧状态中对应的 key value
return Object.assign(_globalState, { [changeKey]: state[changeKey] });
}
console.warn(`[qiankun] '${changeKey}' not declared when init state!`);
return _globalState;
}, globalState),
);
if (changeKeys.length === 0) {
console.warn('[qiankun] state has not changed!');
return false;
}
// 触发全局监听
emitGlobal(globalState, prevGlobalState);
return true;
},
// 注销该应用下的依赖
offGlobalStateChange() {
delete deps[id];
return true;
},
};
}
六、全局未捕获异常处理器
/**
* 整个文件的逻辑一眼明了,整个框架提供了两种全局异常捕获,一个是 single-spa 提供的,另一个是 qiankun 自己的,你只需提供相应的 回调函数即可
*/
// single-spa 的异常捕获
export { addErrorHandler, removeErrorHandler } from 'single-spa';
// qiankun 的异常捕获
// 监听了 error 和 unhandlerejection 事件
export function addGlobalUncaughtErrorHandler(errorHandler: OnErrorEventHandlerNonNull): void {
window.addEventListener('error', errorHandler);
window.addEventListener('unhandledrejection', errorHandler);
}
// 移除 error 和 unhandlerejection 事件监听
export function removeGlobalUncaughtErrorHandler(errorHandler: (...args: any[]) => any) {
window.removeEventListener('error', errorHandler);
window.removeEventListener('unhandledrejection', errorHandler);
}
七、setDefaultMountApp
/**
* 设置主应用启动后默认进入的微应用,其实是规定了第一个微应用挂载完成后决定默认进入哪个微应用
* 利用的是 single-spa 的 single-spa:no-app-change 事件,该事件在所有微应用状态改变结束后(即发生路由切换且新的微应用已经被挂载完成)触发
* @param defaultAppLink 微应用的链接,比如 /react16
*/
export function setDefaultMountApp(defaultAppLink: string) {
// 当事件触发时就说明微应用已经挂载完成,但这里只监听了一次,因为事件被触发以后就移除了监听,所以说是主应用启动后默认进入的微应用,且只执行了一次的原因
window.addEventListener('single-spa:no-app-change', function listener() {
// 说明微应用已经挂载完成,获取挂载的微应用列表,再次确认确实有微应用挂 载了,其实这个确认没啥必要
const mountedApps = getMountedApps();
if (!mountedApps.length) {
// 这个是 single-spa 提供的一个 api,通过触发 window.location.hash 或者 pushState 更改路由,切换微应用
navigateToUrl(defaultAppLink);
}
// 触发一次以后,就移除该事件的监听函数,后续的路由切换(事件触发)时就不再响应
window.removeEventListener('single-spa:no-app-change', listener);
});
}
// 这个 api 和 setDefaultMountApp 作用一致,官网也提到,兼容老版本的一个 api
export function runDefaultMountEffects(defaultAppLink: string) {
console.warn(
'[qiankun] runDefaultMountEffects will be removed in next version, please use setDefaultMountApp instead',
);
setDefaultMountApp(defaultAppLink);
}