-
Notifications
You must be signed in to change notification settings - Fork 233
Expand file tree
/
Copy pathMicroApp.tsx.tpl
More file actions
291 lines (262 loc) · 9.47 KB
/
MicroApp.tsx.tpl
File metadata and controls
291 lines (262 loc) · 9.47 KB
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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
// @ts-ignore
// @ts-ignore
import { ErrorBoundary } from "@@/plugin-qiankun/ErrorBoundary";
import { getMasterOptions } from "@@/plugin-qiankun/masterOptions";
// @ts-ignore
import MicroAppLoader from "@@/plugin-qiankun/MicroAppLoader";
import {
BrowserHistoryBuildOptions,
HashHistoryBuildOptions,
MemoryHistoryBuildOptions,
} from "history-with-query";
import concat from "{{{ lodashPath }}}/concat";
import mergeWith from "{{{ lodashPath }}}/mergeWith";
import noop from "{{{ lodashPath }}}/noop";
import {
FrameworkConfiguration,
loadMicroApp,
MicroApp as MicroAppType,
prefetchApps,
} from "{{{ qiankunPath }}}";
import React, {
forwardRef,
Ref,
useEffect,
useImperativeHandle,
useRef,
useState,
} from "react";
// @ts-ignore
import { History, useModel } from "umi";
import { MasterOptions } from "./types";
const qiankunStateForSlaveModelNamespace = "@@qiankunStateForSlave";
type HashHistory = {
type?: "hash";
} & HashHistoryBuildOptions;
type BrowserHistory = {
type?: "browser";
} & BrowserHistoryBuildOptions;
type MemoryHistory = {
type?: "memory";
} & MemoryHistoryBuildOptions;
export type Props = {
name: string;
settings?: FrameworkConfiguration;
base?: string;
history?:
| "hash"
| "browser"
| "memory"
| HashHistory
| BrowserHistory
| MemoryHistory;
getMatchedBase?: () => string;
loader?: (loading: boolean) => React.ReactNode;
errorBoundary?: (error: any) => React.ReactNode;
onHistoryInit?: (history: History) => void;
autoSetLoading?: boolean;
autoCaptureError?: boolean;
// 仅开启 loader 时需要
wrapperClassName?: string;
className?: string;
} & Record<string, any>;
function unmountMicroApp(microApp?: MicroAppType) {
if (microApp) {
microApp.mountPromise.then(() => microApp.unmount());
}
}
let noneMounted = true;
export const MicroApp = forwardRef(
(componentProps: Props, componentRef: Ref<MicroAppType>) => {
const {
masterHistoryType,
apps = [],
lifeCycles: globalLifeCycles,
prefetch = true,
appNameKeyAlias = 'name',
prefetchThreshold = 5,
...globalSettings
} = getMasterOptions() as MasterOptions;
const {
settings: settingsFromProps = {},
loader,
errorBoundary,
lifeCycles,
wrapperClassName,
className,
...propsFromParams
} = componentProps;
// name 跟 appNameKeyAlias 这两个 key 同时存在时,优先使用 name,避免对存量应用造成 breaking change。
// 比如 appNameKeyAlias 配置是 id,但之前 id 正好作为普通的 props 使用过,如 <MicroApp name="app" id="123" />
// 正常场景会优先匹配 appNameKeyAlias 对应的字段,fallback 到 name,避免对已经使用 <MicroApp name="app" /> 的应用造成影响
const name = (componentProps.name && componentProps[appNameKeyAlias]) ? componentProps.name : (componentProps[appNameKeyAlias] || componentProps.name);
const isCurrentApp = (app: any) => app[appNameKeyAlias] === name || app.name === name;
const [loading, setLoading] = useState(true);
const [error, setError] = useState<any>(null);
// 未配置自定义 errorBoundary 且开启了 autoCaptureError 场景下,使用插件默认的 errorBoundary,否则使用自定义 errorBoundary
const microAppErrorBoundary =
errorBoundary ||
(propsFromParams.autoCaptureError
? (e) => <ErrorBoundary error={e} />
: null);
// 配置了 errorBoundary 才改 error 状态,否则直接往上抛异常
const setComponentError = (error: any) => {
if (microAppErrorBoundary) {
setError(error);
// error log 出来,不要吞
if (error) {
console.error(error);
}
} else if (error) {
throw error;
}
};
const containerRef = useRef<HTMLDivElement>();
const microAppRef = useRef<MicroAppType>();
const updatingPromise = useRef<Promise<any>>();
const updatingTimestamp = useRef(Date.now());
useImperativeHandle(componentRef, () => microAppRef.current);
const appConfig = apps.find((app: any) => isCurrentApp(app));
useEffect(() => {
if (!appConfig) {
setComponentError(
new Error(
`[@umijs/plugin-qiankun]: Can not find the configuration of ${name} app! Currently, only the following apps are configured:\n${JSON.stringify(apps, null, 2)}`
)
);
}
return noop;
}, []);
// 约定使用 src/app.ts/useQiankunStateForSlave 中的数据作为主应用透传给微应用的 props,优先级高于 propsFromConfig
const stateForSlave = (useModel || noop)(
qiankunStateForSlaveModelNamespace
);
const { entry, props: propsFromConfig = {} } = appConfig || {};
useEffect(() => {
setComponentError(null);
setLoading(true);
const configuration = {
globalContext: window,
...globalSettings,
...settingsFromProps,
};
microAppRef.current = loadMicroApp(
{
name,
entry,
container: containerRef.current!,
props: {
...propsFromConfig,
...stateForSlave,
...propsFromParams,
globalRoutesInfo: {
masterHistoryType,
routes: globalSettings.routes
},
setLoading,
},
},
configuration,
mergeWith({}, globalLifeCycles, lifeCycles, (v1, v2) =>
concat(v1 ?? [], v2 ?? [])
)
);
// 当配置了 prefetch true 时,在第一个应用 mount 完成之后,再去预加载其他应用
if (prefetch && prefetch !== "all" && noneMounted) {
microAppRef.current?.mountPromise.then(() => {
if (noneMounted) {
if (Array.isArray(prefetch)) {
const specialPrefetchApps = apps.filter(
(app) => !isCurrentApp(app) && (prefetch.indexOf(app[appNameKeyAlias]) !== -1 || prefetch.indexOf(app.name) !== -1)
);
prefetchApps(specialPrefetchApps, configuration);
} else {
// 不能无脑全量 prefetch,需要有一个阈值
const otherNotMountedApps = apps.filter((app) => !isCurrentApp(app)).slice(0, prefetchThreshold);
prefetchApps(otherNotMountedApps, configuration);
}
noneMounted = false;
}
});
}
(["loadPromise", "bootstrapPromise", "mountPromise"] as const).forEach(
(key) => {
const promise = microAppRef.current?.[key];
promise.catch((e) => {
setComponentError(e);
setLoading(false);
});
}
);
return () => unmountMicroApp(microAppRef.current);
}, [name]);
useEffect(() => {
const microApp = microAppRef.current;
if (microApp) {
if (!updatingPromise.current) {
// 初始化 updatingPromise 为 microApp.mountPromise,从而确保后续更新是在应用 mount 完成之后
updatingPromise.current = microApp.mountPromise;
} else {
// 确保 microApp.update 调用是跟组件状态变更顺序一致的,且后一个微应用更新必须等待前一个更新完成
updatingPromise.current = updatingPromise.current.then(() => {
const canUpdate = (microApp?: MicroAppType) =>
microApp?.update && microApp.getStatus() === "MOUNTED";
if (canUpdate(microApp)) {
const props = {
...propsFromConfig,
...stateForSlave,
...propsFromParams,
globalRoutesInfo: {
masterHistoryType,
routes: globalSettings.routes
},
setLoading,
};
if (process.env.NODE_ENV === "development") {
if (Date.now() - updatingTimestamp.current < 200) {
console.warn(
`[@umijs/plugin-qiankun] It seems like microApp ${name} is updating too many times in a short time(200ms), you may need to do some optimization to avoid the unnecessary re-rendering.`
);
}
console.info(
`[@umijs/plugin-qiankun] MicroApp ${name} is updating with props: `,
props
);
updatingTimestamp.current = Date.now();
}
// 返回 microApp.update 形成链式调用
// @ts-ignore
return microApp.update(props);
}
return void 0;
});
}
}
return noop;
}, Object.values({ ...stateForSlave, ...propsFromParams }));
// 未配置自定义 loader 且开启了 autoSetLoading 场景下,使用插件默认的 loader,否则使用自定义 loader
const microAppLoader =
loader ||
(propsFromParams.autoSetLoading
? (loading) => <MicroAppLoader loading={loading} />
: null);
const wrapperStyle = { position: "relative" };
return Boolean(microAppLoader) || Boolean(microAppErrorBoundary) ? (
<div
style={wrapperStyle}
className={`${wrapperClassName || ''} qiankun-micro-app`}
>
{Boolean(microAppLoader) && microAppLoader(loading)}
{Boolean(microAppErrorBoundary) &&
Boolean(error) &&
microAppErrorBoundary(error)}
<div
ref={containerRef}
className={`${className || ''} qiankun-micro-app-container`}
/>
</div>
) : (
<div ref={containerRef} className={className} />
);
}
);