微前端框架 Qiankun 源码解析

上一篇博客我们介绍了Qiankun的沙盒机制,也就是不同的子应用之间是如何做环境隔离的,这篇博客我们就基于上一篇博客讲一下如何利用沙盒去进行子应用的加载和切换。

qiankun是基于single-spa实现的,所有内部用到了部分single-spa的接口,简单说一下single-spa和qiankun的区别,single-spa只做了子应用的注册,切换,路由监听等,还没有达到一个商用的水准,而qiankun在此之上加了js和css的沙箱隔离,按照官方文档,qiankun是达到了商用的要求。

globalState 全局状态的保存和修改

下面的代码我会加上自己的注释,同时有两点需要注意:

  • 这个全局状态,即globalState是唯一的,也就是说所有的子应用都用的同一个全局状态对象
  • 但是每个子应用都可以为全局状态的改变注册自己的回调函数,以appInstanceId为key存储在deps数组中,虽然当状态改变的时候所有的deps都会触发,但是通过appInstanceId为key可以在子应用被销毁的时候正确地注销该应用的回调函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
/**
* @author dbkillerf6
* @since 2020-04-10
*/

import { cloneDeep } from 'lodash';
import type { OnGlobalStateChangeCallback, MicroAppStateActions } from './interfaces';

// 全局状态,所有子应用共享
let globalState: Record<string, any> = {};

// 状态改变时的回调函数,key是app的id,value是回调函数
const deps: Record<string, OnGlobalStateChangeCallback> = {};

// 触发全局监听,遍历所有的deps并挨个触发
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));
}
});
}

// 暴露一个接口,提供一种独立于子应用之外的手段去修改全局状态
export function initGlobalState(state: Record<string, any> = {}) {
if (process.env.NODE_ENV === 'development') {
console.warn(`[qiankun] globalState tools will be removed in 3.0, pls don't use it!`);
}

if (state === globalState) {
console.warn('[qiankun] state has not changed!');
} else {
const prevGlobalState = cloneDeep(globalState);
globalState = cloneDeep(state);
emitGlobal(globalState, prevGlobalState);
}
return getMicroAppStateActions(`global-${+new Date()}`, true);
}

