single-spa 源码分析

在了解single-spa基本功能后,可以将其简单概括为:single-spa 的核心就是动态将子应用的资源文件插入到主应用中。那内部是如何管理子应用、如何做到子应用之间的动态切换。带着这些疑问探究 single-spa 源码。

源码主要分为三部分:app 应用、Navigation 路由、生命周期。

application

应用状态

为了更好的管理 app,每个app在运行期间都有自己的状态
每个应用在整个运行期间都有自己的状态,根据不同状态做对应的处理。

  • null : app 不存在
  • NOT_LOADED:已经注册还没加载
  • LOADING_SOURCE_CODE:正在加载 app 代码(registerApplication 的第二个参数)
  • NOT_BOOTSTRAPPED:已经加载还没启动,即未执行 app 的 bootstrap 生命周期函数
  • BOOTSTRAPPING:正在启动,执行 app 的 bootstrap 生命周期函数,只执行一次
  • NOT_MOUNTED:已经启动还没挂载
  • MOUNTING: 正在挂载,执行 app 的 mount 函数
  • MOUNTED:已经挂载
  • UNMOUNTING:正在移除挂载,执行 app 的 unmount 函数
  • UNLOADING: 正在卸载,还没完成
  • SKIP_BECAUSE_BROKEN:执行期间出错

注册应用

框架内部统一管理所有应用来进行应用之间的调度(应用的挂载卸载错误等处理),应用注册成功后,会放到内部统一管理应用的的数组 apps 里。
registerApplication:(appName,applicationOrLoadingFn,activeFn, customProps)

  • appName: 应用唯一标识。
  • applicationOrLoadingFn: 入口 js 文件,这就需要子应用做一些处理,需要打包成特殊的文件格式进行加载
  • activeFn:什么时候激活应用,根据 url 进行匹配。
  • customProps:给子应用传递的参数,例如登录信息权限控制等。

注册成功之后的单个 app 信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apps.push({
loadErrorTime: null,
name: appName,
loadImpl,
activeWhen: activityFn,
status: NOT_LOADED,
parcels: {},
devtools: {
overlays: {
options: {},
selectors: [],
}
},
customProps
});

除了注册传递的参数以外,single-spa 给每个 app 增加了状态 status。注册后的状态为 NOT_LOADED,表示已经注册但未加载。
注册成功后,会执行 reroute 方法,reroute 内部判断是否已经 start,如果未启动,则找到匹配的应用(不报错的、还未 load 的)进行预加载,为挂载做准备。如果已经启动,则取消已经挂载的,找到当前匹配的进行挂载,reroute 是整个 single-app 的核心,后面还会详细分析。

卸载应用

unregisterApplication 会去调用 unloadApplication,然后在 apps 里找到对应的 app 将其删除。
unloadApplication 里,会先去看当前应用是否有正在被 unload,如果存在则直接返回。否则需要先 unmount 之后再去 unload

1
2
3
4
5
window.addEventListener('hashchange', urlReroute);
window.addEventListener('popstate', urlReroute);
function urlReroute() {
reroute([], arguments)
}

全局监听了 hashchangepopstate 事件来拦截 url 的变化,在路由事件到达应用框架(Vue、React)之前做应用的挂载卸载处理,当触发这两个事件后,也会执行 reroute,同时带上事件参数传递给 reroute。下面重点分析一下 reroute。

