generator-mico-cli 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/generators/micro-react/templates/apps/layout/docs/feature-/345/276/256/345/211/215/347/253/257/346/250/241/345/274/217.md +90 -8
- package/generators/micro-react/templates/apps/layout/src/app.tsx +8 -3
- package/generators/micro-react/templates/apps/layout/src/common/micro/index.ts +34 -0
- package/generators/micro-react/templates/apps/layout/src/common/route-guard.ts +316 -0
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +59 -185
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/micro-app-manager.ts +493 -0
- package/generators/micro-react/templates/apps/layout/src/hooks/useRoutePermissionRefresh.ts +26 -23
- package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +55 -42
- package/generators/micro-react/templates/apps/layout/src/pages/403/index.tsx +6 -1
- package/package.json +1 -1
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/container-manager.ts +0 -334
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 微应用管理器 v10
|
|
3
|
+
*
|
|
4
|
+
* 核心设计:
|
|
5
|
+
* 1. 稳定容器:容器在 body 中创建,不受 React 生命周期影响
|
|
6
|
+
* 2. 容器移动:激活时移到目标元素内,停用时移回 body 隐藏
|
|
7
|
+
* 3. 实例缓存:每个 entry 只 loadMicroApp 一次,复用实例
|
|
8
|
+
* 4. 自动路由守卫:由 route-guard.ts 自动检测用户意图,无需手动标记
|
|
9
|
+
*
|
|
10
|
+
* v10 改进(相比 v9):
|
|
11
|
+
* - 移除手动 setUserIntent 机制,改为自动检测用户交互
|
|
12
|
+
* - 业务代码无需感知路由守卫,可使用任意常规路由跳转方式
|
|
13
|
+
* - 路由守卫逻辑抽离到独立模块 route-guard.ts
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { MicroApp } from 'qiankun';
|
|
17
|
+
import { loadMicroApp, prefetchApps } from 'qiankun';
|
|
18
|
+
import { microAppLogger } from '@/common/logger';
|
|
19
|
+
// 导入路由守卫(会自动初始化)
|
|
20
|
+
import { refreshUserIntent, setUserIntent } from '@/common/route-guard';
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// 类型定义
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
export interface MicroAppConfig {
|
|
27
|
+
name: string;
|
|
28
|
+
entry: string;
|
|
29
|
+
/** 目标挂载位置(用于移动容器) */
|
|
30
|
+
target: HTMLElement;
|
|
31
|
+
props: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface MicroAppState {
|
|
35
|
+
loading: boolean;
|
|
36
|
+
error: string | null;
|
|
37
|
+
mounted: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type StateChangeCallback = (state: MicroAppState) => void;
|
|
41
|
+
|
|
42
|
+
type ManagerState = 'idle' | 'loading' | 'mounted';
|
|
43
|
+
|
|
44
|
+
interface AppInstance {
|
|
45
|
+
microApp: MicroApp;
|
|
46
|
+
container: HTMLElement;
|
|
47
|
+
entry: string;
|
|
48
|
+
/** qiankun 实例名称(可能与缓存 key 不同,用于避免重复加载问题) */
|
|
49
|
+
qiankunName: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// 常量
|
|
54
|
+
// ============================================================================
|
|
55
|
+
|
|
56
|
+
const LOAD_TIMEOUT = 30000;
|
|
57
|
+
const MOUNT_TIMEOUT = 30000;
|
|
58
|
+
const UNMOUNT_TIMEOUT = 10000;
|
|
59
|
+
|
|
60
|
+
const CSS_CLASS = {
|
|
61
|
+
base: 'micro-app-stable-container',
|
|
62
|
+
hidden: 'micro-app-stable-container--hidden',
|
|
63
|
+
active: 'micro-app-stable-container--active',
|
|
64
|
+
} as const;
|
|
65
|
+
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// 工具函数
|
|
68
|
+
// ============================================================================
|
|
69
|
+
|
|
70
|
+
function withTimeout<T>(
|
|
71
|
+
promise: Promise<T>,
|
|
72
|
+
ms: number,
|
|
73
|
+
message: string,
|
|
74
|
+
): Promise<T> {
|
|
75
|
+
return Promise.race([
|
|
76
|
+
promise,
|
|
77
|
+
new Promise<T>((_, reject) => {
|
|
78
|
+
setTimeout(() => reject(new Error(message)), ms);
|
|
79
|
+
}),
|
|
80
|
+
]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function createStableContainer(appName: string): HTMLElement {
|
|
84
|
+
const container = document.createElement('div');
|
|
85
|
+
container.id = `micro-app-stable-${appName}`;
|
|
86
|
+
container.className = `${CSS_CLASS.base} ${CSS_CLASS.hidden}`;
|
|
87
|
+
container.style.cssText = 'display: none; position: absolute; left: -9999px;';
|
|
88
|
+
document.body.appendChild(container);
|
|
89
|
+
return container;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function activateContainer(container: HTMLElement, target: HTMLElement): void {
|
|
93
|
+
target.appendChild(container);
|
|
94
|
+
container.classList.remove(CSS_CLASS.hidden);
|
|
95
|
+
container.classList.add(CSS_CLASS.active);
|
|
96
|
+
container.style.cssText = 'display: block; width: 100%; height: 100%;';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function deactivateContainer(container: HTMLElement): void {
|
|
100
|
+
document.body.appendChild(container);
|
|
101
|
+
container.classList.remove(CSS_CLASS.active);
|
|
102
|
+
container.classList.add(CSS_CLASS.hidden);
|
|
103
|
+
container.style.cssText = 'display: none; position: absolute; left: -9999px;';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// MicroAppManager 类
|
|
108
|
+
// ============================================================================
|
|
109
|
+
|
|
110
|
+
class MicroAppManager {
|
|
111
|
+
private static instance: MicroAppManager;
|
|
112
|
+
|
|
113
|
+
/** 当前状态 */
|
|
114
|
+
private state: ManagerState = 'idle';
|
|
115
|
+
|
|
116
|
+
/** 已加载的应用实例缓存 */
|
|
117
|
+
private appCache = new Map<string, AppInstance>();
|
|
118
|
+
|
|
119
|
+
/** 当前激活的应用名称 */
|
|
120
|
+
private currentAppName: string | null = null;
|
|
121
|
+
|
|
122
|
+
/** 待处理的请求 */
|
|
123
|
+
private pendingRequest: MicroAppConfig | null = null;
|
|
124
|
+
|
|
125
|
+
/** 状态变化回调 */
|
|
126
|
+
private stateCallback: StateChangeCallback | null = null;
|
|
127
|
+
|
|
128
|
+
/** 操作序列号(用于检测过期操作) */
|
|
129
|
+
private operationSeq = 0;
|
|
130
|
+
|
|
131
|
+
private constructor() {
|
|
132
|
+
// 路由守卫已在 route-guard.ts 中自动初始化
|
|
133
|
+
microAppLogger.log('MicroAppManager initialized');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
static getInstance(): MicroAppManager {
|
|
137
|
+
if (!MicroAppManager.instance) {
|
|
138
|
+
MicroAppManager.instance = new MicroAppManager();
|
|
139
|
+
}
|
|
140
|
+
return MicroAppManager.instance;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ==========================================================================
|
|
144
|
+
// 公开 API
|
|
145
|
+
// ==========================================================================
|
|
146
|
+
|
|
147
|
+
setStateCallback(callback: StateChangeCallback | null): void {
|
|
148
|
+
console.log('🔍[路由调试] setStateCallback', { hasCallback: !!callback });
|
|
149
|
+
this.stateCallback = callback;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 切换到指定的微应用
|
|
154
|
+
*/
|
|
155
|
+
switchTo(config: MicroAppConfig): void {
|
|
156
|
+
microAppLogger.log('switchTo:', config.name, {
|
|
157
|
+
current: this.currentAppName,
|
|
158
|
+
state: this.state,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// 设置/刷新路由意图,确保在整个加载过程中拦截子应用的非预期路由操作
|
|
162
|
+
// 使用当前 URL pathname 作为意图路径
|
|
163
|
+
setUserIntent(window.location.pathname);
|
|
164
|
+
|
|
165
|
+
// 如果是当前已激活的应用,只更新 props
|
|
166
|
+
if (this.state === 'mounted' && this.currentAppName === config.name) {
|
|
167
|
+
microAppLogger.log('Already mounted, updating props only');
|
|
168
|
+
const cached = this.appCache.get(config.name);
|
|
169
|
+
if (cached && cached.microApp.getStatus() === 'MOUNTED') {
|
|
170
|
+
cached.microApp.update?.(config.props);
|
|
171
|
+
if (cached.container.parentElement !== config.target) {
|
|
172
|
+
activateContainer(cached.container, config.target);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 保存请求
|
|
179
|
+
this.pendingRequest = config;
|
|
180
|
+
|
|
181
|
+
// 根据状态处理
|
|
182
|
+
if (this.state === 'idle') {
|
|
183
|
+
this.processRequest();
|
|
184
|
+
} else if (this.state === 'mounted') {
|
|
185
|
+
this.updateState({ loading: true, error: null, mounted: false });
|
|
186
|
+
this.deactivateCurrentAndProcess();
|
|
187
|
+
} else {
|
|
188
|
+
// loading 状态,等待完成后处理
|
|
189
|
+
microAppLogger.log('Busy, request queued');
|
|
190
|
+
this.updateState({ loading: true, error: null, mounted: false });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
updateProps(props: Record<string, unknown>): void {
|
|
195
|
+
if (this.currentAppName && this.state === 'mounted') {
|
|
196
|
+
const cached = this.appCache.get(this.currentAppName);
|
|
197
|
+
if (cached && cached.microApp.getStatus() === 'MOUNTED') {
|
|
198
|
+
cached.microApp.update?.(props);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
cancel(): void {
|
|
204
|
+
microAppLogger.log('cancel');
|
|
205
|
+
this.pendingRequest = null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async clearCache(): Promise<void> {
|
|
209
|
+
microAppLogger.log('Clearing cache');
|
|
210
|
+
for (const [name, instance] of this.appCache) {
|
|
211
|
+
try {
|
|
212
|
+
await this.safeUnmount(instance.microApp);
|
|
213
|
+
instance.container.remove();
|
|
214
|
+
} catch (err) {
|
|
215
|
+
microAppLogger.warn('Clear cache error for', name, err);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
this.appCache.clear();
|
|
219
|
+
this.currentAppName = null;
|
|
220
|
+
this.state = 'idle';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
getDebugInfo(): object {
|
|
224
|
+
return {
|
|
225
|
+
state: this.state,
|
|
226
|
+
currentAppName: this.currentAppName,
|
|
227
|
+
pendingRequest: this.pendingRequest?.name ?? null,
|
|
228
|
+
cacheSize: this.appCache.size,
|
|
229
|
+
cachedApps: Array.from(this.appCache.entries()).map(([name, inst]) => ({
|
|
230
|
+
name,
|
|
231
|
+
qiankunName: inst.qiankunName,
|
|
232
|
+
entry: inst.entry,
|
|
233
|
+
status: inst.microApp.getStatus(),
|
|
234
|
+
})),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ==========================================================================
|
|
239
|
+
// 私有方法
|
|
240
|
+
// ==========================================================================
|
|
241
|
+
|
|
242
|
+
private updateState(state: MicroAppState): void {
|
|
243
|
+
console.log('🔍[路由调试] updateState', { state, hasCallback: !!this.stateCallback });
|
|
244
|
+
this.stateCallback?.(state);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private shouldAbort(currentName: string, mySeq: number): boolean {
|
|
248
|
+
if (mySeq !== this.operationSeq) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
if (this.pendingRequest && this.pendingRequest.name !== currentName) {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private async processRequest(): Promise<void> {
|
|
258
|
+
const request = this.pendingRequest;
|
|
259
|
+
this.pendingRequest = null;
|
|
260
|
+
|
|
261
|
+
if (!request) {
|
|
262
|
+
microAppLogger.log('No pending request');
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const mySeq = ++this.operationSeq;
|
|
267
|
+
console.log('🔍[路由调试] processRequest 开始', { name: request.name, seq: mySeq });
|
|
268
|
+
|
|
269
|
+
// 刷新意图,确保在整个加载过程中意图保持有效
|
|
270
|
+
refreshUserIntent();
|
|
271
|
+
|
|
272
|
+
this.state = 'loading';
|
|
273
|
+
this.updateState({ loading: true, error: null, mounted: false });
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
// 预加载资源
|
|
277
|
+
if (!this.appCache.has(request.name)) {
|
|
278
|
+
try {
|
|
279
|
+
prefetchApps([{ name: request.name, entry: request.entry }]);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
microAppLogger.warn('Prefetch failed:', error);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (this.shouldAbort(request.name, mySeq)) {
|
|
286
|
+
console.log('🔍[路由调试] ⚠️ Aborted before load', { name: request.name, mySeq, operationSeq: this.operationSeq });
|
|
287
|
+
this.state = 'idle';
|
|
288
|
+
if (this.pendingRequest) this.processRequest();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let appInstance = this.appCache.get(request.name);
|
|
293
|
+
|
|
294
|
+
if (appInstance) {
|
|
295
|
+
// 复用已有实例
|
|
296
|
+
console.log('🔍[路由调试] 复用缓存实例', { name: request.name, status: appInstance.microApp.getStatus() });
|
|
297
|
+
|
|
298
|
+
// 刷新意图
|
|
299
|
+
refreshUserIntent();
|
|
300
|
+
|
|
301
|
+
if (this.shouldAbort(request.name, mySeq)) {
|
|
302
|
+
console.log('🔍[路由调试] ⚠️ Aborted (cached) before activate', { name: request.name });
|
|
303
|
+
this.state = 'idle';
|
|
304
|
+
if (this.pendingRequest) this.processRequest();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
activateContainer(appInstance.container, request.target);
|
|
309
|
+
|
|
310
|
+
const status = appInstance.microApp.getStatus();
|
|
311
|
+
console.log('🔍[路由调试] 缓存实例状态', { name: request.name, qiankunName: appInstance.qiankunName, status });
|
|
312
|
+
|
|
313
|
+
// 根据状态决定如何挂载
|
|
314
|
+
if (status === 'BOOTSTRAPPING') {
|
|
315
|
+
// 实例在 loadPromise 后被缓存,但还未完成 mount
|
|
316
|
+
// 需要等待 mountPromise 完成
|
|
317
|
+
console.log('🔍[路由调试] 等待缓存实例 mountPromise...', { name: request.name });
|
|
318
|
+
await withTimeout(appInstance.microApp.mountPromise, MOUNT_TIMEOUT, '子应用挂载超时');
|
|
319
|
+
console.log('🔍[路由调试] 缓存实例 mountPromise 完成', { name: request.name, status: appInstance.microApp.getStatus() });
|
|
320
|
+
} else if (status === 'NOT_MOUNTED') {
|
|
321
|
+
// 实例之前被 unmount 过,需要重新 mount
|
|
322
|
+
console.log('🔍[路由调试] 开始 mount 缓存实例', { name: request.name });
|
|
323
|
+
await withTimeout(appInstance.microApp.mount(), MOUNT_TIMEOUT, '子应用挂载超时');
|
|
324
|
+
console.log('🔍[路由调试] 缓存实例 mount 完成', { name: request.name, status: appInstance.microApp.getStatus() });
|
|
325
|
+
}
|
|
326
|
+
// 如果 status === 'MOUNTED',则无需操作
|
|
327
|
+
|
|
328
|
+
// 刷新意图,保护 mount 成功后的短暂窗口期
|
|
329
|
+
refreshUserIntent();
|
|
330
|
+
|
|
331
|
+
if (this.shouldAbort(request.name, mySeq)) {
|
|
332
|
+
console.log('🔍[路由调试] ⚠️ Aborted (cached) after mount', { name: request.name });
|
|
333
|
+
await this.safeUnmount(appInstance.microApp);
|
|
334
|
+
deactivateContainer(appInstance.container);
|
|
335
|
+
this.state = 'idle';
|
|
336
|
+
if (this.pendingRequest) this.processRequest();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
this.currentAppName = request.name;
|
|
341
|
+
this.state = 'mounted';
|
|
342
|
+
// 不立即清除意图,让它自然过期(5秒)
|
|
343
|
+
// 这样可以防止被 abort 的子应用在后台执行时修改路由
|
|
344
|
+
console.log('🔍[路由调试] ✅ 缓存实例挂载成功', { name: request.name, qiankunName: appInstance.qiankunName, status: appInstance.microApp.getStatus() });
|
|
345
|
+
this.updateState({ loading: false, error: null, mounted: true });
|
|
346
|
+
|
|
347
|
+
} else {
|
|
348
|
+
// 创建新实例
|
|
349
|
+
// 使用唯一的 qiankun 实例名称,避免 qiankun 内部状态冲突
|
|
350
|
+
// 当应用被 abort 后重新加载时,qiankun 可能仍保留旧的内部状态
|
|
351
|
+
// 使用时间戳后缀确保每次 loadMicroApp 都是全新的实例
|
|
352
|
+
const qiankunName = `${request.name}__${Date.now()}`;
|
|
353
|
+
console.log('🔍[路由调试] 创建新实例', { name: request.name, qiankunName });
|
|
354
|
+
|
|
355
|
+
if (this.shouldAbort(request.name, mySeq)) {
|
|
356
|
+
this.state = 'idle';
|
|
357
|
+
if (this.pendingRequest) this.processRequest();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const container = createStableContainer(qiankunName);
|
|
362
|
+
activateContainer(container, request.target);
|
|
363
|
+
|
|
364
|
+
console.log('🔍[路由调试] 调用 loadMicroApp', { name: request.name, qiankunName, entry: request.entry });
|
|
365
|
+
const microApp = loadMicroApp(
|
|
366
|
+
{
|
|
367
|
+
name: qiankunName,
|
|
368
|
+
entry: request.entry,
|
|
369
|
+
container,
|
|
370
|
+
props: request.props,
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
sandbox: { strictStyleIsolation: false, experimentalStyleIsolation: true },
|
|
374
|
+
},
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
console.log('🔍[路由调试] 等待 loadPromise...', { name: request.name });
|
|
378
|
+
await withTimeout(microApp.loadPromise, LOAD_TIMEOUT, '子应用资源加载超时');
|
|
379
|
+
console.log('🔍[路由调试] loadPromise 完成', { name: request.name, status: microApp.getStatus() });
|
|
380
|
+
|
|
381
|
+
// 刷新意图,继续保护后续操作
|
|
382
|
+
refreshUserIntent();
|
|
383
|
+
|
|
384
|
+
// loadPromise 完成后立即缓存实例
|
|
385
|
+
// 即使后续被 abort,下次也能复用此实例,避免创建多个实例导致重复请求
|
|
386
|
+
appInstance = { microApp, container, entry: request.entry, qiankunName };
|
|
387
|
+
this.appCache.set(request.name, appInstance);
|
|
388
|
+
console.log('🔍[路由调试] 实例已缓存(loadPromise 后)', { name: request.name, qiankunName });
|
|
389
|
+
|
|
390
|
+
if (this.shouldAbort(request.name, mySeq)) {
|
|
391
|
+
console.log('🔍[路由调试] ⚠️ Aborted after loadPromise', { name: request.name, mySeq, operationSeq: this.operationSeq });
|
|
392
|
+
// 不销毁实例,只是 deactivate 容器,实例保留在缓存中供下次复用
|
|
393
|
+
deactivateContainer(container);
|
|
394
|
+
this.state = 'idle';
|
|
395
|
+
if (this.pendingRequest) this.processRequest();
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
console.log('🔍[路由调试] 等待 mountPromise...', { name: request.name, qiankunName, status: microApp.getStatus() });
|
|
400
|
+
await withTimeout(microApp.mountPromise, MOUNT_TIMEOUT, '子应用挂载超时');
|
|
401
|
+
console.log('🔍[路由调试] mountPromise 完成', { name: request.name, qiankunName, status: microApp.getStatus() });
|
|
402
|
+
|
|
403
|
+
// 刷新意图,保护 mount 成功后的短暂窗口期
|
|
404
|
+
refreshUserIntent();
|
|
405
|
+
|
|
406
|
+
if (this.shouldAbort(request.name, mySeq)) {
|
|
407
|
+
console.log('🔍[路由调试] ⚠️ Aborted after mountPromise', { name: request.name, mySeq, operationSeq: this.operationSeq });
|
|
408
|
+
// 实例已在 loadPromise 后缓存,这里只需 unmount 和 deactivate
|
|
409
|
+
await this.safeUnmount(microApp);
|
|
410
|
+
deactivateContainer(container);
|
|
411
|
+
this.state = 'idle';
|
|
412
|
+
if (this.pendingRequest) this.processRequest();
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// 实例已在 loadPromise 后缓存,这里不需要重复缓存
|
|
417
|
+
|
|
418
|
+
this.currentAppName = request.name;
|
|
419
|
+
this.state = 'mounted';
|
|
420
|
+
// 不立即清除意图,让它自然过期(5秒)
|
|
421
|
+
// 这样可以防止被 abort 的子应用在后台执行时修改路由
|
|
422
|
+
console.log('🔍[路由调试] ✅ 新实例挂载成功', { name: request.name, qiankunName, status: microApp.getStatus() });
|
|
423
|
+
this.updateState({ loading: false, error: null, mounted: true });
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (this.pendingRequest) {
|
|
427
|
+
this.deactivateCurrentAndProcess();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
} catch (err) {
|
|
431
|
+
microAppLogger.error('Error:', err);
|
|
432
|
+
|
|
433
|
+
if (mySeq !== this.operationSeq) return;
|
|
434
|
+
|
|
435
|
+
this.state = 'idle';
|
|
436
|
+
this.currentAppName = null;
|
|
437
|
+
|
|
438
|
+
if (this.pendingRequest) {
|
|
439
|
+
this.processRequest();
|
|
440
|
+
} else {
|
|
441
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
442
|
+
this.updateState({
|
|
443
|
+
loading: false,
|
|
444
|
+
error: message.includes('Failed to fetch')
|
|
445
|
+
? '无法连接到子应用服务,请检查子应用是否已启动'
|
|
446
|
+
: message,
|
|
447
|
+
mounted: false,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private async deactivateCurrentAndProcess(): Promise<void> {
|
|
454
|
+
if (!this.currentAppName) {
|
|
455
|
+
this.state = 'idle';
|
|
456
|
+
this.processRequest();
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const appInstance = this.appCache.get(this.currentAppName);
|
|
461
|
+
microAppLogger.log('Deactivating:', this.currentAppName);
|
|
462
|
+
|
|
463
|
+
if (appInstance) {
|
|
464
|
+
try {
|
|
465
|
+
await this.safeUnmount(appInstance.microApp);
|
|
466
|
+
deactivateContainer(appInstance.container);
|
|
467
|
+
} catch (err) {
|
|
468
|
+
microAppLogger.warn('Deactivate error:', err);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
this.currentAppName = null;
|
|
473
|
+
this.state = 'idle';
|
|
474
|
+
this.processRequest();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private async safeUnmount(microApp: MicroApp): Promise<void> {
|
|
478
|
+
try {
|
|
479
|
+
if (microApp.getStatus() === 'MOUNTED') {
|
|
480
|
+
await withTimeout(microApp.unmount(), UNMOUNT_TIMEOUT, 'Unmount timeout');
|
|
481
|
+
}
|
|
482
|
+
} catch (err) {
|
|
483
|
+
microAppLogger.warn('safeUnmount error:', err);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export const microAppManager = MicroAppManager.getInstance();
|
|
489
|
+
|
|
490
|
+
// 暴露到 window 供调试
|
|
491
|
+
if (typeof window !== 'undefined') {
|
|
492
|
+
(window as unknown as Record<string, unknown>).__MICRO_APP_MANAGER__ = microAppManager;
|
|
493
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useLocation, useModel } from '@umijs/max';
|
|
2
|
-
import
|
|
3
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
3
|
import { layoutLogger } from '@/common/logger';
|
|
5
4
|
import { NO_AUTH_ROUTE_LIST } from '@/constants';
|
|
6
5
|
|
|
@@ -21,49 +20,53 @@ export function useRoutePermissionRefresh() {
|
|
|
21
20
|
|
|
22
21
|
const isFirstRender = useRef(true);
|
|
23
22
|
const prevPathRef = useRef(location.pathname);
|
|
24
|
-
// 使用 ref 持久化 refresh,避免 debounce 实例重建
|
|
25
23
|
const refreshRef = useRef(refresh);
|
|
24
|
+
const timerRef = useRef<ReturnType<typeof setTimeout>>();
|
|
26
25
|
refreshRef.current = refresh;
|
|
27
26
|
|
|
28
|
-
const debouncedRefresh = useMemo(
|
|
29
|
-
() =>
|
|
30
|
-
debounce(async (pathname: string) => {
|
|
31
|
-
layoutLogger.log('Route changed, refreshing user info:', pathname);
|
|
32
|
-
try {
|
|
33
|
-
await refreshRef.current?.();
|
|
34
|
-
} finally {
|
|
35
|
-
setIsRefreshing(false);
|
|
36
|
-
}
|
|
37
|
-
}, 300),
|
|
38
|
-
[],
|
|
39
|
-
);
|
|
40
|
-
|
|
41
27
|
useEffect(() => {
|
|
42
|
-
//
|
|
28
|
+
// 跳过首次渲染
|
|
43
29
|
if (isFirstRender.current) {
|
|
44
30
|
isFirstRender.current = false;
|
|
45
31
|
return;
|
|
46
32
|
}
|
|
47
33
|
|
|
48
|
-
//
|
|
34
|
+
// 路径未变化
|
|
49
35
|
if (prevPathRef.current === location.pathname) {
|
|
50
36
|
return;
|
|
51
37
|
}
|
|
38
|
+
|
|
52
39
|
prevPathRef.current = location.pathname;
|
|
53
40
|
|
|
54
|
-
//
|
|
41
|
+
// 免认证路由不需要刷新
|
|
55
42
|
if (NO_AUTH_ROUTE_LIST.includes(location.pathname)) {
|
|
56
43
|
return;
|
|
57
44
|
}
|
|
58
45
|
|
|
59
|
-
//
|
|
46
|
+
// 清除之前的定时器
|
|
47
|
+
if (timerRef.current) {
|
|
48
|
+
clearTimeout(timerRef.current);
|
|
49
|
+
}
|
|
50
|
+
|
|
60
51
|
setIsRefreshing(true);
|
|
61
|
-
|
|
52
|
+
layoutLogger.log('Route changed, scheduling user info refresh:', location.pathname);
|
|
53
|
+
|
|
54
|
+
// 防抖 300ms
|
|
55
|
+
timerRef.current = setTimeout(async () => {
|
|
56
|
+
try {
|
|
57
|
+
await refreshRef.current?.();
|
|
58
|
+
layoutLogger.log('User info refreshed');
|
|
59
|
+
} finally {
|
|
60
|
+
setIsRefreshing(false);
|
|
61
|
+
}
|
|
62
|
+
}, 300);
|
|
62
63
|
|
|
63
64
|
return () => {
|
|
64
|
-
|
|
65
|
+
if (timerRef.current) {
|
|
66
|
+
clearTimeout(timerRef.current);
|
|
67
|
+
}
|
|
65
68
|
};
|
|
66
|
-
}, [location.pathname
|
|
69
|
+
}, [location.pathname]);
|
|
67
70
|
|
|
68
71
|
return { isRefreshing };
|
|
69
72
|
}
|