generator-mico-cli 0.1.18 → 0.1.20

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.
@@ -0,0 +1,54 @@
1
+ // https://umijs.org/config/
2
+
3
+ import { defineConfig } from '@umijs/max';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+
7
+ // 使用 fs 读取 JSON 避免成为配置依赖,修改 menus.json 不会触发服务器重启
8
+ const mockMenus = JSON.parse(
9
+ fs.readFileSync(path.join(__dirname, '../mock/menus.json'), 'utf-8'),
10
+ );
11
+
12
+ const config: ReturnType<typeof defineConfig> = {
13
+ publicPath: '/',
14
+ /**
15
+ * @name 注入到 HTML head 的脚本
16
+ * @description 开发环境注入 mock 菜单数据
17
+ * @doc https://umijs.org/docs/api/config#headscripts
18
+ */
19
+ headScripts: [
20
+ {
21
+ content: `
22
+ window.__MICO_MENUS__ = ${JSON.stringify(mockMenus)};
23
+ window.__MICO_CONFIG__ = {
24
+ appName: 'Audit Center',
25
+ apiBaseUrl: '',
26
+ };
27
+ `,
28
+ },
29
+ ],
30
+
31
+ proxy: {
32
+ '/api/': {
33
+ // 要代理的地址
34
+ target: 'https://preview.pro.ant.design',
35
+ // 配置了这个可以从 http 代理到 https
36
+ // 依赖 origin 的功能可能需要这个,比如 cookie
37
+ changeOrigin: true,
38
+ },
39
+ },
40
+ define: {
41
+ 'process.env.NODE_ENV': 'development',
42
+ 'process.env.APP_ID': 'mibot_dev',
43
+ 'process.env.API_BASE_URL': 'https://dashboard-api-test.micoplatform.com',
44
+ 'process.env.PROXY_SUFFIX': '/',
45
+ 'process.env.LOGIN_ENDPOINT':
46
+ '',
47
+ 'process.env.REFRESH_ENDPOINT':
48
+ '',
49
+ 'process.env.EXTERNAL_LOGIN_PATH':
50
+ '',
51
+ },
52
+ };
53
+
54
+ export default defineConfig(config);
@@ -4,8 +4,8 @@
4
4
  "author": "Easton <easton@micous.com>",
