生命周期:load 和 unload
本节讲解 single-spa 中的生命周期 load 和 unload 函数的原理。在 single-spa 中 load 是在 bootstrap、mount 阶段之前,注册应用后的阶段。此阶段的主要工作是加载并检验 load 配置、校验配置和钩子函数、规整钩子函数等。unload 是在 unmount 之后 unregister 之前的阶段。
# 目录
# app 是什么?
因为上一章中是从宏观上管理微应用,我们只需要知道微应用是一个 object 接口。在这一章中,因为我们要详细探讨微应用的生命周期,这必然要涉及到对微应用中某些属性的操作,因此在探讨 toLoadPromise
之前,先来看下 app 是什么?因为 s-spa 不是 typescript 编写的,我们在文档和代码中还原 app 的原状。以内部命名为准。
参数 | 描述 |
---|---|
name | App name that single-spa will register and reference this application with, and will be labelled with in dev tools. |
loadApp | Application object or a function that returns the resolved application (Promise or not) |
activeWhen | Can be a path prefix which will match every URL starting with this path, an activity function (as described in the simple arguments) or an array containing both of them. |
customProps | Will be passed to the application during each lifecycle method. |
loadErrorTime | 微应用 load 错误的时间点 |
loadPromise | 微应用 load 的 promise |
status | 微应用的状态 |
parcels | |
devtools | devtools 的配置 |
提示
加粗的参数未用户会配置的参数,其余为内部使用参数。
# toLoadPromise
这个函数的结构很复杂,先来探讨下其结构:
function toLoadPromise(app) {
// P0
return Promise.resolve().then(() => {
// ......
// P1
return (app.loadPromise = Promise.resolve().then(() => {
// ......
// P2
return app;
})).catch(err => {
// ......
// P3
return app;
} )
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Promise 的类型定义参考:qiankun: loadApp 加载微应用
- P0:返回
Promise.resolve().then
的 return,即Promise<app>
。 - P1:P1 的返回就是 P0 的返回。
- P2/P3:P2/P3 的返回就是
Promise<app>
。
MDN:Promise.resolve()
This function flattens nested layers of promise-like objects (e.g. a promise that resolves to a promise that resolves to something) into a single layer.
Promise.resolve()
会将 Promise 进行 flat,但是这里使用嵌套的 Promise.resolve
来返回 Promise<app>
,目的是为了是让 toLoadPromise,即返回 Promise<app>
这个操作成为 Promise,希望它能够在微任务里执行,提升执行效率。关于 Promise 模拟微任务可参见:30 分钟看懂 React 框架原理。
结合此函数在 reroute
和 performAppChanges
里调用,执行的频率较高,这么做也就可以理解了。
下面来具体看下源码:
export function toLoadPromise(app) {
// load app 返回 loadPromise,Promise.resolve() 会将嵌套的 promise 摊平
return Promise.resolve().then(() => {
// 如果 loadPromise 已经存在,直接返回,不用再生成 loadPromise,执行 load 过程
if (app.loadPromise) {
return app.loadPromise;
}
// app.status 必须是 NOT_LOADED 或者 LOAD_ERROR
if (app.status !== NOT_LOADED && app.status !== LOAD_ERROR) {
return app;
}
// 状态修改为 LOADING_SOURCE_CODE,这是因为这 LOADING_SOURCE_CODE 阶段主要目前的加载应用配置对象
// 配置中包含应用生命周期钩子(加载源码并不是 load 阶段,这部分由外界完成,
// 如 qiankun 就自定义的加载源代码这块以实现 prefecth 等增强功能)
app.status = LOADING_SOURCE_CODE;
let appOpts, isUserErr;
// 将 loadPromise 保存在 app 上以
return (app.loadPromise = Promise.resolve()
.then(() => {
// load app promise,规整传递给 loadApp 的参数
const loadPromise = app.loadApp(getProps(app));
// 判断是否是 promise,如果不是 promise 就会报错
// 参见 registerMicroApps 中 app 使用 async 返回对象
if (!smellsLikeAPromise(loadPromise)) {
isUserErr = true;
// throw Error() ......
}
return loadPromise.then((val) => {
app.loadErrorTime = null;
appOpts = val;
let validationErrMessage, validationErrCode;
// loadPromise 必须返回 object
if (typeof appOpts !== "object") {
validationErrCode = 34;
}
// 有 bootstrap 钩子,但是钩子不合法
if (
// ES Modules don't have the Object prototype
Object.prototype.hasOwnProperty.call(appOpts, "bootstrap") &&
!validLifecycleFn(appOpts.bootstrap)
) {
validationErrCode = 35;
}
// mount 钩子必须有,但是钩子不合法
if (!validLifecycleFn(appOpts.mount)) {
validationErrCode = 36;
}
// unmount 钩子必须有,但是钩子不合法
if (!validLifecycleFn(appOpts.unmount)) {
validationErrCode = 37;
}
// 判断 app 是 parcel 还是 application,根据 appOpts.unmountThisParcel 判断
const type = objectType(appOpts);
if (validationErrCode) {
// ......
handleAppError(validationErrMessage, app, SKIP_BECAUSE_BROKEN);
return app;
}
// 状态由 LOADING_SOURCE_CODE 更新为 NOT_BOOTSTRAPPED
app.status = NOT_BOOTSTRAPPED;
// 规整 bootstrap 钩子,将钩子转成 钩子数组,并且返回 promise reduce pipeline
// 以下类比,注意这里只是返回 promise reduce pipeline,并没有执行
app.bootstrap = flattenFnArray(appOpts, "bootstrap");
app.mount = flattenFnArray(appOpts, "mount");
app.unmount = flattenFnArray(appOpts, "unmount");
app.unload = flattenFnArray(appOpts, "unload");
// 规整 timeouts 的配置,并且跟默认配置合并
app.timeouts = ensureValidAppTimeouts(appOpts.timeouts);
// load 过程完毕,删除 app.loadPromise, 这个 app.loadPromise 相当于一个互斥锁
delete app.loadPromise;
return app;
});
})
.catch((err) => {
delete app.loadPromise;
let newStatus;
// 出错,如果是用户配置问题,将状态置为 SKIP_BECAUSE_BROKEN,否则将状态置为 LOAD_ERROR
if (isUserErr) {
newStatus = SKIP_BECAUSE_BROKEN;
} else {
newStatus = LOAD_ERROR;
app.loadErrorTime = new Date().getTime();
}
// 初始应用错误
handleAppError(err, app, newStatus);
return app;
}));
});
}
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
先从整体上总结这个函数的作用:
- 函数返回 loadPromise 即,
Promise<app>
,在注册应用之后,mount 应用之前,此函数将 load 应用。 - load 应用主要做如下工作:调用 app.loadApp 加载并检验 load 配置、校验配置和钩子函数、规整钩子函数(转成数组、将钩子数组包装成 reduce pipeline)、错误处理。
- 此过程中状态为
LOADING_SOURCE_CODE
(校验 app.loadPromise 和 app.status 之后) 和NOT_BOOTSTRAPPED
(配置和钩子函数检验完毕之后)。
参考:
- Applications API | single-spa | registerApplication (opens new window)
- Promise.resolve() - JavaScript | MDN (opens new window)
# smellsLikeAPromise
判断是否是 Promise。
export function smellsLikeAPromise(promise) {
return (
promise &&
typeof promise.then === "function" &&
typeof promise.catch === "function"
);
}
2
3
4
5
6
7
为什么不用 p instanceof Promise
呢?我们注意到很多源码中(Vue3 等)都使用上述的方式判断 promise,而非使用 instanceof
判断。这是因为 instanceof
仅可判断由 ES6 实现的 Promise,而对于很多自己实现的 Promise,包括 polyfill 中实现的 Promise,这种方法可能就没法很好判断。而上述的判断是基于 ES 的实现规范的判断方法,这种判断方法更为普遍。
# flattenFnArray
export function flattenFnArray(appOrParcel, lifecycle) {
let fns = appOrParcel[lifecycle] || [];
fns = Array.isArray(fns) ? fns : [fns];
if (fns.length === 0) {
fns = [() => Promise.resolve()];
}
const type = objectType(appOrParcel);
const name = toName(appOrParcel);
return function (props) {
// promise reduce pipeline
// 执行钩子数组中所有的钩子,并且将最后 promise 返回
return fns.reduce((resultPromise, fn, index) => {
return resultPromise.then(() => {
const thisPromise = fn(props);
return smellsLikeAPromise(thisPromise)
? thisPromise
: Promise.reject(/** ...... **/)
});
}, Promise.resolve());
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
数组中的钩子函数将依次在微任务里执行,知道返回最后一个钩子执行的 promise。
关于 reduce pipeline,在中也 qiankun: loadApp 加载微应用运用了这个技巧。
# toUnloadPromise
toUnloadPromise 函数卸载微应用。
export function toUnloadPromise(app) {
return Promise.resolve().then(() => {
const unloadInfo = appsToUnload[toName(app)];
// 如果已经 unload 没有排队,直接返回,因为 unloadApplication 没有被调用
if (!unloadInfo) {
/* No one has called unloadApplication for this app,
*/
return app;
}
// 如果状态已经是 NOT_LOADED,直接 finishUnloadingApp
if (app.status === NOT_LOADED) {
/* This app is already unloaded. We just need to clean up
* anything that still thinks we need to unload the app.
*/
finishUnloadingApp(app, unloadInfo);
return app;
}
// 可能是 unloadApplication 和 reroute 同时想要 unload 应用
// 等待已经 unload 执行完毕
if (app.status === UNLOADING) {
/* Both unloadApplication and reroute want to unload this app.
* It only needs to be done once, though.
*/
return unloadInfo.promise.then(() => app);
}
// 如果应用状态不是 NOT_MOUNTED 或者 LOAD_ERROR,没有办法 unload,直接返回
// 需要先 unmount 到 NOT_MOUNTED 状态才能 unload
if (app.status !== NOT_MOUNTED && app.status !== LOAD_ERROR) {
/* The app cannot be unloaded until it is unmounted.
*/
return app;
}
const unloadPromise =
app.status === LOAD_ERROR
? Promise.resolve()
: reasonableTime(app, "unload");
app.status = UNLOADING;
// 如果不是 LOAD_ERROR 状态就执行 unload 的钩子
return unloadPromise
.then(() => {
// 执行成功后 finishUnloadingApp
finishUnloadingApp(app, unloadInfo);
return app;
})
.catch((err) => {
errorUnloadingApp(app, unloadInfo, err);
return app;
});
});
}
function finishUnloadingApp(app, unloadInfo) {
// 从 unload 队列中删除
delete appsToUnload[toName(app)];
// 删除生命周期
// Unloaded apps don't have lifecycles
delete app.bootstrap;
delete app.mount;
delete app.unmount;
delete app.unload;
// 更新状态为 NOT_LOADED
app.status = NOT_LOADED;
// resolve unloadInfo promise,完成整个 unload 过程
/* resolve the promise of whoever called unloadApplication.
* This should be done after all other cleanup/bookkeeping
*/
unloadInfo.resolve();
}
function errorUnloadingApp(app, unloadInfo, err) {
delete appsToUnload[toName(app)];
// Unloaded apps don't have lifecycles
delete app.bootstrap;
delete app.mount;
delete app.unmount;
delete app.unload;
handleAppError(err, app, SKIP_BECAUSE_BROKEN);
// 将错误 reject 给外部
unloadInfo.reject(err);
}
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
使用队列的方式来 unload 应用是因为 unloadApplication 和 reroute 可能会同时想要 unload 应用。
这个函数的核心作用是:
- 调用 unload 钩子,将应用 unload。
- 删除应用的生命周期,并且将队列中 unload 任务的 promise 进行 resolve 或者 reject。