generator-mico-cli 0.2.5 → 0.2.7

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.
@@ -179,8 +179,7 @@ document.body.setAttribute('data-theme', 'dark');
179
179
  ### 文件位置
180
180
  ```
181
181
  apps/layout/mock/
182
- ├── menus.json # 菜单数据
183
- ├── menus.ts # 菜单数据处理
182
+ ├── menus.ts # 菜单 Mock 数据(带类型)
184
183
  └── user.mock.ts # 用户相关 Mock API
185
184
  ```
186
185
 
@@ -1,13 +1,9 @@
1
1
  // https://umijs.org/config/
2
2
 
3
3
  import { defineConfig } from '@umijs/max';
4
- import fs from 'fs';
5
- import path from 'path';
6
4
 
7
- // 使用 fs 读取 JSON 避免成为配置依赖,修改 menus.json 不会触发服务器重启
8
- const mockMenus = JSON.parse(
9
- fs.readFileSync(path.join(__dirname, '../mock/menus.json'), 'utf-8'),
10
- );
5
+
6
+ import mockMenus from '../mock/menus';
11
7
 
12
8
  const config: ReturnType<typeof defineConfig> = {
13
9
  publicPath: '/',
@@ -98,6 +98,7 @@ const config: ReturnType<typeof defineConfig> = {
98
98
  */
99
99
  mock: {
100
100
  include: ['mock/**/_mock.ts', 'mock/**/*.mock.ts', 'src/pages/**/_mock.ts'],
101
+ exclude: ['mock/**/menus.ts'],
101
102
  },
102
103
 
103
104
  /**
@@ -2,10 +2,132 @@
2
2
  * Mock 菜单数据
3
3
  * 用于开发环境测试
4
4
  *
5
- * 注意:实际数据存放在 menus.json 中
6
- * 修改菜单数据请编辑 menus.json 文件,不会触发服务器重启
5
+ * 菜单结构说明:
6
+ * - id: 菜单唯一标识
7
+ * - name: 菜单名称
8
+ * - type: 'page' | 'group' (page: 页面, group: 分组)
9
+ * - path: 路由路径 (group 类型为 null)
10
+ * - icon: 图标名称
11
+ * - enabled: 是否启用
12
+ * - sortOrder: 排序权重
13
+ * - pageId: 关联的页面 ID
14
+ * - page: 页面配置 (微前端入口)
15
+ * - htmlUrl: 子应用入口地址
16
+ * - jsUrls: 额外 JS 资源
17
+ * - cssUrls: 额外 CSS 资源
18
+ * - children: 子菜单数组
7
19
  */
8
- import mockMenusData from './menus.json';
9
20
 
10
- export const mockMenus = mockMenusData;
21
+ import type { MenuItem, PageConfig } from '@/common/menu/types';
22
+
23
+ /** Mock 页面配置 - 只需要核心字段 */
24
+ type MockPageConfig = Pick<PageConfig, 'id' | 'name' | 'route' | 'enabled' | 'htmlUrl' | 'jsUrls' | 'cssUrls'>;
25
+
26
+ /** Mock 菜单项 - page 字段使用简化类型 */
27
+ type MockMenuItem = Omit<MenuItem, 'page' | 'children'> & {
28
+ page: MockPageConfig | null;
29
+ children: MockMenuItem[];
30
+ };
31
+
32
+ const mockMenus: MockMenuItem[] = [
33
+ {
34
+ "id": 1,
35
+ "name": "首页",
36
+ "type": "page",
37
+ "path": null,
38
+ "icon": "Home",
39
+ "enabled": true,
40
+ "sortOrder": 0,
41
+ "pageId": 1,
42
+ "page": {
43
+ "id": 1,
44
+ "name": "home",
45
+ "route": "/",
46
+ "enabled": true,
47
+ "htmlUrl": "",
48
+ "jsUrls": [],
49
+ "cssUrls": []
50
+ },
51
+ "children": []
52
+ },
53
+ {
54
+ "id": 2,
55
+ "name": "示例模块",
56
+ "type": "group",
57
+ "path": null,
58
+ "icon": "List",
59
+ "enabled": true,
60
+ "sortOrder": 1,
61
+ "pageId": null,
62
+ "page": null,
63
+ "children": [
64
+ {
65
+ "id": 3,
66
+ "name": "示例页面",
67
+ "type": "page",
68
+ "path": "/example/page",
69
+ "icon": "File",
70
+ "enabled": true,
71
+ "sortOrder": 1,
72
+ "pageId": 45,
73
+ "page": {
74
+ "id": 45,
75
+ "name": "example-page",
76
+ "route": "/example/page",
77
+ "enabled": true,
78
+ "htmlUrl": "",
79
+ "jsUrls": [],
80
+ "cssUrls": []
81
+ },
82
+ "children": []
83
+ }
84
+ ]
85
+ },
86
+ {
87
+ "id": 5,
88
+ "name": "微应用示例",
89
+ "type": "group",
90
+ "path": null,
91
+ "icon": "Apps",
92
+ "enabled": true,
93
+ "sortOrder": 2,
94
+ "pageId": null,
95
+ "page": null,
96
+ "children": [
97
+ {
98
+ "id": 6,
99
+ "name": "子应用页面",
100
+ "type": "page",
101
+ "path": null,
102
+ "icon": "Desktop",
103
+ "enabled": true,
104
+ "sortOrder": 1,
105
+ "pageId": 55,
106
+ "page": {
107
+ "id": 55,
108
+ "name": "subapp-example",
109
+ "route": "/subapp",
110
+ "enabled": true,
111
+ "htmlUrl": "//localhost:8010",
112
+ "jsUrls": [],
113
+ "cssUrls": []
114
+ },
115
+ "children": []
116
+ }
117
+ ]
118
+ },
119
+ {
120
+ "id": 7,
121
+ "name": "外部链接",
122
+ "type": "link",
123
+ "path": "https://github.com",
124
+ "icon": "Link",
125
+ "enabled": true,
126
+ "sortOrder": 3,
127
+ "pageId": null,
128
+ "page": null,
129
+ "children": []
130
+ }
131
+ ]
132
+
11
133
  export default mockMenus;
@@ -18,7 +18,7 @@ import {
18
18
  } from './common/micro';
19
19
  import { initTheme } from './common/theme';
20
20
  import MicroAppLoader from './components/MicroAppLoader';
21
- import { NO_AUTH_ROUTE_LIST } from './constants';
21
+ import { NO_AUTH_ROUTE_LIST } from '@/constants';
22
22
  import './global.less';
23
23
 
24
24
  // ==================== qiankun 全局错误处理 ====================
@@ -18,7 +18,6 @@ import { loadMicroApp } from 'qiankun';
18
18
  import { microAppLogger } from '@/common/logger';
19
19
  // 导入路由守卫(会自动初始化)
20
20
  import { refreshUserIntent, setUserIntent } from '@/common/route-guard';
21
-
22
21
  // 导入低优先级预加载函数
23
22
  import { markAppAsPrefetched, prefetchMicroAppsLowPriority } from '@/common/micro-prefetch';
24
23
 
@@ -99,6 +98,27 @@ function activateContainer(container: HTMLElement, target: HTMLElement): void {
99
98
  container.style.cssText = 'display: block; width: 100%; height: 100%;';
100
99
  }
101
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
+
102
122
  function deactivateContainer(container: HTMLElement): void {
103
123
  document.body.appendChild(container);
104
124
  container.classList.remove(CSS_CLASS.active);
@@ -170,7 +190,7 @@ class MicroAppManager {
170
190
  microAppLogger.log('Already mounted, updating props only');
171
191
  const cached = this.appCache.get(config.name);
172
192
  if (cached && cached.microApp.getStatus() === 'MOUNTED') {
173
- cached.microApp.update?.(config.props);
193
+ safeUpdate(cached.microApp, config.props);
174
194
  if (cached.container.parentElement !== config.target) {
175
195
  activateContainer(cached.container, config.target);
176
196
  }
@@ -200,7 +220,7 @@ class MicroAppManager {
200
220
  if (this.currentAppName && this.state === 'mounted') {
201
221
  const cached = this.appCache.get(this.currentAppName);
202
222
  if (cached && cached.microApp.getStatus() === 'MOUNTED') {
203
- cached.microApp.update?.(props);
223
+ safeUpdate(cached.microApp, props);
204
224
  }
205
225
  }
206
226
  }
@@ -217,7 +237,7 @@ class MicroAppManager {
217
237
  await this.safeUnmount(instance.microApp);
218
238
  instance.container.remove();
219
239
  } catch (err) {
220
- microAppLogger.warn('Clear cache error for', name, err);
240
+ microAppLogger.error('Clear cache error for', name, err);
221
241
  }
222
242
  }
223
243
  this.appCache.clear();
@@ -278,7 +298,6 @@ class MicroAppManager {
278
298
  this.updateState({ loading: true, error: null, mounted: false });
279
299
 
280
300
  try {
281
-
282
301
  if (this.shouldAbort(request.name, mySeq)) {
283
302
  console.log('🔍[路由调试] ⚠️ Aborted before load', { name: request.name, mySeq, operationSeq: this.operationSeq });
284
303
  this.state = 'idle';
@@ -314,25 +333,19 @@ class MicroAppManager {
314
333
  console.log('🔍[路由调试] 等待缓存实例 mountPromise...', { name: request.name });
315
334
  await withTimeout(appInstance.microApp.mountPromise, MOUNT_TIMEOUT, '子应用挂载超时');
316
335
  console.log('🔍[路由调试] 缓存实例 mountPromise 完成', { name: request.name, status: appInstance.microApp.getStatus() });
317
- // 重要:mount 完成后立即更新 props,确保子应用使用最新的 routePath
318
- // qiankun 的 mount() 使用的是创建实例时的原始 props,可能包含过期的 routePath
319
- appInstance.microApp.update?.(request.props);
320
- console.log('🔍[路由调试] 缓存实例 props 已更新(BOOTSTRAPPING 后)', { name: request.name });
321
336
  } else if (status === 'NOT_MOUNTED') {
322
337
  // 实例之前被 unmount 过,需要重新 mount
323
338
  console.log('🔍[路由调试] 开始 mount 缓存实例', { name: request.name });
324
339
  await withTimeout(appInstance.microApp.mount(), MOUNT_TIMEOUT, '子应用挂载超时');
325
340
  console.log('🔍[路由调试] 缓存实例 mount 完成', { name: request.name, status: appInstance.microApp.getStatus() });
326
- // 重要:mount 完成后立即更新 props,确保子应用使用最新的 routePath
327
- // qiankun 的 mount() 使用的是创建实例时的原始 props,可能包含过期的 routePath
328
- appInstance.microApp.update?.(request.props);
329
- console.log('🔍[路由调试] 缓存实例 props 已更新(NOT_MOUNTED 后)', { name: request.name });
330
341
  }
331
342
  // 如果 status === 'MOUNTED',则无需操作(已在 switchTo 入口处处理)
332
343
 
333
344
  // 刷新意图,保护 mount 成功后的短暂窗口期
334
345
  refreshUserIntent();
335
346
 
347
+ // 关键:在调用 update 之前检查是否需要 abort
348
+ // 这可以避免在即将被 unmount 的应用上调用 update,从而防止 single-spa error #32
336
349
  if (this.shouldAbort(request.name, mySeq)) {
337
350
  console.log('🔍[路由调试] ⚠️ Aborted (cached) after mount', { name: request.name });
338
351
  await this.safeUnmount(appInstance.microApp);
@@ -342,6 +355,13 @@ class MicroAppManager {
342
355
  return;
343
356
  }
344
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
+
345
365
  this.currentAppName = request.name;
346
366
  this.state = 'mounted';
347
367
  // 不立即清除意图,让它自然过期(5秒)
@@ -398,7 +418,17 @@ class MicroAppManager {
398
418
 
399
419
  if (this.shouldAbort(request.name, mySeq)) {
400
420
  console.log('🔍[路由调试] ⚠️ Aborted after loadPromise', { name: request.name, mySeq, operationSeq: this.operationSeq });
401
- // 不销毁实例,只是 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
+ }
402
432
  deactivateContainer(container);
403
433
  this.state = 'idle';
404
434
  if (this.pendingRequest) this.processRequest();
@@ -409,14 +439,11 @@ class MicroAppManager {
409
439
  await withTimeout(microApp.mountPromise, MOUNT_TIMEOUT, '子应用挂载超时');
410
440
  console.log('🔍[路由调试] mountPromise 完成', { name: request.name, qiankunName, status: microApp.getStatus() });
411
441
 
412
- // 重要:mount 完成后调用 update,触发子应用的路由同步
413
- // 由于子应用 mount 生命周期不再调用 syncRoute,需要通过 update 来同步路由
414
- microApp.update?.(request.props);
415
- console.log('🔍[路由调试] 新实例 props 已更新', { name: request.name });
416
-
417
442
  // 刷新意图,保护 mount 成功后的短暂窗口期
418
443
  refreshUserIntent();
419
444
 
445
+ // 关键:在调用 update 之前检查是否需要 abort
446
+ // 这可以避免在即将被 unmount 的应用上调用 update,从而防止 single-spa error #32
420
447
  if (this.shouldAbort(request.name, mySeq)) {
421
448
  console.log('🔍[路由调试] ⚠️ Aborted after mountPromise', { name: request.name, mySeq, operationSeq: this.operationSeq });
422
449
  // 实例已在 loadPromise 后缓存,这里只需 unmount 和 deactivate
@@ -427,6 +454,11 @@ class MicroAppManager {
427
454
  return;
428
455
  }
429
456
 
457
+ // mount 完成且不需要 abort,安全地调用 update 同步路由
458
+ // 由于子应用 mount 生命周期不再调用 syncRoute,需要通过 update 来同步路由
459
+ await safeUpdate(microApp, request.props);
460
+ console.log('🔍[路由调试] 新实例 props 已更新', { name: request.name });
461
+
430
462
  // 实例已在 loadPromise 后缓存,这里不需要重复缓存
431
463
 
432
464
  this.currentAppName = request.name;
@@ -483,7 +515,7 @@ class MicroAppManager {
483
515
  await this.safeUnmount(appInstance.microApp);
484
516
  deactivateContainer(appInstance.container);
485
517
  } catch (err) {
486
- microAppLogger.warn('Deactivate error:', err);
518
+ microAppLogger.error('Deactivate error:', err);
487
519
  }
488
520
  }
489
521
 
@@ -494,11 +526,31 @@ class MicroAppManager {
494
526
 
495
527
  private async safeUnmount(microApp: MicroApp): Promise<void> {
496
528
  try {
497
- 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') {
498
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);
499
551
  }
500
552
  } catch (err) {
501
- microAppLogger.warn('safeUnmount error:', err);
553
+ microAppLogger.error('safeUnmount error:', err);
502
554
  }
503
555
  }
504
556
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "generator-mico-cli",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Yeoman generator for Mico CLI projects",
5
5
  "keywords": [
6
6
  "yeoman-generator",
@@ -1,100 +0,0 @@
1
- [
2
- {
3
- "id": 1,
4
- "name": "首页",
5
- "type": "page",
6
- "path": null,
7
- "icon": "Home",
8
- "enabled": true,
9
- "sortOrder": 0,
10
- "pageId": 1,
11
- "page": {
12
- "id": 1,
13
- "name": "home",
14
- "route": "/",
15
- "enabled": true,
16
- "htmlUrl": null,
17
- "jsUrls": [],
18
- "cssUrls": []
19
- },
20
- "children": []
21
- },
22
- {
23
- "id": 2,
24
- "name": "示例模块",
25
- "type": "group",
26
- "path": null,
27
- "icon": "List",
28
- "enabled": true,
29
- "sortOrder": 1,
30
- "pageId": null,
31
- "page": null,
32
- "children": [
33
- {
34
- "id": 3,
35
- "name": "示例页面",
36
- "type": "page",
37
- "path": "/example/page",
38
- "icon": "File",
39
- "enabled": true,
40
- "sortOrder": 1,
41
- "pageId": 45,
42
- "page": {
43
- "id": 45,
44
- "name": "example-page",
45
- "route": "/example/page",
46
- "enabled": true,
47
- "htmlUrl": null,
48
- "jsUrls": [],
49
- "cssUrls": []
50
- },
51
- "children": []
52
- }
53
- ]
54
- },
55
- {
56
- "id": 5,
57
- "name": "微应用示例",
58
- "type": "group",
59
- "path": null,
60
- "icon": "Apps",
61
- "enabled": true,
62
- "sortOrder": 2,
63
- "pageId": null,
64
- "page": null,
65
- "children": [
66
- {
67
- "id": 6,
68
- "name": "子应用页面",
69
- "type": "page",
70
- "path": null,
71
- "icon": "Desktop",
72
- "enabled": true,
73
- "sortOrder": 1,
74
- "pageId": 55,
75
- "page": {
76
- "id": 55,
77
- "name": "subapp-example",
78
- "route": "/subapp",
79
- "enabled": true,
80
- "htmlUrl": "//localhost:8010",
81
- "jsUrls": [],
82
- "cssUrls": []
83
- },
84
- "children": []
85
- }
86
- ]
87
- },
88
- {
89
- "id": 7,
90
- "name": "外部链接",
91
- "type": "link",
92
- "path": "https://github.com",
93
- "icon": "Link",
94
- "enabled": true,
95
- "sortOrder": 3,
96
- "pageId": null,
97
- "page": null,
98
- "children": []
99
- }
100
- ]