5
5
  "scripts": {
6
6
  "build": "npm run build:production",
7
- "build:development": "cross-env UMI_ENV=dev max build",
8
- "build:production": "cross-env UMI_ENV=prod max build",
7
+ "build:development": "cross-env UMI_ENV=development max build",
8
+ "build:production": "cross-env UMI_ENV=production max build",
9
9
  "build:testing": "cross-env UMI_ENV=testing max build",
10
10
  "dev": "npm run start:development",
11
11
  "format": "prettier --cache --write .",
@@ -0,0 +1,202 @@
1
+ /**
2
+ * 微应用容器管理器
3
+ *
4
+ * 核心策略:
5
+ * 1. 容器在 document.body 中创建,与 React 生命周期解耦
6
+ * 2. 激活时移动到占位元素内,参与正常文档流
7
+ * 3. 停用时移回 body 并隐藏
8
+ *
9
+ * 这样既避免了 React unmount 时删除容器导致的竞态问题,
10
+ * 又能让容器在激活时参与正常的 CSS 布局。
11
+ *
12
+ * @see https://github.com/umijs/qiankun/issues/2845
13
+ */
14
+
15
+ import type { MicroApp } from 'qiankun';
16
+
17
+ // ============================================================================
18
+ // 类型定义
19
+ // ============================================================================
20
+
21
+ type ContainerStatus = 'active' | 'hidden' | 'pending-delete';
22
+
23
+ interface ContainerEntry {
24
+ container: HTMLElement;
25
+ microApp: MicroApp | null;
26
+ status: ContainerStatus;
27
+ deleteTimer: ReturnType<typeof setTimeout> | null;
28
+ }
29
+
30
+ // ============================================================================
31
+ // 常量
32
+ // ============================================================================
33
+
34
+ /** 容器删除延迟(毫秒)- 给 qiankun 足够时间完成清理 */
35
+ const DELETE_DELAY = 5000;
36
+
37
+ /** CSS 类名 */
38
+ const CSS_CLASS = {
39
+ base: 'micro-app-container-managed',
40
+ hidden: 'micro-app-container-managed--hidden',
41
+ active: 'micro-app-container-managed--active',
42
+ } as const;
43
+
44
+ // ============================================================================
45
+ // 状态
46
+ // ============================================================================
47
+
48
+ const containers = new Map<string, ContainerEntry>();
49
+
50
+ // ============================================================================
51
+ // 私有方法
52
+ // ============================================================================
53
+
54
+ function createContainer(appName: string): HTMLElement {
55
+ const container = document.createElement('div');
56
+ container.id = `micro-app-${appName}`;
57
+ container.className = `${CSS_CLASS.base} ${CSS_CLASS.hidden}`;
58
+ document.body.appendChild(container);
59
+ return container;
60
+ }
61
+
62
+ // ============================================================================
63
+ // 公开 API
64
+ // ============================================================================
65
+
66
+ /**
67
+ * 获取或创建容器
68
+ */
69
+ export function getContainer(appName: string): HTMLElement {
70
+ let entry = containers.get(appName);
71
+
72
+ if (entry) {
73
+ // 取消待删除的计时器(容器被复用)
74
+ if (entry.deleteTimer) {
75
+ clearTimeout(entry.deleteTimer);
76
+ entry.deleteTimer = null;
77
+ }
78
+ entry.status = 'hidden';
79
+ return entry.container;
80
+ }
81
+
82
+ // 创建新容器
83
+ const container = createContainer(appName);
84
+ entry = {
85
+ container,
86
+ microApp: null,
87
+ status: 'hidden',
88
+ deleteTimer: null,
89
+ };
90
+ containers.set(appName, entry);
91
+
92
+ return container;
93
+ }
94
+
95
+ /**
96
+ * 激活容器:移动到占位元素内,参与正常文档流
97
+ */
98
+ export function activateContainer(appName: string, target: HTMLElement): void {
99
+ const entry = containers.get(appName);
100
+ if (!entry) return;
101
+
102
+ const { container } = entry;
103
+
104
+ // 移动容器到占位元素内(参与文档流)
105
+ target.appendChild(container);
106
+
107
+ // 切换样式
108
+ container.classList.remove(CSS_CLASS.hidden);
109
+ container.classList.add(CSS_CLASS.active);
110
+
111
+ entry.status = 'active';
112
+ }
113
+
114
+ /**
115
+ * 停用容器:移回 body 并隐藏
116
+ */
117
+ export function deactivateContainer(appName: string): void {
118
+ const entry = containers.get(appName);
119
+ if (!entry) return;
120
+
121
+ const { container } = entry;
122
+
123
+ // 移回 body(脱离 React 树)
124
+ document.body.appendChild(container);
125
+
126
+ // 切换样式
127
+ container.classList.remove(CSS_CLASS.active);
128
+ container.classList.add(CSS_CLASS.hidden);
129
+
130
+ entry.status = 'hidden';
131
+ }
132
+
133
+ /**
134
+ * 设置微应用实例
135
+ */
136
+ export function setMicroApp(appName: string, microApp: MicroApp): void {
137
+ const entry = containers.get(appName);
138
+ if (entry) {
139
+ entry.microApp = microApp;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * 获取微应用实例
145
+ */
146
+ export function getMicroApp(appName: string): MicroApp | null {
147
+ return containers.get(appName)?.microApp ?? null;
148
+ }
149
+
150
+ /**
151
+ * 安全卸载微应用并安排容器延迟删除
152
+ */
153
+ export async function unmountApp(appName: string): Promise<void> {
154
+ const entry = containers.get(appName);
155
+ if (!entry) return;
156
+
157
+ const { microApp } = entry;
158
+
159
+ if (microApp) {
160
+ entry.microApp = null;
161
+
162
+ // 等待各阶段完成再卸载
163
+ try {
164
+ await microApp.loadPromise;
165
+ await microApp.bootstrapPromise;
166
+ await microApp.mountPromise;
167
+ } catch {
168
+ // 忽略
169
+ }
170
+
171
+ try {
172
+ await microApp.unmount();
173
+ } catch {
174
+ // 忽略
175
+ }
176
+ }
177
+
178
+ // 安排延迟删除
179
+ if (!entry.deleteTimer) {
180
+ entry.status = 'pending-delete';
181
+ entry.deleteTimer = setTimeout(() => {
182
+ deleteContainer(appName);
183
+ }, DELETE_DELAY);
184
+ }
185
+ }
186
+
187
+ /**
188
+ * 删除容器
189
+ */
190
+ function deleteContainer(appName: string): void {
191
+ const entry = containers.get(appName);
192
+ if (!entry) return;
193
+
194
+ // 只删除 pending-delete 状态的容器(未被重新激活)
195
+ if (entry.status === 'pending-delete') {
196
+ entry.container.remove();
197
+ containers.delete(appName);
198
+ } else {
199
+ // 容器被重新激活了,清除计时器引用
200
+ entry.deleteTimer = null;
201
+ }
202
+ }
@@ -1,3 +1,7 @@
1
+ // ============================================================================
2
+ // 占位容器(在 React 树中)
3
+ // ============================================================================
4
+
1
5
  .micro-app-container {
2
6
  width: 100%;
3
7
  height: 100%;
@@ -18,18 +22,6 @@
18
22
  z-index: 10;
19
23
  }
20
24
 
21
- .micro-app-iframe {
22
- width: 100%;
23
- height: 100%;
24
- border: none;
25
- transition: opacity 0.3s;
26
- }
27
-
28
- .micro-app-content {
29
- width: 100%;
30
- height: 100%;
31
- }
32
-
33
25
  .micro-app-error {
34
26
  display: flex;
35
27
  flex-direction: column;
@@ -41,4 +33,41 @@
41
33
  p {
42
34
  margin: 8px 0;
43
35
  }
36
+
37
+ .error-detail {
38
+ font-size: 12px;
39
+ color: var(--color-text-4);
40
+ }
41
+ }
42
+
43
+ // ============================================================================
44
+ // 托管容器(在 body 中创建,激活时移动到占位元素内)
45
+ // ============================================================================
46
+
47
+ .micro-app-container-managed {
48
+ // 激活时在文档流中,继承占位元素的尺寸
49
+ width: 100%;
50
+ height: 100%;
51
+ overflow: auto;
52
+
53
+ > div {
54
+ height: 100%;
55
+ }
56
+
57
+ // 隐藏状态(在 body 中)
58
+ &--hidden {
59
+ position: absolute;
60
+ top: -9999px;
61
+ left: -9999px;
62
+ width: 1px;
63
+ height: 1px;
64
+ visibility: hidden;
65
+ pointer-events: none;
66
+ }
67
+
68
+ // 激活状态(在占位元素内,参与文档流)
69
+ &--active {
70
+ visibility: visible;
71
+ pointer-events: auto;
72
+ }
44
73
  }
@@ -1,99 +1,153 @@
1
- import { microAppLogger } from '@/common/logger';
2
- import { request } from '@/common/request';
1
+ import { getAuthInfo } from '@/common/auth/cs-auth-manager';
2
+ import { EEnv, getEnv } from '@/common/env';
3
3
  import { Spin } from '@arco-design/web-react';
4
- import { loadMicroApp, type MicroApp } from 'qiankun';
5
- import React, { useEffect, useRef, useState } from 'react';
4
+ import { loadMicroApp } from 'qiankun';
5
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
6
+ import {
7
+ activateContainer,
8
+ deactivateContainer,
9
+ getContainer,
10
+ getMicroApp,
11
+ setMicroApp,
12
+ unmountApp,
13
+ } from './container-manager';
6
14
  import './index.less';
7
15
 
8
16
  interface MicroAppLoaderProps {
9
- /** 微应用入口 URL */
10
17
  entry: string;
11
- /** 微应用唯一标识(用于生成容器 ID,应为无空格的技术名称) */
12
18
  name: string;
13
- /** 微应用显示名称(用于 loading 提示,可包含中文) */
14
19
  displayName?: string;
15
20
  }
16
21
 
17
- /**
18
- * 将路由路径转换为有效的 HTML ID
19
- * 例如:/homepage → homepage, /audit/pending → audit-pending
20
- */
21
22
  const sanitizeId = (path: string): string => {
22
23
  return (
23
24
  path
24
- .replace(/^\/+/, '') // 移除开头的斜杠
25
- .replace(/\/+/g, '-') // 斜杠替换为连字符
26
- .replace(/\s+/g, '-') // 空格替换为连字符
27
- .replace(/[^a-zA-Z0-9_-]/g, '') || // 移除其他特殊字符
28
- `app-${Date.now()}`
29
- ); // 如果结果为空,使用时间戳
25
+ .replace(/^\/+/, '')
26
+ .replace(/\/+/g, '-')
27
+ .replace(/\s+/g, '-')
28
+ .replace(/[^a-zA-Z0-9_-]/g, '') || `app-${Date.now()}`
29
+ );
30
30
  };
31
31
 
32
- /**
33
- * qiankun 微应用加载器
34
- * 使用 loadMicroApp API 动态加载微前端应用
35
- */
36
32
  const MicroAppLoader: React.FC<MicroAppLoaderProps> = ({
37
33
  entry,
38
34
  name,
39
35
  displayName,
40
36
  }) => {
41
- const containerRef = useRef<HTMLDivElement>(null);
42
- const microAppRef = useRef<MicroApp | null>(null);
37
+ const wrapperRef = useRef<HTMLDivElement>(null);
38
+ const mountIdRef = useRef(0);
43
39
  const [loading, setLoading] = useState(true);
44
40
  const [error, setError] = useState<string | null>(null);
41
+ const isMountedRef = useRef(false);
42
+
43
+ const appName = sanitizeId(name);
45
44
 
46
- // 生成安全的容器 ID(避免空格和特殊字符导致选择器失效)
47
- const safeAppName = sanitizeId(name);
48
- const containerId = `micro-app-${safeAppName}`;
49
- // 用于显示的名称(支持中文)
50
- const label = displayName || name;
45
+ const env = useMemo(() => {
46
+ const currentEnv = getEnv();
47
+ return currentEnv === EEnv.Development
48
+ ? 'development'
49
+ : currentEnv === EEnv.Testing
50
+ ? 'testing'
51
+ : 'production';
52
+ }, []);
51
53
 
54
+ // 使用 ref 存储构建 props 的函数,避免 effect 依赖变化导致微应用重新加载
55
+ const buildPropsRef = useRef(() => ({}));
56
+ buildPropsRef.current = () => {
57
+ const authInfo = getAuthInfo();
58
+ return {
59
+ mainApp: 'portal-web',
60
+ env,
61
+ authToken: authInfo.token,
62
+ wsToken: authInfo.wsToken,
63
+ uid: authInfo.uid,
64
+ avatar: authInfo.avatar,
65
+ nickname: authInfo.nickname,
66
+ };
67
+ };
68
+
69
+ // 当 sharedServices(websocket)变化时,更新子应用的 props
52
70
  useEffect(() => {
53
- if (!containerRef.current) return;
71
+ if (!isMountedRef.current) return;
72
+
73
+ const microApp = getMicroApp(appName);
74
+ microApp?.update?.(buildPropsRef.current());
75
+ }, [appName]);
76
+
77
+ useEffect(() => {
78
+ const wrapper = wrapperRef.current;
79
+ if (!wrapper) return;
80
+
81
+ const currentMountId = ++mountIdRef.current;
82
+ let isCancelled = false;
83
+
84
+ setLoading(true);
85
+ setError(null);
54
86
 
55
- microAppLogger.log('Loading micro app:', {
56
- name,
57
- entry,
58
- containerId,
59
- containerExists: !!document.getElementById(containerId),
60
- });
87
+ const container = getContainer(appName);
88
+ activateContainer(appName, wrapper);
61
89
 
62
- // 加载微应用
63
90
  const loadApp = async () => {
91
+ // 卸载旧实例
92
+ const existingApp = getMicroApp(appName);
93
+ if (existingApp) {
94
+ try {
95
+ await existingApp.unmount();
96
+ } catch {
97
+ // 忽略
98
+ }
99
+ }
100
+
101
+ if (isCancelled) return;
102
+
64
103
  try {
65
- microAppRef.current = loadMicroApp({
66
- name,
67
- entry,
68
- container: `#${containerId}`,
69
- props: {
70
- // 传递给子应用的数据
71
- mainApp: '<%= projectName %>',
72
- // 共享主应用的 request 实例
73
- request,
104
+ const microApp = loadMicroApp(
105
+ {
106
+ name: `${appName}_${currentMountId}`,
107
+ entry,
108
+ container,
109
+ props: buildPropsRef.current(),
74
110
  },
75
- });
111
+ { sandbox: { loose: true } },
112
+ );
113
+
114
+ setMicroApp(appName, microApp);
115
+
116
+ if (isCancelled) {
117
+ unmountApp(appName);
118
+ return;
119
+ }
76
120
 
77
- // 等待微应用挂载完成
78
- await microAppRef.current.mountPromise;
79
- setLoading(false);
121
+ await microApp.mountPromise;
122
+
123
+ if (!isCancelled) {
124
+ isMountedRef.current = true;
125
+ setLoading(false);
126
+ }
80
127
  } catch (err) {
81
- microAppLogger.error(`Failed to load micro app [${label}]:`, err);
82
- setError(err instanceof Error ? err.message : 'Unknown error');
83
- setLoading(false);
128
+ if (!isCancelled) {
129
+ setError(err instanceof Error ? err.message : 'Unknown error');
130
+ setLoading(false);
131
+ }
84
132
  }
85
133
  };
86
134
 
87
135
  loadApp();
88
136
 
89
- // 卸载微应用
90
137
  return () => {
91
- if (microAppRef.current) {
92
- microAppRef.current.unmount();
93
- microAppRef.current = null;
94
- }
138
+ isCancelled = true;
139
+ isMountedRef.current = false;
140
+
141
+ // 同步执行,确保在下一次 activate 前移走容器
142
+ deactivateContainer(appName);
143
+
144
+ // 异步卸载微应用,给 qiankun 足够时间完成清理
145
+ requestAnimationFrame(() => {
146
+ unmountApp(appName);
147
+ });
95
148
  };
96
- }, [entry, name, containerId, label]);
149
+ // 注意:不依赖 sharedServices,websocket 变化时通过 update() 更新 props 而不重新加载微应用
150
+ }, [entry, appName, env]);
97
151
 
98
152
  if (error) {
99
153
  return (
@@ -107,13 +161,12 @@ const MicroAppLoader: React.FC<MicroAppLoaderProps> = ({
107
161
  }
108
162
 
109
163
  return (
110
- <div className="micro-app-container" ref={containerRef}>
164
+ <div className="micro-app-container" ref={wrapperRef}>
111
165
  {loading && (
112
166
  <div className="micro-app-loading">
113
- <Spin dot tip={`正在加载 ${label}...`} />
167
+ <Spin dot tip={`正在加载 ${displayName || name}...`} />
114
168
  </div>
115
169
  )}
116
- <div id={containerId} className="micro-app-content" />
117
170
  </div>
118
171
  );
119
172
  };
@@ -0,0 +1,40 @@
1
+ // https://umijs.org/config/
2
+
3
+ import { defineConfig } from '@umijs/max';
4
+ const { CDN_PUBLIC_PATH } = process.env;
5
+
6
+ const PUBLIC_PATH: string = CDN_PUBLIC_PATH
7
+ ? `${CDN_PUBLIC_PATH.replace(/\/?$/, '/')}homepage/`
8
+ : '/homepage/';
9
+
10
+ const config: ReturnType<typeof defineConfig> = {
11
+ // 测试环境:将所有代码打包到一个文件
12
+ extraBabelPlugins: ['babel-plugin-dynamic-import-node'],
13
+
14
+ // 禁用代码分割,只输出一个 JS 和一个 CSS
15
+ chainWebpack(memo) {
16
+ // 禁用 splitChunks
17
+ memo.optimization.splitChunks(false);
18
+ // 禁用 runtimeChunk
19
+ memo.optimization.runtimeChunk(false);
20
+ return memo;
21
+ },
22
+ publicPath: PUBLIC_PATH,
23
+
24
+ /**
25
+ * @name 外部依赖配置
26
+ * @description 将大型公共库排除打包,运行时从主应用获取
27
+ * @doc https://umijs.org/docs/api/config#externals
28
+ *
29
+ * 作为 qiankun 子应用时,这些库由主应用提供:
30
+ * - react / react-dom: 主应用已加载,避免多实例问题
31
+ * - @arco-design/web-react: 主应用已加载,复用组件和样式
32
+ */
33
+ externals: {
34
+ react: 'React',
35
+ 'react-dom': 'ReactDOM',
36
+ '@arco-design/web-react': 'arco',
37
+ },
38
+ };
39
+
40
+ export default defineConfig(config);
@@ -6,8 +6,8 @@
6
6
  "scripts": {
7
7
  "dev": "max dev",
8
8
  "build": "npm run build:production",
9
- "build:development": "cross-env UMI_ENV=dev max build",
10
- "build:production": "cross-env UMI_ENV=prod max build",
9
+ "build:development": "cross-env UMI_ENV=development max build",
10
+ "build:production": "cross-env UMI_ENV=production max build",
11
11
  "build:testing": "cross-env UMI_ENV=testing max build",
12
12
  "postinstall": "max setup",
13
13
  "setup": "max setup",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "generator-mico-cli",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "Yeoman generator for Mico CLI projects",
5
5
  "keywords": [
6
6
  "yeoman-generator",