export function getMicroAppStateActions(id: string, isMaster?: boolean): MicroAppStateActions {
return {
/**
* onGlobalStateChange 全局依赖监听
*
* 收集 setState 时所需要触发的依赖
*
* 限制条件:每个子应用只有一个激活状态的全局监听,新监听覆盖旧监听,若只是监听部分属性,请使用 onGlobalStateChange
*
* 这么设计是为了减少全局监听滥用导致的内存爆炸
*
* 依赖数据结构为:
* {
* {id}: callback
* }
*
* @param callback
* @param fireImmediately
*/
onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) {
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.`);
}
deps[id] = callback;
if (fireImmediately) {
const cloneState = cloneDeep(globalState);
callback(cloneState, cloneState);
}
},

/**
* setGlobalState 更新 store 数据
*
* 1. 对输入 state 的第一层属性做校验,只有初始化时声明过的第一层(bucket)属性才会被更改
* 2. 修改 store 并触发全局监听
*
* @param state
*/
setGlobalState(state: Record<string, any> = {}) {
if (state === globalState) {
console.warn('[qiankun] state has not changed!');
return false;
}

const changeKeys: string[] = [];
const prevGlobalState = cloneDeep(globalState);
globalState = cloneDeep(
Object.keys(state).reduce((_globalState, changeKey) => {
if (isMaster || _globalState.hasOwnProperty(changeKey)) {
changeKeys.push(changeKey);
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;
},
};
}

prefetch 预加载子应用资源

这里主要是提供预加载子应用的策略和方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/**
* @author Kuitos
* @since 2019-02-26
*/

import type { Entry, ImportEntryOpts } from 'import-html-entry';
import { importEntry } from 'import-html-entry';
import { isFunction } from 'lodash';
import { getAppStatus, getMountedApps, NOT_LOADED } from 'single-spa';
import type { AppMetadata, PrefetchStrategy } from './interfaces';

declare global {
interface NetworkInformation {
saveData: boolean;
effectiveType: string;
}
}

// 如果浏览器不支持requestIdleCallback,降级为setTimeout
// RIC and shim for browsers setTimeout() without it
const requestIdleCallback =
window.requestIdleCallback ||
function requestIdleCallback(cb: CallableFunction) {
const start = Date.now();
return setTimeout(() => {
cb({
didTimeout: false,
timeRemaining() {
return Math.max(0, 50 - (Date.now() - start));
},
});
}, 1);
};

declare global {
interface Navigator {
connection: {
saveData: boolean;
effectiveType: string;
type: 'bluetooth' | 'cellular' | 'ethernet' | 'none' | 'wifi' | 'wimax' | 'other' | 'unknown';
};
}
}

const isSlowNetwork = navigator.connection
? navigator.connection.saveData ||
(navigator.connection.type !== 'wifi' &&
navigator.connection.type !== 'ethernet' &&
/([23])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;
}

// 调用single-spa的importEntry接口去加载子应用入口,返回加载js和css的函数,然后利用requestIdleCallback函数在空闲时加载
requestIdleCallback(async () => {
const { getExternalScripts, getExternalStyleSheets } = await importEntry(entry, opts);
requestIdleCallback(getExternalStyleSheets);
requestIdleCallback(getExternalScripts);
});
}

// 当single-spa first-mount事件触发时,查询所有app中尚未加载的,然后进行预加载
function prefetchAfterFirstMounted(apps: AppMetadata[], opts?: ImportEntryOpts): void {
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);
});
}

// 立即执行预加载,并且不会从apps中过滤已经加载过的
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));
}

// 根据参数重传入的策略配置选择适当的预加载策略
export function doPrefetchStrategy(
apps: AppMetadata[],
prefetchStrategy: PrefetchStrategy,
importEntryOpts?: ImportEntryOpts,
) {
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 {
switch (prefetchStrategy) {
case true:
prefetchAfterFirstMounted(apps, importEntryOpts);
break;

case 'all':
prefetchImmediately(apps, importEntryOpts);
break;

default:
break;
}
}
}

loader 子应用加载器

loader内部工具函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
function assertElementExist(element: Element | null | undefined, msg?: string) {
if (!element) {
if (msg) {
throw new QiankunError(msg);
}

throw new QiankunError('element not existed!');
}
}

function execHooksChain<T extends ObjectType>(
hooks: Array<LifeCycleFn<T>>,
app: LoadableApp<T>,
global = window,
): Promise<any> {
if (hooks.length) {
return hooks.reduce((chain, hook) => chain.then(() => hook(app, global)), Promise.resolve());
}

return Promise.resolve();
}

async function validateSingularMode<T extends ObjectType>(
validate: FrameworkConfiguration['singular'],
app: LoadableApp<T>,
): Promise<boolean> {
return typeof validate === 'function' ? validate(app) : !!validate;
}

const supportShadowDOM = !!document.head.attachShadow || !!(document.head as any).createShadowRoot;

function getLifecyclesFromExports(
scriptExports: LifeCycles<any>,
appName: string,
global: WindowProxy,
globalLatestSetProp?: PropertyKey | null,
) {
if (validateExportLifecycle(scriptExports)) {
return scriptExports;
}

// fallback to sandbox latest set property if it had
if (globalLatestSetProp) {
const lifecycles = (<any>global)[globalLatestSetProp];
if (validateExportLifecycle(lifecycles)) {
return lifecycles;
}
}

if (process.env.NODE_ENV === 'development') {
console.warn(
`[qiankun] lifecycle not found from ${appName} entry exports, fallback to get from window['${appName}']`,
);
}

// fallback to global variable who named with ${appName} while module exports not found
const globalVariableExports = (global as any)[appName];

if (validateExportLifecycle(globalVariableExports)) {
return globalVariableExports;
}

throw new QiankunError(`You need to export lifecycle functions in ${appName} entry`);
}

createElement

创建新的元素,参数列表为:

  • appContent:字符串类型,需要传入html文本
  • strictStyleIsolation:是否使用独立的shadow dom,如果为真,并且当前浏览器支持shadow dom,那么appContent中的内容会在一个shadow dom内部
  • scopedCSS:如果为真,则将本次创建的html中的所有css内容加上前缀,具体操作看我的另一篇博客,qiankun如何做css隔离的
  • appInstanceId:字符串类型,子应用的id
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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) {
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;
}
}

if (scopedCSS) {
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;
}

getAppWrapperGetter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function getAppWrapperGetter(
appInstanceId: string,
useLegacyRender: boolean,
strictStyleIsolation: boolean,
scopedCSS: boolean,
elementGetter: () => HTMLElement | null,
) {
return () => {
if (useLegacyRender) {
if (strictStyleIsolation) throw new QiankunError('strictStyleIsolation can not be used with legacy render!');
if (scopedCSS) throw new QiankunError('experimentalStyleIsolation can not be used with legacy render!');

const appWrapper = document.getElementById(getWrapperId(appInstanceId));
assertElementExist(appWrapper, `Wrapper element for ${appInstanceId} is not existed!`);
return appWrapper!;
}

const element = elementGetter();
assertElementExist(element, `Wrapper element for ${appInstanceId} is not existed!`);

if (strictStyleIsolation && supportShadowDOM) {
return element!.shadowRoot!;
}

return element!;
};
}

getRender

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* Get the render function
* If the legacy render function is provide, used as it, otherwise we will insert the app element to target container by qiankun
* @param appInstanceId
* @param appContent
* @param legacyRender
*/
function getRender(appInstanceId: string, appContent: string, legacyRender?: HTMLContentRender) {
const render: ElementRender = ({ element, loading, container }, phase) => {
if (legacyRender) {
if (process.env.NODE_ENV === 'development') {
console.error(
'[qiankun] Custom rendering function is deprecated and will be removed in 3.0, you can use the container element setting instead!',
);
}

return legacyRender({ loading, appContent: element ? appContent : '' });
}

const containerElement = getContainer(container!);

// The container might have be removed after micro app unmounted.
// Such as the micro app unmount lifecycle called by a react componentWillUnmount lifecycle, after micro app unmounted, the react component might also be removed
if (phase !== 'unmounted') {
const errorMsg = (() => {
switch (phase) {
case 'loading':
case 'mounting':
return `Target container with ${container} not existed while ${appInstanceId} ${phase}!`;

case 'mounted':
return `Target container with ${container} not existed after ${appInstanceId} ${phase}!`;

default:
return `Target container with ${container} not existed while ${appInstanceId} rendering!`;
}
})();
assertElementExist(containerElement, errorMsg);
}

if (containerElement && !containerElement.contains(element)) {
// clear the container
while (containerElement!.firstChild) {
rawRemoveChild.call(containerElement, containerElement!.firstChild);
}

// append the element to container if it exist
if (element) {
rawAppendChild.call(containerElement, element);
}
}

return undefined;
};

return render;
}

loadAPP

代码很长,简单分析下分别做了什么:

  • 调用single-spa的importEntry接口,获取子应用的加载入口等信息
  • 上一步解析出来的html entry会作为appContent传入createElement函数,创建一个新节点,并把html entry挂载到该节点之下,然后返回新的节点,赋值给initialAppWrapperElement
  • 调用getRender获取渲染函数,赋值给render
  • initialAppWrapperElement传入render函数
  • 调用getAppWrapperGetter获取增强过的函数initialAppWrapperGetter,内部会做一些判断,如果判断没有问题,返回的函数initialAppWrapperGetter会返回initialAppWrapperElement
  • 如果选择启动沙箱功能,则调用createSandboxContainer去返回sandboxContainer,并将全局对象赋值为sandboxContainer的proxy,启动沙箱功能
  • 通过调用getAddOns来获取所有的生命周期函数
  • 调用execHooksChain来执行上一步获得的生命周期中的beforeLoad函数
  • 通过execScripts(第一步single-spa的解析结果)配合getLifecyclesFromExports来获取子应用的生命周期钩子函数
  • 最终生成parcelConfigGetter函数,该函数会返回关于该子应用的 bootstrap,mount,和unmount方法,这些方法中会按顺序分别调用前面获取的不同的生命周期函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
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
const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);

// 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);
}

const appContent = getDefaultTplWrapper(appInstanceId)(template);

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!",
);
}

const scopedCSS = isEnableScopedCSS(sandbox);
let initialAppWrapperElement: HTMLElement | null = createElement(
appContent,
strictStyleIsolation,
scopedCSS,
appInstanceId,
);

const initialContainer = 'container' in app ? app.container : undefined;
const legacyRender = 'render' in app ? app.render : undefined;

const render = getRender(appInstanceId, appContent, legacyRender);

// 第一次加载设置应用可见区域 dom 结构
// 确保每次应用加载前容器 dom 结构已经设置完毕
render({ element: initialAppWrapperElement, loading: true, container: initialContainer }, 'loading');

const initialAppWrapperGetter = getAppWrapperGetter(
appInstanceId,
!!legacyRender,
strictStyleIsolation,
scopedCSS,
() => initialAppWrapperElement,
);

let global = globalContext;
let mountSandbox = () => Promise.resolve();
let unmountSandbox = () => Promise.resolve();
const useLooseSandbox = typeof sandbox === 'object' && !!sandbox.loose;
const speedySandbox = typeof sandbox === 'object' && !!sandbox.speedy;
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;
}

const {
beforeUnmount = [],
afterUnmount = [],
afterMount = [],
beforeMount = [],
beforeLoad = [],
} = mergeWith({}, getAddOns(global, assetPublicPath), lifeCycles, (v1, v2) => concat(v1 ?? [], v2 ?? []));

await execHooksChain(toArray(beforeLoad), app, global);

// get the lifecycle hooks from module exports
const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox, {
scopedGlobalVariables: speedySandbox ? trustedGlobals : [],
});
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>;

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 }),
// 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;
}

工具方法

error

这里就是声明了一个Error类而已

1
2
3
4
5
export class QiankunError extends Error {
constructor(message: string) {
super(`[qiankun]: ${message}`);
}
}

errorHandler

暴露一个方法去添加全局的错误处理监听函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @author Kuitos
* @since 2020-02-21
*/

export { addErrorHandler, removeErrorHandler } from 'single-spa';

export function addGlobalUncaughtErrorHandler(errorHandler: OnErrorEventHandlerNonNull): void {
window.addEventListener('error', errorHandler);
window.addEventListener('unhandledrejection', errorHandler);
}

export function removeGlobalUncaughtErrorHandler(errorHandler: (...args: any[]) => any) {
window.removeEventListener('error', errorHandler);
window.removeEventListener('unhandledrejection', errorHandler);
}

utils

这里的只列举函数签名及其作用,具体代码实现就不贴在这里了

  • function toArray(array: T | T[]): T[]
  • function sleep(ms: number)
  • function nextTask(cb: () => void): void
  • function isConstructable(fn: () => any | FunctionConstructor)
  • function isCallable(fn: any)
  • function isPropertyFrozen(target: any, p?: PropertyKey): boolean
  • function isBoundedFunction(fn: CallableFunction)
  • const qiankunHeadTagName = ‘qiankun-head’;
  • function getDefaultTplWrapper(name: string)
  • function getWrapperId(name: string)
  • const nativeGlobal = new Function(‘return this’)()
  • const genAppInstanceIdByName = (appName: string): string
  • function validateExportLifecycle(exports: any)
  • function performanceGetEntriesByName(markName: string, type?: string)
  • function performanceMark(markName: string)
  • function performanceMeasure(measureName: string, markName: string)
  • function isEnableScopedCSS(sandbox: FrameworkConfiguration[‘sandbox’])
  • function getXPathForElement(el: Node, document: Document): string | void

qiankun api

这一部分是qiankun对外暴露和使用的主要api

工具方法引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { noop } from 'lodash';
import type { ParcelConfigObject } from 'single-spa';
import { mountRootParcel, registerApplication, start as startSingleSpa } from 'single-spa';
import type {
FrameworkConfiguration,
FrameworkLifeCycles,
LoadableApp,
MicroApp,
ObjectType,
RegistrableApp,
} from './interfaces';
import type { ParcelConfigObjectGetter } from './loader';
import { loadApp } from './loader';
import { doPrefetchStrategy } from './prefetch';
import { Deferred, getContainerXPath, toArray } from './utils';

let microApps: Array<RegistrableApp<Record<string, unknown>>> = [];

export let frameworkConfiguration: FrameworkConfiguration = {};

let started = false;
const defaultUrlRerouteOnly = true;

const frameworkStartedDefer = new Deferred<void>();

const autoDowngradeForLowVersionBrowser = (configuration: FrameworkConfiguration): FrameworkConfiguration => {
const { sandbox, singular } = configuration;
if (sandbox) {
if (!window.Proxy) {
console.warn('[qiankun] Miss window.Proxy, proxySandbox will degenerate into snapshotSandbox');

if (singular === false) {
console.warn(
'[qiankun] Setting singular as false may cause unexpected behavior while your browser not support window.Proxy',
);
}

return { ...configuration, sandbox: typeof sandbox === 'object' ? { ...sandbox, loose: true } : { loose: true } };
}
}

return configuration;
};

registerMicroApps

利用single-spa的接口registerApplication来注册子应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
export function registerMicroApps<T extends ObjectType>(
apps: Array<RegistrableApp<T>>,
lifeCycles?: FrameworkLifeCycles<T>,
) {
// Each app only needs to be registered once
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;

registerApplication({
name,
app: async () => {
loader(true);
await frameworkStartedDefer.promise;

const { mount, ...otherMicroAppConfigs } = (
await loadApp({ name, props, ...appConfig }, frameworkConfiguration, lifeCycles)
)();

return {
mount: [async () => loader(true), ...toArray(mount), async () => loader(false)],
...otherMicroAppConfigs,
};
},
activeWhen: activeRule,
customProps: props,
});
});
}

loadMicroApp

这个函数的功能就是调用loader的loadApp,然后使用single-spa的startSingleSpa去启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
export function loadMicroApp<T extends ObjectType>(
app: LoadableApp<T>,
configuration?: FrameworkConfiguration & { autoStart?: boolean },
lifeCycles?: FrameworkLifeCycles<T>,
): MicroApp {
const { props, name } = app;

const container = 'container' in app ? app.container : undefined;
// Must compute the container xpath at beginning to keep it consist around app running
// If we compute it every time, the container dom structure most probably been changed and result in a different xpath value
const containerXPath = getContainerXPath(container);
const appContainerXPathKey = `${name}-${containerXPath}`;

let microApp: MicroApp;
const wrapParcelConfigForRemount = (config: ParcelConfigObject): ParcelConfigObject => {
let microAppConfig = config;
if (container) {
if (containerXPath) {
const containerMicroApps = containerMicroAppsMap.get(appContainerXPathKey);
if (containerMicroApps?.length) {
const mount = [
async () => {
// While there are multiple micro apps mounted on the same container, we must wait until the prev instances all had unmounted
// Otherwise it will lead some concurrent issues
const prevLoadMicroApps = containerMicroApps.slice(0, containerMicroApps.indexOf(microApp));
const prevLoadMicroAppsWhichNotBroken = prevLoadMicroApps.filter(
(v) => v.getStatus() !== 'LOAD_ERROR' && v.getStatus() !== 'SKIP_BECAUSE_BROKEN',
);
await Promise.all(prevLoadMicroAppsWhichNotBroken.map((v) => v.unmountPromise));
},
...toArray(microAppConfig.mount),
];

microAppConfig = {
...config,
mount,
};
}
}
}

return {
...microAppConfig,
// empty bootstrap hook which should not run twice while it calling from cached micro app
bootstrap: () => Promise.resolve(),
};
};

/**
* using name + container xpath as the micro app instance id,
* it means if you rendering a micro app to a dom which have been rendered before,
* the micro app would not load and evaluate its lifecycles again
*/
const memorizedLoadingFn = async (): Promise<ParcelConfigObject> => {
const userConfiguration = autoDowngradeForLowVersionBrowser(
configuration ?? { ...frameworkConfiguration, singular: false },
);
const { $$cacheLifecycleByAppName } = userConfiguration;

if (container) {
// using appName as cache for internal experimental scenario
if ($$cacheLifecycleByAppName) {
const parcelConfigGetterPromise = appConfigPromiseGetterMap.get(name);
if (parcelConfigGetterPromise) return wrapParcelConfigForRemount((await parcelConfigGetterPromise)(container));
}

if (containerXPath) {
const parcelConfigGetterPromise = appConfigPromiseGetterMap.get(appContainerXPathKey);
if (parcelConfigGetterPromise) return wrapParcelConfigForRemount((await parcelConfigGetterPromise)(container));
}
}

const parcelConfigObjectGetterPromise = loadApp(app, userConfiguration, lifeCycles);

if (container) {
if ($$cacheLifecycleByAppName) {
appConfigPromiseGetterMap.set(name, parcelConfigObjectGetterPromise);
} else if (containerXPath) appConfigPromiseGetterMap.set(appContainerXPathKey, parcelConfigObjectGetterPromise);
}

return (await parcelConfigObjectGetterPromise)(container);
};

if (!started && configuration?.autoStart !== false) {
// We need to invoke start method of single-spa as the popstate event should be dispatched while the main app calling pushState/replaceState automatically,
// but in single-spa it will check the start status before it dispatch popstate
// see https://github.com/single-spa/single-spa/blob/f28b5963be1484583a072c8145ac0b5a28d91235/src/navigation/navigation-events.js#L101
// ref https://github.com/umijs/qiankun/pull/1071
startSingleSpa({ urlRerouteOnly: frameworkConfiguration.urlRerouteOnly ?? defaultUrlRerouteOnly });
}

microApp = mountRootParcel(memorizedLoadingFn, { domElement: document.createElement('div'), ...props });

if (container) {
if (containerXPath) {
// Store the microApps which they mounted on the same container
const microAppsRef = containerMicroAppsMap.get(appContainerXPathKey) || [];
microAppsRef.push(microApp);
containerMicroAppsMap.set(appContainerXPathKey, microAppsRef);

const cleanup = () => {
const index = microAppsRef.indexOf(microApp);
microAppsRef.splice(index, 1);
// @ts-ignore
microApp = null;
};

// gc after unmount
microApp.unmountPromise.then(cleanup).catch(cleanup);
}
}

return microApp;
}

start

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export function start(opts: FrameworkConfiguration = {}) {
frameworkConfiguration = { prefetch: true, singular: true, sandbox: true, ...opts };
const {
prefetch,
sandbox,
singular,
urlRerouteOnly = defaultUrlRerouteOnly,
...importEntryOpts
} = frameworkConfiguration;

if (prefetch) {
doPrefetchStrategy(microApps, prefetch, importEntryOpts);
}

frameworkConfiguration = autoDowngradeForLowVersionBrowser(frameworkConfiguration);

startSingleSpa({ urlRerouteOnly });
started = true;

frameworkStartedDefer.resolve();
}

总结

总的来说,内部的api分工如下:

  • prefetch提供了预加载所有子应用资源的方法,
  • loader提供了根据子应用的入口去加载资源,执行入口文件,然后将结果html挂载到一个新建的div上
  • qiankun对外暴露的api主要是两个
    • registerMicroApps主要是调用sigle-spa的register接口去注册子应用
    • loadMicroApp会调用loader中的方法去加载子应用,loader内部其实最终还是调用了single-spa的load方法,只是做了一些沙箱的隔离,css的隔离等。