reroute

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
/*
reroute 接收两个参数
pendingPromises:表示正在队列中等待执行的 reroute。reroute 在执行期间,可能会有多个 reroute 被调用(路由触发或者应用注册)。
eventArguments:路由事件触发的 event。只有路由改变才会有这个参数
*/
export function reroute(pendingPromises = [], eventArguments) {
// appChangeUnderway 是一个开关,用来表示 reroute 是否正处于执行期间。
// 如果正处于执行期间还会有 reroute 要执行,则会将 reroute 放入队列里等待执行
if (appChangeUnderway) {
return new Promise((resolve, reject) => {
peopleWaitingOnAppChange.push({
resolve,
reject,
eventArguments,
});
});
}

appChangeUnderway = true;
// wasNoOp 为 true 表示没有应用发生变更。
let wasNoOp = true;

if (isStarted()) {
return performAppChanges();
} else {
return loadApps();
}

function loadApps() {
return Promise.resolve().then(() => {
const loadPromises = getAppsToLoad().map(toLoadPromise);
// 获取要 load 的 app(未出错的命中的),如果有说明应用发生了变更
if (loadPromises.length > 0) {
wasNoOp = false;
}
// 执行 load 并且直接 finishUpAndReturn
// 并不进行 mount,因为未 start,此时进行预加载
// 即:页面上没有挂载该应用,但是会去请求对应的资源文件
// 不会去调用应用文件里的bootstrap、mount、unmount等生命周期
return Promise
.all(loadPromises)
.then(finishUpAndReturn)
.catch(err => {
callAllEventListeners();
throw err;
})
})
}

function performAppChanges() {
return Promise.resolve().then(() => {
window.dispatchEvent(new CustomEvent("single-spa:before-routing-event", getCustomEventDetail()));
const unloadPromises = getAppsToUnload().map(toUnloadPromise);

const unmountUnloadPromises = getAppsToUnmount()
.map(toUnmountPromise)
.map(unmountPromise => unmountPromise.then(toUnloadPromise));

const allUnmountPromises = unmountUnloadPromises.concat(unloadPromises);
// 获取要 unload 以及要 unmout 的 app,如果有表示应用发生了变更
// 如果此时触发的是子应用内部的路由,则此时 allUnmountPromises 为[],表示没有应用的卸载或挂载
if (allUnmountPromises.length > 0) {
wasNoOp = false;
}

const unmountAllPromise = Promise.all(allUnmountPromises);

const appsToLoad = getAppsToLoad();
// 在其他应用 unmounting 期间将需要 load 的 app 执行 load、bootstrap(并行优势),等所有的 unmounting 都结束之后去挂载当前的 app
const loadThenMountPromises = appsToLoad.map(app => {
return toLoadPromise(app)
.then(toBootstrapPromise)
.then(app => {
return unmountAllPromise
.then(() => toMountPromise(app))
})
})
// 如果有要挂载的 app,也说明应用发生了变更
if (loadThenMountPromises.length > 0) {
wasNoOp = false;
}
// 获取已经 bootstrap 要去 mount 的 app,同上,等到其他 app unmounting 之后执行挂载
const mountPromises = getAppsToMount()
.filter(appToMount => appsToLoad.indexOf(appToMount) < 0)
.map(appToMount => {
return toBootstrapPromise(appToMount)
.then(() => unmountAllPromise)
.then(() => toMountPromise(appToMount))
})
// 同上
if (mountPromises.length > 0) {
wasNoOp = false;
}
// 所有要卸载的执行完之后,执行回调
return unmountAllPromise
.catch(err => {
callAllEventListeners();
throw err;
})
.then(() => {
callAllEventListeners();

return Promise
.all(loadThenMountPromises.concat(mountPromises))
.catch(err => {
pendingPromises.forEach(promise => promise.reject(err));
throw err;
})
.then(() => finishUpAndReturn(false))
})

})
}

function finishUpAndReturn(callEventListeners=true) {
const returnValue = getMountedApps();

if (callEventListeners) {
callAllEventListeners();
}
pendingPromises.forEach(promise => promise.resolve(returnValue));

try {
const appChangeEventName = wasNoOp ? "single-spa:no-app-change": "single-spa:app-change";
window.dispatchEvent(new CustomEvent(appChangeEventName, getCustomEventDetail()));
window.dispatchEvent(new CustomEvent("single-spa:routing-event", getCustomEventDetail()));
} catch (err) {
setTimeout(() => {
throw err;
});
}
// 打开开关,执行队列里的 reroute
appChangeUnderway = false;
// 虽然批量执行所有等待的 reroute,但这个地方还是需要递归的执行,因为在执行 reroute 期间可能又会有 reroute 进来
if (peopleWaitingOnAppChange.length > 0) {
const nextPendingPromises = peopleWaitingOnAppChange;
peopleWaitingOnAppChange = [];
reroute(nextPendingPromises);
}

return returnValue;
}

function callAllEventListeners() {
pendingPromises.forEach(pendingPromise => {
callCapturedEventListeners(pendingPromise.eventArguments);
});

callCapturedEventListeners(eventArguments);
}

function getCustomEventDetail() {
const result = {detail: {}}

if (eventArguments && eventArguments[0]) {
result.detail.originalEvent = eventArguments[0]
}

return result
}
}

画个简易的流程图如下:
reroute

lifecycles

single-spa 的亮点除了顶层路由的设计,另一个亮点就是生命周期的设计。生命周期的设计使得主应用更好的控制子应用。整个生命周期状态变更如下:
lifecycle

app 出错的状态有两个:SKIP_BECAUSE_BROKENLOAD_ERRORSKIP_BECAUSE_BROKEN 表示在状态变更时出错,阻止往下个状态变更,LOAD_ERROR 表示加载错误,此时会记录当前的时间戳,当路由再次导航到对应用时还会尝试去加载(时间间隔大于200ms)。
挂载阶段出错时,在状态变成 SKIP_BECAUSE_BROKEN 之前需要先将状态变成 mounted,因为出错后要执行 toUnmountPromise 卸载应用,而 toUnmountPromise 会判断如果状态不是 MOUNTED 时会跳过。

single-spa-react

上面提到,在加载应用资源时,会去检查 app 的三个生命周期状态,single-spa 要求接入的应用都提供这三个生命周期,所以官方官方适配出了各个框架的工具。以single-spa-react 为例,react 应用的入口文件通过 single-spa-react 封装,暴露给 single-spa 三个生命周期函数,并且都是 Promise。

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
const defaultOpts = {
// required opts
React: null,
ReactDOM: null,
rootComponent: null,
loadRootComponent: null,
suppressComponentDidCatchWarning: false,
domElements: {},

// optional opts
domElementGetter: null,
parcelCanUpdate: true,
}
export default function singleSpaReact(userOpts) {
// ...
const opts = {
...defaultOpts,
...userOpts,
};
// ...
const lifecycles = {
bootstrap: bootstrap.bind(null, opts),
mount: mount.bind(null, opts),
unmount: unmount.bind(null, opts),
};
// ...

return lifecycles
}
  • bootstrap: 如果是 class 或者无状态组件,则直接返回。确保传入的是 React 根组件。
  • mount: 找到 single-spa 传递给应用的 DOM 节点(domElementGetter),执行 reactDomRender 进行渲染。
  • unmount: 从 DOM 中移除 React 应用。