generator-mico-cli 0.2.2 → 0.2.5

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.
Files changed (20) hide show
  1. 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
  2. package/generators/micro-react/templates/apps/layout/src/app.tsx +5 -69
  3. package/generators/micro-react/templates/apps/layout/src/common/micro/index.ts +34 -0
  4. package/generators/micro-react/templates/apps/layout/src/common/micro-prefetch.ts +108 -0
  5. package/generators/micro-react/templates/apps/layout/src/common/request/index.ts +2 -2
  6. package/generators/micro-react/templates/apps/layout/src/common/route-guard.ts +345 -0
  7. package/generators/micro-react/templates/apps/layout/src/common/theme.ts +1 -1
  8. package/generators/micro-react/templates/apps/layout/src/common/upload/oss.ts +1 -1
  9. package/generators/micro-react/templates/apps/layout/src/common/upload/types.ts +1 -1
  10. package/generators/micro-react/templates/apps/layout/src/common/uploadFiles.ts +1 -1
  11. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +59 -185
  12. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/micro-app-manager.ts +511 -0
  13. package/generators/micro-react/templates/apps/layout/src/constants/index.ts +63 -4
  14. package/generators/micro-react/templates/apps/layout/src/hooks/useRoutePermissionRefresh.ts +26 -23
  15. package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +55 -42
  16. package/generators/micro-react/templates/apps/layout/src/pages/403/index.tsx +6 -1
  17. package/generators/subapp-react/templates/homepage/src/app.tsx +4 -24
  18. package/generators/subapp-react/templates/homepage/src/global.less +4 -4
  19. package/package.json +1 -1
  20. package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/container-manager.ts +0 -334
@@ -1,6 +1,7 @@
1
1
  # 微前端模式
2
2
 
3
3
  > 创建时间:2025-12-26
4
+ > 更新时间:2025-01-25(加载健壮性增强)
4
5
 
5
6
  ## 功能概述
6
7
 
@@ -27,14 +28,16 @@
27
28
 
28
29
  ### 核心文件
29
30
 
30
- | 文件路径 | 说明 |
31
- | ------------------------------------------ | -------------------------- |
32
- | `src/components/MicroAppLoader/index.tsx` | qiankun 微应用加载器组件 |
33
- | `src/components/MicroAppLoader/index.less` | 加载器样式 |
34
- | `src/common/menu/parser.ts` | 菜单解析,包含加载类型判断 |
35
- | `src/common/menu/types.ts` | 类型定义 |
36
- | `src/layouts/index.tsx` | 主布局,集成微应用渲染 |
37
- | `config/config.ts` | qiankun master 配置 |
31
+ | 文件路径 | 说明 |
32
+ | --------------------------------------------------- | -------------------------------- |
33
+ | `src/components/MicroAppLoader/index.tsx` | qiankun 微应用加载器组件 |
34
+ | `src/components/MicroAppLoader/container-manager.ts`| 容器生命周期管理、会话 ID 机制 |
35
+ | `src/components/MicroAppLoader/index.less` | 加载器样式 |
36
+ | `src/common/menu/parser.ts` | 菜单解析,包含加载类型判断 |
37
+ | `src/common/menu/types.ts` | 类型定义 |
38
+ | `src/layouts/index.tsx` | 主布局,集成微应用渲染 |
39
+ | `src/app.tsx` | qiankun 全局错误处理 |
40
+ | `config/config.ts` | qiankun master 配置 |
38
41
 
39
42
  ## API / 组件接口
40
43
 
