generator-mico-cli 0.2.4 → 0.2.6
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/src/app.tsx +1 -70
- package/generators/micro-react/templates/apps/layout/src/common/micro-prefetch.ts +108 -0
- package/generators/micro-react/templates/apps/layout/src/common/request/index.ts +2 -2
- package/generators/micro-react/templates/apps/layout/src/common/route-guard.ts +31 -2
- package/generators/micro-react/templates/apps/layout/src/common/theme.ts +1 -1
- package/generators/micro-react/templates/apps/layout/src/common/upload/oss.ts +1 -1
- package/generators/micro-react/templates/apps/layout/src/common/upload/types.ts +1 -1
- package/generators/micro-react/templates/apps/layout/src/common/uploadFiles.ts +1 -1
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/micro-app-manager.ts +88 -18
- package/generators/micro-react/templates/apps/layout/src/constants/index.ts +63 -4
- package/generators/subapp-react/templates/homepage/src/app.tsx +4 -24
- package/generators/subapp-react/templates/homepage/src/global.less +4 -4
- package/package.json +1 -1
|
@@ -13,13 +13,12 @@ import { extractRoutes, getWindowMenus } from './common/menu';
|
|
|
13
13
|
import { ensureSsoSession } from './common/request/sso';
|
|
14
14
|
import {
|
|
15
15
|
clearMicroAppProps,
|
|
16
|
-
getAppNameFromEntry,
|
|
17
16
|
type IMicroAppProps,
|
|
18
17
|
setMicroAppProps,
|
|
19
18
|
} from './common/micro';
|
|
20
19
|
import { initTheme } from './common/theme';
|
|
21
20
|
import MicroAppLoader from './components/MicroAppLoader';
|
|
22
|
-
import { NO_AUTH_ROUTE_LIST } from '
|
|
21
|
+
import { NO_AUTH_ROUTE_LIST } from '@/constants';
|
|
23
22
|
import './global.less';
|
|
24
23
|
|
|
25
24
|
// ==================== qiankun 全局错误处理 ====================
|
|
@@ -76,74 +75,6 @@ if (typeof window !== 'undefined') {
|
|
|
76
75
|
});
|
|
77
76
|
}
|
|
78
77
|
|
|
79
|
-
// ==================== 微应用预加载 ====================
|
|
80
|
-
// 预加载所有微应用资源,避免快速切换时的竞态条件
|
|
81
|
-
// 当资源已预加载时,loadMicroApp 的异步加载会快速完成,减少容器不存在的错误
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* 是否启用微应用预加载
|
|
85
|
-
* - 可通过 URL 参数 ?prefetch=false 禁用(方便调试加载时序问题)
|
|
86
|
-
* - 可通过 localStorage.setItem('DISABLE_MICRO_APP_PREFETCH', 'true') 禁用
|
|
87
|
-
* - 默认启用
|
|
88
|
-
*/
|
|
89
|
-
const isPrefetchEnabled = (): boolean => {
|
|
90
|
-
if (typeof window === 'undefined') return false;
|
|
91
|
-
|
|
92
|
-
// URL 参数优先级最高
|
|
93
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
94
|
-
const prefetchParam = urlParams.get('prefetch');
|
|
95
|
-
if (prefetchParam === 'false') {
|
|
96
|
-
console.log('[App] Prefetch disabled via URL parameter');
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// localStorage 开关
|
|
101
|
-
if (localStorage.getItem('DISABLE_MICRO_APP_PREFETCH') === 'true') {
|
|
102
|
-
console.log('[App] Prefetch disabled via localStorage');
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return true;
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const prefetchMicroApps = () => {
|
|
110
|
-
if (!isPrefetchEnabled()) {
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
const menus = getWindowMenus();
|
|
116
|
-
const routes = extractRoutes(menus);
|
|
117
|
-
|
|
118
|
-
// 筛选出所有微应用路由
|
|
119
|
-
// 使用 getAppNameFromEntry 生成 name,与 loadMicroApp 保持一致,确保预加载缓存命中
|
|
120
|
-
const microApps = routes
|
|
121
|
-
.filter((route) => route.loadType === 'microapp' && route.entry)
|
|
122
|
-
.map((route) => ({
|
|
123
|
-
name: getAppNameFromEntry(route.entry!),
|
|
124
|
-
entry: route.entry!,
|
|
125
|
-
}));
|
|
126
|
-
|
|
127
|
-
if (microApps.length > 0) {
|
|
128
|
-
console.log('[App] Prefetching micro apps:', microApps);
|
|
129
|
-
prefetchApps(microApps);
|
|
130
|
-
}
|
|
131
|
-
} catch (error) {
|
|
132
|
-
console.warn('[App] Failed to prefetch micro apps:', error);
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
// 在页面加载后预加载微应用
|
|
137
|
-
if (typeof window !== 'undefined') {
|
|
138
|
-
// 使用 requestIdleCallback 在浏览器空闲时预加载,避免影响首屏渲染
|
|
139
|
-
if ('requestIdleCallback' in window) {
|
|
140
|
-
window.requestIdleCallback(() => prefetchMicroApps(), { timeout: 3000 });
|
|
141
|
-
} else {
|
|
142
|
-
// 降级方案:延迟 1 秒后预加载
|
|
143
|
-
setTimeout(prefetchMicroApps, 1000);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
78
|
// ==================== 微前端共享依赖 ====================
|
|
148
79
|
// 将公共库暴露到 window,供子应用复用,避免重复打包
|
|
149
80
|
// 子应用通过 externals 配置引用这些全局变量
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 微应用预加载管理
|
|
3
|
+
*
|
|
4
|
+
* 策略:当前子应用挂载成功后,再逐个预加载其他子应用
|
|
5
|
+
* 避免与当前加载的子应用竞争网络带宽
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { prefetchApps } from 'qiankun';
|
|
9
|
+
import { extractRoutes, getWindowMenus } from './menu';
|
|
10
|
+
import { getAppNameFromEntry } from './micro';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 是否启用微应用预加载
|
|
14
|
+
* - 可通过 URL 参数 ?prefetch=false 禁用(方便调试加载时序问题)
|
|
15
|
+
* - 可通过 localStorage.setItem('DISABLE_MICRO_APP_PREFETCH', 'true') 禁用
|
|
16
|
+
* - 默认启用
|
|
17
|
+
*/
|
|
18
|
+
export const isPrefetchEnabled = (): boolean => {
|
|
19
|
+
if (typeof window === 'undefined') return false;
|
|
20
|
+
|
|
21
|
+
// URL 参数优先级最高
|
|
22
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
23
|
+
const prefetchParam = urlParams.get('prefetch');
|
|
24
|
+
if (prefetchParam === 'false') {
|
|
25
|
+
console.log('[Prefetch] Disabled via URL parameter');
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// localStorage 开关
|
|
30
|
+
if (localStorage.getItem('DISABLE_MICRO_APP_PREFETCH') === 'true') {
|
|
31
|
+
console.log('[Prefetch] Disabled via localStorage');
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return true;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** 已预加载的应用 entry 集合 */
|
|
39
|
+
const prefetchedApps = new Set<string>();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 获取所有需要预加载的微应用(排除已预加载的)
|
|
43
|
+
*/
|
|
44
|
+
export const getMicroAppsForPrefetch = (
|
|
45
|
+
excludeEntry?: string,
|
|
46
|
+
): Array<{ name: string; entry: string }> => {
|
|
47
|
+
try {
|
|
48
|
+
const menus = getWindowMenus();
|
|
49
|
+
const routes = extractRoutes(menus);
|
|
50
|
+
|
|
51
|
+
return routes
|
|
52
|
+
.filter(
|
|
53
|
+
(route) =>
|
|
54
|
+
route.loadType === 'microapp' &&
|
|
55
|
+
route.entry &&
|
|
56
|
+
route.entry !== excludeEntry &&
|
|
57
|
+
!prefetchedApps.has(route.entry),
|
|
58
|
+
)
|
|
59
|
+
.map((route) => ({
|
|
60
|
+
name: getAppNameFromEntry(route.entry!),
|
|
61
|
+
entry: route.entry!,
|
|
62
|
+
}));
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.warn('[Prefetch] Failed to get micro apps:', error);
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 预加载微应用(低优先级,在浏览器空闲时并发预加载)
|
|
71
|
+
* @param currentEntry 当前正在加载的应用 entry,会被排除
|
|
72
|
+
*/
|
|
73
|
+
export const prefetchMicroAppsLowPriority = (currentEntry?: string): void => {
|
|
74
|
+
if (!isPrefetchEnabled()) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const apps = getMicroAppsForPrefetch(currentEntry);
|
|
79
|
+
if (apps.length === 0) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 标记为已预加载,避免重复
|
|
84
|
+
apps.forEach((app) => prefetchedApps.add(app.entry));
|
|
85
|
+
|
|
86
|
+
console.log(
|
|
87
|
+
'[Prefetch] Will prefetch micro apps:',
|
|
88
|
+
apps.map((a) => a.name),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// 使用 requestIdleCallback 确保在浏览器空闲时执行,不阻塞当前渲染
|
|
92
|
+
const doPrefetch = () => {
|
|
93
|
+
prefetchApps(apps); // qiankun 会并发预加载所有应用
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
if ('requestIdleCallback' in window) {
|
|
97
|
+
window.requestIdleCallback(doPrefetch, { timeout: 5000 });
|
|
98
|
+
} else {
|
|
99
|
+
setTimeout(doPrefetch, 100);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 标记应用已加载(避免重复预加载)
|
|
105
|
+
*/
|
|
106
|
+
export const markAppAsPrefetched = (entry: string): void => {
|
|
107
|
+
prefetchedApps.add(entry);
|
|
108
|
+
};
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { request as rawRequest } from '@umijs/max';
|
|
18
18
|
import { setStoredAuthToken } from '../auth/auth-manager';
|
|
19
|
-
import {
|
|
19
|
+
import { NO_AUTH_ROUTE_LIST } from '@/constants';
|
|
20
20
|
|
|
21
21
|
// 配置相关
|
|
22
22
|
import {
|
|
@@ -64,7 +64,7 @@ initDefaultInterceptors(isFetchingToken, addToPendingQueue);
|
|
|
64
64
|
* 判断当前路由是否跳过认证
|
|
65
65
|
*/
|
|
66
66
|
const shouldSkipAuth = (): boolean => {
|
|
67
|
-
return
|
|
67
|
+
return NO_AUTH_ROUTE_LIST.includes(location.pathname);
|
|
68
68
|
};
|
|
69
69
|
|
|
70
70
|
/**
|
|
@@ -57,6 +57,9 @@ let isInUserInteraction = false;
|
|
|
57
57
|
/** 交互结束时间(用于 grace period) */
|
|
58
58
|
let interactionEndTime = 0;
|
|
59
59
|
|
|
60
|
+
/** 当前用户交互开始时间(用于判断意图是否在当前交互期间设置的) */
|
|
61
|
+
let currentInteractionStartTime = 0;
|
|
62
|
+
|
|
60
63
|
/** 调试模式 */
|
|
61
64
|
const DEBUG = true;
|
|
62
65
|
|
|
@@ -180,8 +183,33 @@ export function getIntentDebugInfo(): object {
|
|
|
180
183
|
* 判断是否应该拦截此次导航
|
|
181
184
|
*/
|
|
182
185
|
function shouldIntercept(targetPath: string): boolean {
|
|
183
|
-
|
|
186
|
+
const targetBase = getBasePath(targetPath);
|
|
187
|
+
|
|
188
|
+
// 1. 在用户交互上下文中
|
|
184
189
|
if (isInInteractionContext()) {
|
|
190
|
+
// 检查是否有有效意图,且意图是在当前用户交互期间由 setUserIntent 设置的
|
|
191
|
+
// 只有这种情况才需要检查匹配,以防止微应用在 mount 过程中推送错误的路由
|
|
192
|
+
//
|
|
193
|
+
// 关键区分:
|
|
194
|
+
// - 意图在当前交互之前设置(如页面加载时):允许用户的新导航覆盖旧意图
|
|
195
|
+
// - 意图在当前交互期间由 setUserIntent 设置:正在进行微应用切换,需要保护
|
|
196
|
+
if (isIntentValid() && !currentIntent!.fromInteraction) {
|
|
197
|
+
// 检查意图是否在当前交互期间设置的
|
|
198
|
+
const isIntentFromCurrentInteraction = currentIntent!.timestamp >= currentInteractionStartTime;
|
|
199
|
+
if (isIntentFromCurrentInteraction) {
|
|
200
|
+
const intentBase = getBasePath(currentIntent!.path);
|
|
201
|
+
if (targetBase !== intentBase) {
|
|
202
|
+
warn('❌ 拦截不匹配意图的导航(交互上下文中):', {
|
|
203
|
+
targetPath,
|
|
204
|
+
targetBase,
|
|
205
|
+
intentPath: currentIntent!.path,
|
|
206
|
+
intentBase,
|
|
207
|
+
});
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// 目标匹配意图、无有效意图、或意图是旧的,允许导航并更新意图
|
|
185
213
|
log('用户交互触发导航:', targetPath);
|
|
186
214
|
currentIntent = {
|
|
187
215
|
path: targetPath,
|
|
@@ -198,7 +226,6 @@ function shouldIntercept(targetPath: string): boolean {
|
|
|
198
226
|
}
|
|
199
227
|
|
|
200
228
|
// 3. 有有效意图,检查目标路径是否匹配
|
|
201
|
-
const targetBase = getBasePath(targetPath);
|
|
202
229
|
const intentBase = getBasePath(currentIntent!.path);
|
|
203
230
|
|
|
204
231
|
if (targetBase === intentBase) {
|
|
@@ -273,6 +300,8 @@ if (typeof window !== 'undefined') {
|
|
|
273
300
|
|
|
274
301
|
log('用户交互开始:', event.type);
|
|
275
302
|
isInUserInteraction = true;
|
|
303
|
+
// 记录交互开始时间,用于判断意图是否在当前交互期间设置的
|
|
304
|
+
currentInteractionStartTime = Date.now();
|
|
276
305
|
|
|
277
306
|
// 在当前事件循环结束后重置标记
|
|
278
307
|
// 使用 setTimeout 0 确保同步代码执行完毕
|
|
@@ -3,7 +3,7 @@ import type { UploadProps } from '@arco-design/web-react';
|
|
|
3
3
|
import { Message } from '@arco-design/web-react';
|
|
4
4
|
import type { UploadItem } from '@arco-design/web-react/es/Upload';
|
|
5
5
|
import SparkMD5 from 'spark-md5';
|
|
6
|
-
import type { TDirCategory } from '
|
|
6
|
+
import type { TDirCategory } from '@/constants';
|
|
7
7
|
import type {
|
|
8
8
|
OssUploadError,
|
|
9
9
|
UploadLifecycleEvent,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ! 这个函数不能放到 ./helpers.ts 中,因为 ./request/index.ts 从 ./helpers.ts 导入了 getFromStorage, safeParseJSON,如果放到 ./helpers.ts 中,会导致循环引入
|
|
2
2
|
// ! 开发态下,发生循环引入时, mako 不会有编译时报错,但运行时会报错
|
|
3
3
|
|
|
4
|
-
import type { TDirCategory } from '
|
|
4
|
+
import type { TDirCategory } from '@/constants';
|
|
5
5
|
import { uploadToOss } from './upload/oss';
|
|
6
6
|
|
|
7
7
|
/** 上传文件到 OSS 并获取 URL */
|
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import type { MicroApp } from 'qiankun';
|
|
17
|
-
import { loadMicroApp
|
|
17
|
+
import { loadMicroApp } from 'qiankun';
|
|
18
18
|
import { microAppLogger } from '@/common/logger';
|
|
19
19
|
// 导入路由守卫(会自动初始化)
|
|
20
20
|
import { refreshUserIntent, setUserIntent } from '@/common/route-guard';
|
|
21
|
+
// 导入低优先级预加载函数
|
|
22
|
+
import { markAppAsPrefetched, prefetchMicroAppsLowPriority } from '@/common/micro-prefetch';
|
|
21
23
|
|
|
22
24
|
// ============================================================================
|
|
23
25
|
// 类型定义
|
|
@@ -96,6 +98,27 @@ function activateContainer(container: HTMLElement, target: HTMLElement): void {
|
|
|
96
98
|
container.style.cssText = 'display: block; width: 100%; height: 100%;';
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
/**
|
|
102
|
+
* 安全地更新微应用 props
|
|
103
|
+
* 避免 single-spa 错误 #32(Cannot update parcel because it is not mounted)
|
|
104
|
+
* @see https://single-spa.js.org/error/?code=32
|
|
105
|
+
*/
|
|
106
|
+
async function safeUpdate(microApp: MicroApp, props: Record<string, unknown>): Promise<void> {
|
|
107
|
+
try {
|
|
108
|
+
const status = microApp.getStatus();
|
|
109
|
+
if (status !== 'MOUNTED') {
|
|
110
|
+
microAppLogger.log('safeUpdate: skipped, status =', status);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// update 可能返回 Promise,需要 await 以捕获异步错误
|
|
114
|
+
await microApp.update?.(props);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
// 捕获错误但不抛出,避免 unhandled rejection 导致页面崩溃
|
|
117
|
+
// 使用 error 级别确保异常可见,便于排查问题
|
|
118
|
+
microAppLogger.error('safeUpdate: caught error:', err);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
99
122
|
function deactivateContainer(container: HTMLElement): void {
|
|
100
123
|
document.body.appendChild(container);
|
|
101
124
|
container.classList.remove(CSS_CLASS.active);
|
|
@@ -167,10 +190,12 @@ class MicroAppManager {
|
|
|
167
190
|
microAppLogger.log('Already mounted, updating props only');
|
|
168
191
|
const cached = this.appCache.get(config.name);
|
|
169
192
|
if (cached && cached.microApp.getStatus() === 'MOUNTED') {
|
|
170
|
-
cached.microApp
|
|
193
|
+
safeUpdate(cached.microApp, config.props);
|
|
171
194
|
if (cached.container.parentElement !== config.target) {
|
|
172
195
|
activateContainer(cached.container, config.target);
|
|
173
196
|
}
|
|
197
|
+
// 通知组件当前状态(重要:组件可能在 cleanup 后重新挂载,需要同步状态)
|
|
198
|
+
this.updateState({ loading: false, error: null, mounted: true });
|
|
174
199
|
}
|
|
175
200
|
return;
|
|
176
201
|
}
|
|
@@ -195,7 +220,7 @@ class MicroAppManager {
|
|
|
195
220
|
if (this.currentAppName && this.state === 'mounted') {
|
|
196
221
|
const cached = this.appCache.get(this.currentAppName);
|
|
197
222
|
if (cached && cached.microApp.getStatus() === 'MOUNTED') {
|
|
198
|
-
cached.microApp
|
|
223
|
+
safeUpdate(cached.microApp, props);
|
|
199
224
|
}
|
|
200
225
|
}
|
|
201
226
|
}
|
|
@@ -212,7 +237,7 @@ class MicroAppManager {
|
|
|
212
237
|
await this.safeUnmount(instance.microApp);
|
|
213
238
|
instance.container.remove();
|
|
214
239
|
} catch (err) {
|
|
215
|
-
microAppLogger.
|
|
240
|
+
microAppLogger.error('Clear cache error for', name, err);
|
|
216
241
|
}
|
|
217
242
|
}
|
|
218
243
|
this.appCache.clear();
|
|
@@ -273,15 +298,6 @@ class MicroAppManager {
|
|
|
273
298
|
this.updateState({ loading: true, error: null, mounted: false });
|
|
274
299
|
|
|
275
300
|
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
301
|
if (this.shouldAbort(request.name, mySeq)) {
|
|
286
302
|
console.log('🔍[路由调试] ⚠️ Aborted before load', { name: request.name, mySeq, operationSeq: this.operationSeq });
|
|
287
303
|
this.state = 'idle';
|
|
@@ -323,11 +339,13 @@ class MicroAppManager {
|
|
|
323
339
|
await withTimeout(appInstance.microApp.mount(), MOUNT_TIMEOUT, '子应用挂载超时');
|
|
324
340
|
console.log('🔍[路由调试] 缓存实例 mount 完成', { name: request.name, status: appInstance.microApp.getStatus() });
|
|
325
341
|
}
|
|
326
|
-
// 如果 status === 'MOUNTED'
|
|
342
|
+
// 如果 status === 'MOUNTED',则无需操作(已在 switchTo 入口处处理)
|
|
327
343
|
|
|
328
344
|
// 刷新意图,保护 mount 成功后的短暂窗口期
|
|
329
345
|
refreshUserIntent();
|
|
330
346
|
|
|
347
|
+
// 关键:在调用 update 之前检查是否需要 abort
|
|
348
|
+
// 这可以避免在即将被 unmount 的应用上调用 update,从而防止 single-spa error #32
|
|
331
349
|
if (this.shouldAbort(request.name, mySeq)) {
|
|
332
350
|
console.log('🔍[路由调试] ⚠️ Aborted (cached) after mount', { name: request.name });
|
|
333
351
|
await this.safeUnmount(appInstance.microApp);
|
|
@@ -337,6 +355,13 @@ class MicroAppManager {
|
|
|
337
355
|
return;
|
|
338
356
|
}
|
|
339
357
|
|
|
358
|
+
// mount 完成且不需要 abort,安全地调用 update 同步路由
|
|
359
|
+
// qiankun 的 mount() 使用的是创建实例时的原始 props,可能包含过期的 routePath
|
|
360
|
+
if (status === 'BOOTSTRAPPING' || status === 'NOT_MOUNTED') {
|
|
361
|
+
await safeUpdate(appInstance.microApp, request.props);
|
|
362
|
+
console.log('🔍[路由调试] 缓存实例 props 已更新', { name: request.name });
|
|
363
|
+
}
|
|
364
|
+
|
|
340
365
|
this.currentAppName = request.name;
|
|
341
366
|
this.state = 'mounted';
|
|
342
367
|
// 不立即清除意图,让它自然过期(5秒)
|
|
@@ -344,6 +369,10 @@ class MicroAppManager {
|
|
|
344
369
|
console.log('🔍[路由调试] ✅ 缓存实例挂载成功', { name: request.name, qiankunName: appInstance.qiankunName, status: appInstance.microApp.getStatus() });
|
|
345
370
|
this.updateState({ loading: false, error: null, mounted: true });
|
|
346
371
|
|
|
372
|
+
// 挂载成功后,标记当前应用已加载,并触发其他应用的低优先级预加载
|
|
373
|
+
markAppAsPrefetched(request.entry);
|
|
374
|
+
prefetchMicroAppsLowPriority(request.entry);
|
|
375
|
+
|
|
347
376
|
} else {
|
|
348
377
|
// 创建新实例
|
|
349
378
|
// 使用唯一的 qiankun 实例名称,避免 qiankun 内部状态冲突
|
|
@@ -389,7 +418,17 @@ class MicroAppManager {
|
|
|
389
418
|
|
|
390
419
|
if (this.shouldAbort(request.name, mySeq)) {
|
|
391
420
|
console.log('🔍[路由调试] ⚠️ Aborted after loadPromise', { name: request.name, mySeq, operationSeq: this.operationSeq });
|
|
392
|
-
//
|
|
421
|
+
// 关键修复:loadPromise 完成后 qiankun 会自动开始 mount
|
|
422
|
+
// 必须等待 mountPromise 完成后再 unmount,否则会触发 single-spa 错误 #32
|
|
423
|
+
// 参考:https://github.com/single-spa/single-spa/issues/1184
|
|
424
|
+
try {
|
|
425
|
+
console.log('🔍[路由调试] Abort: 等待 mountPromise 完成...', { status: microApp.getStatus() });
|
|
426
|
+
await withTimeout(microApp.mountPromise, MOUNT_TIMEOUT, 'Mount timeout during abort');
|
|
427
|
+
console.log('🔍[路由调试] Abort: mountPromise 完成,执行 unmount', { status: microApp.getStatus() });
|
|
428
|
+
await this.safeUnmount(microApp);
|
|
429
|
+
} catch (abortErr) {
|
|
430
|
+
microAppLogger.error('Abort cleanup error:', abortErr);
|
|
431
|
+
}
|
|
393
432
|
deactivateContainer(container);
|
|
394
433
|
this.state = 'idle';
|
|
395
434
|
if (this.pendingRequest) this.processRequest();
|
|
@@ -403,6 +442,8 @@ class MicroAppManager {
|
|
|
403
442
|
// 刷新意图,保护 mount 成功后的短暂窗口期
|
|
404
443
|
refreshUserIntent();
|
|
405
444
|
|
|
445
|
+
// 关键:在调用 update 之前检查是否需要 abort
|
|
446
|
+
// 这可以避免在即将被 unmount 的应用上调用 update,从而防止 single-spa error #32
|
|
406
447
|
if (this.shouldAbort(request.name, mySeq)) {
|
|
407
448
|
console.log('🔍[路由调试] ⚠️ Aborted after mountPromise', { name: request.name, mySeq, operationSeq: this.operationSeq });
|
|
408
449
|
// 实例已在 loadPromise 后缓存,这里只需 unmount 和 deactivate
|
|
@@ -413,6 +454,11 @@ class MicroAppManager {
|
|
|
413
454
|
return;
|
|
414
455
|
}
|
|
415
456
|
|
|
457
|
+
// mount 完成且不需要 abort,安全地调用 update 同步路由
|
|
458
|
+
// 由于子应用 mount 生命周期不再调用 syncRoute,需要通过 update 来同步路由
|
|
459
|
+
await safeUpdate(microApp, request.props);
|
|
460
|
+
console.log('🔍[路由调试] 新实例 props 已更新', { name: request.name });
|
|
461
|
+
|
|
416
462
|
// 实例已在 loadPromise 后缓存,这里不需要重复缓存
|
|
417
463
|
|
|
418
464
|
this.currentAppName = request.name;
|
|
@@ -421,6 +467,10 @@ class MicroAppManager {
|
|
|
421
467
|
// 这样可以防止被 abort 的子应用在后台执行时修改路由
|
|
422
468
|
console.log('🔍[路由调试] ✅ 新实例挂载成功', { name: request.name, qiankunName, status: microApp.getStatus() });
|
|
423
469
|
this.updateState({ loading: false, error: null, mounted: true });
|
|
470
|
+
|
|
471
|
+
// 挂载成功后,标记当前应用已加载,并触发其他应用的低优先级预加载
|
|
472
|
+
markAppAsPrefetched(request.entry);
|
|
473
|
+
prefetchMicroAppsLowPriority(request.entry);
|
|
424
474
|
}
|
|
425
475
|
|
|
426
476
|
if (this.pendingRequest) {
|
|
@@ -465,7 +515,7 @@ class MicroAppManager {
|
|
|
465
515
|
await this.safeUnmount(appInstance.microApp);
|
|
466
516
|
deactivateContainer(appInstance.container);
|
|
467
517
|
} catch (err) {
|
|
468
|
-
microAppLogger.
|
|
518
|
+
microAppLogger.error('Deactivate error:', err);
|
|
469
519
|
}
|
|
470
520
|
}
|
|
471
521
|
|
|
@@ -476,11 +526,31 @@ class MicroAppManager {
|
|
|
476
526
|
|
|
477
527
|
private async safeUnmount(microApp: MicroApp): Promise<void> {
|
|
478
528
|
try {
|
|
479
|
-
|
|
529
|
+
const status = microApp.getStatus();
|
|
530
|
+
microAppLogger.log('safeUnmount: current status =', status);
|
|
531
|
+
|
|
532
|
+
// 处理正在挂载的情况:等待 mountPromise 完成后再 unmount
|
|
533
|
+
// 这避免了 single-spa 错误 #32(unmount 一个正在 mounting 的应用)
|
|
534
|
+
if (status === 'MOUNTING' || status === 'BOOTSTRAPPING') {
|
|
535
|
+
microAppLogger.log('safeUnmount: waiting for mountPromise...');
|
|
536
|
+
try {
|
|
537
|
+
await withTimeout(microApp.mountPromise, MOUNT_TIMEOUT, 'Mount timeout during unmount');
|
|
538
|
+
} catch (mountErr) {
|
|
539
|
+
// 如果等待 mount 超时或失败,记录错误但继续尝试 unmount
|
|
540
|
+
microAppLogger.error('safeUnmount: mountPromise failed, continuing unmount:', mountErr);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// 重新检查状态,因为等待 mountPromise 后状态可能已变化
|
|
545
|
+
const currentStatus = microApp.getStatus();
|
|
546
|
+
if (currentStatus === 'MOUNTED') {
|
|
480
547
|
await withTimeout(microApp.unmount(), UNMOUNT_TIMEOUT, 'Unmount timeout');
|
|
548
|
+
microAppLogger.log('safeUnmount: unmount completed');
|
|
549
|
+
} else {
|
|
550
|
+
microAppLogger.log('safeUnmount: skipped unmount, status =', currentStatus);
|
|
481
551
|
}
|
|
482
552
|
} catch (err) {
|
|
483
|
-
microAppLogger.
|
|
553
|
+
microAppLogger.error('safeUnmount error:', err);
|
|
484
554
|
}
|
|
485
555
|
}
|
|
486
556
|
}
|
|
@@ -1,15 +1,74 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 应用常量定义
|
|
3
|
+
*/
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
|
-
*
|
|
6
|
+
* 路由路径常量
|
|
5
7
|
*/
|
|
6
|
-
export const
|
|
8
|
+
export const ROUTES = {
|
|
9
|
+
/** 登录页 */
|
|
7
10
|
LOGIN: '/user/login',
|
|
11
|
+
/** 注册页 */
|
|
12
|
+
REGISTER: '/user/register',
|
|
13
|
+
/** 注册结果页 */
|
|
14
|
+
REGISTER_RESULT: '/user/register-result',
|
|
15
|
+
/** 403 无权限页 */
|
|
8
16
|
FORBIDDEN: '/403',
|
|
17
|
+
/** 404 未找到页 */
|
|
9
18
|
NOT_FOUND: '/404',
|
|
10
19
|
} as const;
|
|
11
20
|
|
|
12
21
|
/**
|
|
13
22
|
* 无需认证的路由列表
|
|
14
23
|
*/
|
|
15
|
-
export const NO_AUTH_ROUTE_LIST =
|
|
24
|
+
export const NO_AUTH_ROUTE_LIST: string[] = [
|
|
25
|
+
ROUTES.LOGIN,
|
|
26
|
+
ROUTES.REGISTER,
|
|
27
|
+
ROUTES.REGISTER_RESULT,
|
|
28
|
+
ROUTES.FORBIDDEN,
|
|
29
|
+
ROUTES.NOT_FOUND,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 主题相关常量
|
|
34
|
+
*/
|
|
35
|
+
export const THEME = {
|
|
36
|
+
/** localStorage 存储键 */
|
|
37
|
+
STORAGE_KEY: 'audit-center-theme',
|
|
38
|
+
/** 默认主题 */
|
|
39
|
+
DEFAULT: 'light' as const,
|
|
40
|
+
/** 可选主题值 */
|
|
41
|
+
VALUES: ['light', 'dark'] as const,
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 时区相关常量
|
|
46
|
+
*/
|
|
47
|
+
export const TIMEZONE = {
|
|
48
|
+
/** localStorage 存储键(IANA 时区,如 Asia/Shanghai) */
|
|
49
|
+
STORAGE_KEY: 'audit-center-timezone',
|
|
50
|
+
/** localStorage 存储键(用于展示的地区/名称,可选) */
|
|
51
|
+
REGION_STORAGE_KEY: 'audit-center-timezone-region',
|
|
52
|
+
} as const;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 在线状态相关常量
|
|
56
|
+
*/
|
|
57
|
+
export const PRESENCE = {
|
|
58
|
+
/** localStorage 存储键 */
|
|
59
|
+
STORAGE_KEY: 'audit-center-presence-status',
|
|
60
|
+
} as const;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 存储键常量
|
|
64
|
+
*/
|
|
65
|
+
export const STORAGE_KEYS = {
|
|
66
|
+
APP_INFO: 'appInfo',
|
|
67
|
+
IS_SUPERUSER: 'is_superuser',
|
|
68
|
+
GROUPS: 'groups',
|
|
69
|
+
} as const;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* OSS 上传目录分类
|
|
73
|
+
*/
|
|
74
|
+
export type TDirCategory = number;
|
|
@@ -11,25 +11,6 @@ import { history } from '@umijs/max';
|
|
|
11
11
|
import { appLogger } from './common/logger';
|
|
12
12
|
import { type IMicroAppProps, setMainAppProps } from './common/mainApp';
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
* @name 独立运行时加载主题
|
|
16
|
-
* @description 仅在开发环境且非 qiankun 环境中加载主题样式
|
|
17
|
-
*
|
|
18
|
-
* 实现原理:
|
|
19
|
-
* - 生产构建时 process.env.NODE_ENV !== 'development',整个 if 块被 tree-shake
|
|
20
|
-
* - 开发环境独立运行时加载主题,支持本地预览
|
|
21
|
-
* - 作为微应用运行时(无论开发还是生产),使用主应用的主题
|
|
22
|
-
*/
|
|
23
|
-
if (process.env.NODE_ENV === 'development') {
|
|
24
|
-
// 开发环境:运行时检测是否独立运行
|
|
25
|
-
if (typeof window !== 'undefined' && !window.__POWERED_BY_QIANKUN__) {
|
|
26
|
-
// 直接导入主题样式(开发环境下会被打包)
|
|
27
|
-
import('./styles/theme.less');
|
|
28
|
-
appLogger.log('Running standalone in dev mode, theme loaded');
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
// 生产环境:作为微应用时,主题由主应用注入,无需加载
|
|
32
|
-
|
|
33
14
|
/**
|
|
34
15
|
* @name 路由同步工具
|
|
35
16
|
* @description 处理主应用与子应用之间的路由同步,支持通配符路由模式
|
|
@@ -73,11 +54,10 @@ export const qiankun = {
|
|
|
73
54
|
// 保存主应用传递的 props,包括 request 实例
|
|
74
55
|
setMainAppProps(props);
|
|
75
56
|
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
57
|
+
// 注意:不在 mount 中调用 syncRoute
|
|
58
|
+
// 原因:当子应用从缓存复用时,mount 时的 props 可能是旧的(包含过期的 routePath)
|
|
59
|
+
// 主应用的 MicroAppManager 会在 mount 完成后立即调用 update() 传递正确的 props
|
|
60
|
+
// 路由同步完全由 update 生命周期处理,避免 mount 中的旧 routePath 覆盖正确的路由
|
|
81
61
|
},
|
|
82
62
|
|
|
83
63
|
/**
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* @description homepage 子应用的全局样式
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
@import '<%= packageScope %>/shared-styles
|
|
6
|
+
// 导入共享样式入口(包含 CSS 变量 + Less 变量 + Arco 覆盖样式)
|
|
7
|
+
// 注意:作为微前端子应用运行时,主应用会先加载这些样式
|
|
8
|
+
// 独立运行时,需要自己加载完整样式(包括 Arco 组件覆盖)
|
|
9
|
+
@import '<%= packageScope %>/shared-styles';
|
|
10
10
|
|
|
11
11
|
* {
|
|
12
12
|
box-sizing: border-box;
|