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.
@@ -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 './constants';
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 { ROUTES } from '../constants';
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 ROUTES.NO_AUTH_ROUTES.includes(location.pathname);
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
- // 1. 在用户交互上下文中,允许导航并更新意图
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 @@
3
3
  * 支持在 localStorage 中存储主题状态,并在页面加载时动态加载对应的主题
4
4
  */
5
5
 
6
- import { THEME } from './constants';
6
+ import { THEME } from '@/constants';
7
7
 
8
8
  export type ThemeMode = 'light' | 'dark';
9
9
 
@@ -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 '../constants';
6
+ import type { TDirCategory } from '@/constants';
7
7
  import type {
8
8
  OssUploadError,
9
9
  UploadLifecycleEvent,
@@ -1,4 +1,4 @@
1
- import type { TDirCategory } from '../constants';
1
+ import type { TDirCategory } from '@/constants';
2
2
 
3
3
  export interface UploadLifecycleEvent {
4
4
  stage: 'hashing' | 'signing' | 'uploading';
@@ -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 './constants';
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, prefetchApps } from 'qiankun';
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.update?.(config.props);
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.update?.(props);
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.warn('Clear cache error for', name, err);
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
- // 不销毁实例,只是 deactivate 容器,实例保留在缓存中供下次复用
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.warn('Deactivate error:', err);
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
- if (microApp.getStatus() === 'MOUNTED') {
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.warn('safeUnmount error:', err);
553
+ microAppLogger.error('safeUnmount error:', err);
484
554
  }
485
555
  }
486
556
  }
@@ -1,15 +1,74 @@
1
- export const DEFAULT_NAME = 'Umi Max';
1
+ /**
2
+ * 应用常量定义
3
+ */
2
4
 
3
5
  /**
4
- * 无需认证的路由路径
6
+ * 路由路径常量
5
7
  */
6
- export const NO_AUTH_ROUTES = {
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 = Object.values(NO_AUTH_ROUTES);
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
- // 初始路由同步:延迟到下一帧确保 React 组件已初始化
77
- if (props.routePath) {
78
- const routePath = props.routePath;
79
- requestAnimationFrame(() => syncRoute(routePath, 'initial route sync'));
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
- // 仅导入 Less 变量(不包含 CSS 变量定义,避免重复打包)
7
- // CSS 变量由主应用(layout)注入到 body 上
8
- // Less 变量通过 var(--xxx) 引用 CSS 变量,自动适配主题
9
- @import '<%= packageScope %>/shared-styles/variables-only';
6
+ // 导入共享样式入口(包含 CSS 变量 + Less 变量 + Arco 覆盖样式)
7
+ // 注意:作为微前端子应用运行时,主应用会先加载这些样式
8
+ // 独立运行时,需要自己加载完整样式(包括 Arco 组件覆盖)
9
+ @import '<%= packageScope %>/shared-styles';
10
10
 
11
11
  * {
12
12
  box-sizing: border-box;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "generator-mico-cli",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Yeoman generator for Mico CLI projects",
5
5
  "keywords": [
6
6
  "yeoman-generator",