@@ -430,3 +433,82 @@ export default function HomePage() {
430
433
  有 htmlUrl 或 jsUrls → microapp (使用 qiankun 加载)
431
434
  无 htmlUrl 且无 jsUrls → internal (使用 Outlet 渲染)
432
435
  ```
436
+
437
+ ## 加载健壮性机制
438
+
439
+ > 更新于 2025-01-25
440
+
441
+ ### 解决的问题
442
+
443
+ | 原问题 | 风险 | 修复方案 |
444
+ |--------|------|----------|
445
+ | 无全局错误处理,子应用异常可能导致页面崩溃 | 高 | 添加 `addGlobalUncaughtErrorHandler` |
446
+ | unmount 使用 rAF 时序不可靠 | 高 | 使用 `queueMicrotask` |
447
+ | 同名应用并发加载可能冲突 | 高 | 会话 ID 锁 + `waitForUnmount` |
448
+ | unmount 可能永久卡死 | 中 | 10 秒超时机制 |
449
+ | 加载期间 Props 变化被跳过 | 中 | 加载完成后重新同步 Props |
450
+
451
+ ### 全局错误处理
452
+
453
+ 在 `src/app.tsx` 中注册 qiankun 全局错误处理器,捕获子应用运行时未捕获的异常:
454
+
455
+ ```typescript
456
+ import { addGlobalUncaughtErrorHandler } from 'qiankun';
457
+
458
+ addGlobalUncaughtErrorHandler((event: Event | string) => {
459
+ // 捕获子应用 JS 运行时错误
460
+ // 捕获子应用生命周期钩子异常
461
+ // 捕获资源加载失败
462
+ console.error('[qiankun] Global uncaught error:', event);
463
+ });
464
+ ```
465
+
466
+ ### container-manager API
467
+
468
+ 容器管理器提供会话 ID 机制防止并发加载冲突:
469
+
470
+ ```typescript
471
+ /** 开始加载会话,返回唯一会话 ID */
472
+ function startLoadSession(appName: string): number;
473
+
474
+ /** 检查会话是否仍然有效(被新加载覆盖时返回 false) */
475
+ function isLoadSessionValid(appName: string, sessionId: number): boolean;
476
+
477
+ /** 标记加载完成 */
478
+ function markLoadComplete(appName: string, sessionId: number): boolean;
479
+
480
+ /** 等待当前卸载操作完成(如果有) */
481
+ async function waitForUnmount(appName: string): Promise<void>;
482
+
483
+ /** 获取当前加载状态 */
484
+ function getLoadingStatus(appName: string): 'idle' | 'loading' | 'mounted' | 'unmounting';
485
+ ```
486
+
487
+ ### 快速切换时序
488
+
489
+ ```
490
+ 场景:A → B → A 快速切换
491
+
492
+ A1 开始加载 (sessionId=1)
493
+
494
+ 切换到 B,A1 cleanup 在 queueMicrotask 中执行
495
+
496
+ 切换回 A,A2 开始加载 (sessionId=2)
497
+
498
+ A2 调用 waitForUnmount(),等待 A1 完全卸载
499
+
500
+ A1 卸载完成(带 10 秒超时保护)
501
+
502
+ A2 继续加载,每个异步步骤检查 isLoadSessionValid(sessionId)
503
+
504
+ 加载完成后立即同步最新 Props(locale/timezone 等)
505
+ ```
506
+
507
+ ### 设计决策
508
+
509
+ | 决策点 | 选择 | 理由 |
510
+ |--------|------|------|
511
+ | 卸载时机 | `queueMicrotask` | 在当前事件循环末尾执行,比 `requestAnimationFrame` 更快更可靠 |
512
+ | 并发控制 | 会话 ID 机制 | 简单高效,无需复杂的锁机制 |
513
+ | 超时时间 | 10 秒 | 平衡等待时间与异常检测速度 |
514
+ | Props 同步 | 加载后立即同步 | 确保加载期间的变化不丢失 |
@@ -55,11 +55,14 @@ if (typeof window !== 'undefined') {
55
55
  });
56
56
 
57
57
  // 检查是否是子应用加载失败(资源 404 等)
58
+ // 只匹配明确的关键词,避免误判业务错误(如 "upload failed")
58
59
  const isLoadError =
59
60
  errorMessage.includes('Failed to fetch') ||
60
61
  errorMessage.includes('Loading chunk') ||
61
- errorMessage.includes('load') ||
62
- errorMessage.includes('Script error');
62
+ errorMessage.includes('ChunkLoadError') ||
63
+ errorMessage.includes('Script error') ||
64
+ errorMessage.includes('Loading CSS chunk') ||
65
+ (event instanceof ErrorEvent && event.type === 'error');
63
66
 
64
67
  if (isLoadError) {
65
68
  console.error(
@@ -72,73 +75,6 @@ if (typeof window !== 'undefined') {
72
75
  });
73
76
  }
74
77
 
75
- // ==================== 微应用预加载 ====================
76
- // 预加载所有微应用资源,避免快速切换时的竞态条件
77
- // 当资源已预加载时,loadMicroApp 的异步加载会快速完成,减少容器不存在的错误
78
-
79
- /**
80
- * 是否启用微应用预加载
81
- * - 可通过 URL 参数 ?prefetch=false 禁用(方便调试加载时序问题)
82
- * - 可通过 localStorage.setItem('DISABLE_MICRO_APP_PREFETCH', 'true') 禁用
83
- * - 默认启用
84
- */
85
- const isPrefetchEnabled = (): boolean => {
86
- if (typeof window === 'undefined') return false;
87
-
88
- // URL 参数优先级最高
89
- const urlParams = new URLSearchParams(window.location.search);
90
- const prefetchParam = urlParams.get('prefetch');
91
- if (prefetchParam === 'false') {
92
- console.log('[App] Prefetch disabled via URL parameter');
93
- return false;
94
- }
95
-
96
- // localStorage 开关
97
- if (localStorage.getItem('DISABLE_MICRO_APP_PREFETCH') === 'true') {
98
- console.log('[App] Prefetch disabled via localStorage');
99
- return false;
100
- }
101
-
102
- return true;
103
- };
104
-
105
- const prefetchMicroApps = () => {
106
- if (!isPrefetchEnabled()) {
107
- return;
108
- }
109
-
110
- try {
111
- const menus = getWindowMenus();
112
- const routes = extractRoutes(menus);
113
-
114
- // 筛选出所有微应用路由
115
- const microApps = routes
116
- .filter((route) => route.loadType === 'microapp' && route.entry)
117
- .map((route) => ({
118
- name: route.path,
119
- entry: route.entry!,
120
- }));
121
-
122
- if (microApps.length > 0) {
123
- console.log('[App] Prefetching micro apps:', microApps);
124
- prefetchApps(microApps);
125
- }
126
- } catch (error) {
127
- console.warn('[App] Failed to prefetch micro apps:', error);
128
- }
129
- };
130
-
131
- // 在页面加载后预加载微应用
132
- if (typeof window !== 'undefined') {
133
- // 使用 requestIdleCallback 在浏览器空闲时预加载,避免影响首屏渲染
134
- if ('requestIdleCallback' in window) {
135
- window.requestIdleCallback(() => prefetchMicroApps(), { timeout: 3000 });
136
- } else {
137
- // 降级方案:延迟 1 秒后预加载
138
- setTimeout(prefetchMicroApps, 1000);
139
- }
140
- }
141
-
142
78
  // ==================== 微前端共享依赖 ====================
143
79
  // 将公共库暴露到 window,供子应用复用,避免重复打包
144
80
  // 子应用通过 externals 配置引用这些全局变量
@@ -32,6 +32,40 @@ export type {
32
32
  MicroAppErrorSource,
33
33
  } from './types';
34
34
 
35
+
36
+ // ============================================
37
+ // 微应用名称生成
38
+ // ============================================
39
+
40
+ /**
41
+ * 从 entry URL 中提取微应用标识
42
+ * 同一个 entry 的所有路由使用相同的标识,避免频繁卸载/重载微应用
43
+ *
44
+ * 注意:使用完整的 origin + pathname 作为标识,避免同一 host 上多个子应用冲突
45
+ * 例如:http://localhost:8010/app1/ 和 http://localhost:8010/app2/ 会有不同的标识
46
+ *
47
+ * @param entry 微应用入口 URL
48
+ * @returns 微应用标识(字母数字和连字符组成)
49
+ */
50
+ export const getAppNameFromEntry = (entry: string): string => {
51
+ try {
52
+ const url = new URL(entry, window.location.href);
53
+ // 使用 origin + pathname 作为标识,确保不同路径的子应用有不同标识
54
+ // 如 "localhost-8010" 或 "localhost-8010-app1"
55
+ const identifier = url.host + url.pathname;
56
+ return identifier
57
+ .replace(/[^a-zA-Z0-9]/g, '-')
58
+ .replace(/-+/g, '-')
59
+ .replace(/^-|-$/g, '');
60
+ } catch {
61
+ // fallback:使用 entry 的 hash
62
+ return entry
63
+ .replace(/[^a-zA-Z0-9]/g, '-')
64
+ .replace(/-+/g, '-')
65
+ .replace(/^-|-$/g, '');
66
+ }
67
+ };
68
+
35
69
  // ============================================
36
70
  // 环境检测
37
71
  // ============================================
@@ -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
  /**