generator-mico-cli 0.2.2 → 0.2.4

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.
@@ -1,5 +1,11 @@
1
1
  import { layoutLogger } from '@/common/logger';
2
- import { extractRoutes, filterMenuItems, findRouteByPath, getWindowMenus } from '@/common/menu';
2
+ import {
3
+ extractRoutes,
4
+ filterMenuItems,
5
+ findRouteByPath,
6
+ getWindowMenus,
7
+ } from '@/common/menu';
8
+ import { getAppNameFromEntry } from '@/common/micro';
3
9
  import AppTabs from '@/components/AppTabs';
4
10
  import MicroAppLoader from '@/components/MicroAppLoader';
5
11
  import { NO_AUTH_ROUTE_LIST } from '@/constants';
@@ -14,26 +20,6 @@ import './index.less';
14
20
 
15
21
  const { Content } = Layout;
16
22
 
17
- /**
18
- * 从 entry URL 中提取微应用标识
19
- * 同一个 entry 的所有路由使用相同的标识,避免频繁卸载/重载微应用
20
- *
21
- * 注意:使用完整的 origin + pathname 作为标识,避免同一 host 上多个子应用冲突
22
- * 例如:http://localhost:8010/app1/ 和 http://localhost:8010/app2/ 会有不同的标识
23
- */
24
- const getAppNameFromEntry = (entry: string): string => {
25
- try {
26
- const url = new URL(entry, window.location.href);
27
- // 使用 origin + pathname 作为标识,确保不同路径的子应用有不同标识
28
- // 如 "localhost-8010" 或 "localhost-8010-app1"
29
- const identifier = url.host + url.pathname;
30
- return identifier.replace(/[^a-zA-Z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
31
- } catch {
32
- // fallback:使用 entry 的 hash
33
- return entry.replace(/[^a-zA-Z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
34
- }
35
- };
36
-
37
23
  /**
38
24
  * 主布局组件
39
25
  * 注意:Umi Max 使用 React Router 6,应该使用 <Outlet /> 渲染子路由,而不是 children
@@ -42,7 +28,10 @@ const BasicLayout: React.FC = () => {
42
28
  const location = useLocation();
43
29
  const { initialState } = useModel('@@initialState');
44
30
  const currentUser = initialState?.currentUser;
31
+
45
32
  // 路由切换时自动刷新用户权限
33
+ // isRefreshing 状态仅用于显示 loading 覆盖层
34
+ // 实际的时序协调由 loadingCoordinator 在 MicroAppManager 层面处理
46
35
  const { isRefreshing } = useRoutePermissionRefresh();
47
36
 
48
37
  const filterOptions = useMemo(
@@ -103,13 +92,10 @@ const BasicLayout: React.FC = () => {
103
92
  currentRoute,
104
93
  isForbidden,
105
94
  isRefreshing,
95
+ currentUserChanged: currentUser?.is_superuser,
96
+ sideMenusCount: currentUser?.side_menus?.length,
106
97
  });
107
98
 
108
- // 权限刷新中,显示 loading 阻止用户操作
109
- if (isRefreshing) {
110
- return <Spin dot style={{ width: '100%', marginTop: 100, textAlign: 'center' }} />;
111
- }
112
-
113
99
  // 无权限,显示 403
114
100
  if (isForbidden) {
115
101
  return <ForbiddenPage />;
@@ -121,18 +107,45 @@ const BasicLayout: React.FC = () => {
121
107
  // 使用 entry 的 origin 作为微应用标识,同一个 entry 的所有路由共用一个实例
122
108
  const appName = getAppNameFromEntry(currentRoute.entry);
123
109
  return (
124
- <MicroAppLoader
125
- entry={currentRoute.entry}
126
- // 使用 entry 生成的标识,而不是 path
127
- name={appName}
128
- // 显示名称用于 loading 提示
129
- displayName={currentRoute.name}
130
- // 传递当前路由路径,让子应用进行内部路由切换
131
- routePath={currentRoute.path}
132
- />
110
+ <>
111
+ {/* 权限刷新中显示覆盖层,但不卸载 MicroAppLoader,避免子应用被中断 */}
112
+ {isRefreshing && (
113
+ <div style={{
114
+ position: 'absolute',
115
+ top: 0,
116
+ left: 0,
117
+ right: 0,
118
+ bottom: 0,
119
+ display: 'flex',
120
+ alignItems: 'center',
121
+ justifyContent: 'center',
122
+ background: 'var(--color-bg-1)',
123
+ zIndex: 100,
124
+ }}>
125
+ <Spin dot />
126
+ </div>
127
+ )}
128
+ <MicroAppLoader
129
+ // 使用 appName 作为 key,确保不同微应用使用不同的组件实例
130
+ // 同一个微应用的不同路由共用同一个实例
131
+ key={appName}
132
+ entry={currentRoute.entry}
133
+ // 使用 entry 生成的标识,而不是 path
134
+ name={appName}
135
+ // 显示名称用于 loading 提示
136
+ displayName={currentRoute.name}
137
+ // 传递当前路由路径,让子应用进行内部路由切换
138
+ routePath={currentRoute.path}
139
+ />
140
+ </>
133
141
  );
134
142
  }
135
143
 
144
+ // 权限刷新中,显示 loading(仅对非微应用路由)
145
+ if (isRefreshing) {
146
+ return <Spin dot style={{ width: '100%', marginTop: 100, textAlign: 'center' }} />;
147
+ }
148
+
136
149
  // 默认:使用 Outlet 渲染内部路由
137
150
  return <Outlet />;
138
151
  };
@@ -156,13 +169,13 @@ const BasicLayout: React.FC = () => {
156
169
  <Layout className="layout-content-right">
157
170
  <AppTabs />
158
171
  <Content className="layout-content-outlet">
159
- <Suspense
160
- fallback={<Spin dot style={{ width: '100%', marginTop: 100 }} />}
161
- >
162
- {renderContent()}
163
- </Suspense>
164
- </Content>
165
- </Layout>
172
+ <Suspense
173
+ fallback={<Spin dot style={{ width: '100%', marginTop: 100 }} />}
174
+ >
175
+ {renderContent()}
176
+ </Suspense>
177
+ </Content>
178
+ </Layout>
166
179
  </Layout>
167
180
  </Layout>
168
181
  );
@@ -16,7 +16,12 @@ const ForbiddenPage: React.FC = () => {
16
16
  title="无权限访问"
17
17
  subTitle="抱歉,您没有权限访问此页面"
18
18
  extra={
19
- <Button type="primary" onClick={() => history.push('/')}>
19
+ <Button
20
+ type="primary"
21
+ onClick={() => {
22
+ history.push('/');
23
+ }}
24
+ >
20
25
  返回首页
21
26
  </Button>
22
27
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "generator-mico-cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Yeoman generator for Mico CLI projects",
5
5
  "keywords": [
6
6
  "yeoman-generator",
@@ -1,334 +0,0 @@
1
- /**
2
- * 微应用容器管理器
3
- *
4
- * 核心策略:
5
- * 1. 容器在 document.body 中创建,与 React 生命周期解耦
6
- * 2. 激活时移动到占位元素内,参与正常文档流
7
- * 3. 停用时移回 body 并隐藏
8
- * 4. 加载状态锁防止并发加载冲突
9
- * 5. unmount 超时机制防止子应用卡死
10
- *
11
- * 这样既避免了 React unmount 时删除容器导致的竞态问题,
12
- * 又能让容器在激活时参与正常的 CSS 布局。
13
- *
14
- * @see https://github.com/umijs/qiankun/issues/2845
15
- */
16
-
17
- import type { MicroApp } from 'qiankun';
18
-
19
- // ============================================================================
20
- // 类型定义
21
- // ============================================================================
22
-
23
- type ContainerStatus = 'active' | 'hidden' | 'pending-delete';
24
- type LoadingStatus = 'idle' | 'loading' | 'mounted' | 'unmounting';
25
-
26
- interface ContainerEntry {
27
- container: HTMLElement;
28
- microApp: MicroApp | null;
29
- status: ContainerStatus;
30
- loadingStatus: LoadingStatus;
31
- deleteTimer: ReturnType<typeof setTimeout> | null;
32
- /** 当前加载会话 ID,用于取消过期的加载操作 */
33
- loadSessionId: number;
34
- /** 等待卸载完成的 Promise */
35
- unmountPromise: Promise<void> | null;
36
- }
37
-
38
- // ============================================================================
39
- // 常量
40
- // ============================================================================
41
-
42
- /** 容器删除延迟(毫秒)- 给 qiankun 足够时间完成清理 */
43
- const DELETE_DELAY = 5000;
44
-
45
- /** unmount 超时时间(毫秒)- 防止子应用卡死 */
46
- const UNMOUNT_TIMEOUT = 10000;
47
-
48
- /** CSS 类名 */
49
- const CSS_CLASS = {
50
- base: 'micro-app-container-managed',
51
- hidden: 'micro-app-container-managed--hidden',
52
- active: 'micro-app-container-managed--active',
53
- } as const;
54
-
55
- // ============================================================================
56
- // 状态
57
- // ============================================================================
58
-
59
- const containers = new Map<string, ContainerEntry>();
60
-
61
- /** 全局会话 ID 计数器 */
62
- let globalSessionId = 0;
63
-
64
- // ============================================================================
65
- // 工具函数
66
- // ============================================================================
67
-
68
- /**
69
- * 带超时的 Promise
70
- */
71
- function withTimeout<T>(
72
- promise: Promise<T>,
73
- ms: number,
74
- timeoutMessage: string,
75
- ): Promise<T> {
76
- return Promise.race([
77
- promise,
78
- new Promise<T>((_, reject) => {
79
- setTimeout(() => reject(new Error(timeoutMessage)), ms);
80
- }),
81
- ]);
82
- }
83
-
84
- /**
85
- * 安全执行 Promise,忽略错误
86
- */
87
- async function safeAwait(promise: Promise<unknown>): Promise<void> {
88
- try {
89
- await promise;
90
- } catch {
91
- // 忽略
92
- }
93
- }
94
-
95
- // ============================================================================
96
- // 私有方法
97
- // ============================================================================
98
-
99
- function createContainer(appName: string): HTMLElement {
100
- const container = document.createElement('div');
101
- container.id = `micro-app-${appName}`;
102
- container.className = `${CSS_CLASS.base} ${CSS_CLASS.hidden}`;
103
- document.body.appendChild(container);
104
- return container;
105
- }
106
-
107
- function createEntry(container: HTMLElement): ContainerEntry {
108
- return {
109
- container,
110
- microApp: null,
111
- status: 'hidden',
112
- loadingStatus: 'idle',
113
- deleteTimer: null,
114
- loadSessionId: 0,
115
- unmountPromise: null,
116
- };
117
- }
118
-
119
- // ============================================================================
120
- // 公开 API
121
- // ============================================================================
122
-
123
- /**
124
- * 获取或创建容器
125
- */
126
- export function getContainer(appName: string): HTMLElement {
127
- let entry = containers.get(appName);
128
-
129
- if (entry) {
130
- // 取消待删除的计时器(容器被复用)
131
- if (entry.deleteTimer) {
132
- clearTimeout(entry.deleteTimer);
133
- entry.deleteTimer = null;
134
- }
135
- entry.status = 'hidden';
136
- return entry.container;
137
- }
138
-
139
- // 创建新容器
140
- const container = createContainer(appName);
141
- entry = createEntry(container);
142
- containers.set(appName, entry);
143
-
144
- return container;
145
- }
146
-
147
- /**
148
- * 激活容器:移动到占位元素内,参与正常文档流
149
- */
150
- export function activateContainer(appName: string, target: HTMLElement): void {
151
- const entry = containers.get(appName);
152
- if (!entry) return;
153
-
154
- const { container } = entry;
155
-
156
- // 移动容器到占位元素内(参与文档流)
157
- target.appendChild(container);
158
-
159
- // 切换样式
160
- container.classList.remove(CSS_CLASS.hidden);
161
- container.classList.add(CSS_CLASS.active);
162
-
163
- entry.status = 'active';
164
- }
165
-
166
- /**
167
- * 停用容器:移回 body 并隐藏
168
- */
169
- export function deactivateContainer(appName: string): void {
170
- const entry = containers.get(appName);
171
- if (!entry) return;
172
-
173
- const { container } = entry;
174
-
175
- // 移回 body(脱离 React 树)
176
- document.body.appendChild(container);
177
-
178
- // 切换样式
179
- container.classList.remove(CSS_CLASS.active);
180
- container.classList.add(CSS_CLASS.hidden);
181
-
182
- entry.status = 'hidden';
183
- }
184
-
185
- /**
186
- * 开始加载会话
187
- * 返回会话 ID,用于后续检查该会话是否仍然有效
188
- */
189
- export function startLoadSession(appName: string): number {
190
- const entry = containers.get(appName);
191
- if (!entry) return 0;
192
-
193
- const sessionId = ++globalSessionId;
194
- entry.loadSessionId = sessionId;
195
- entry.loadingStatus = 'loading';
196
-
197
- return sessionId;
198
- }
199
-
200
- /**
201
- * 检查加载会话是否仍然有效
202
- * 如果返回 false,说明已经有新的加载请求,当前加载应该取消
203
- */
204
- export function isLoadSessionValid(appName: string, sessionId: number): boolean {
205
- const entry = containers.get(appName);
206
- return entry?.loadSessionId === sessionId;
207
- }
208
-
209
- /**
210
- * 标记加载完成
211
- */
212
- export function markLoadComplete(appName: string, sessionId: number): boolean {
213
- const entry = containers.get(appName);
214
- if (!entry || entry.loadSessionId !== sessionId) {
215
- return false;
216
- }
217
- entry.loadingStatus = 'mounted';
218
- return true;
219
- }
220
-
221
- /**
222
- * 获取当前加载状态
223
- */
224
- export function getLoadingStatus(appName: string): LoadingStatus {
225
- return containers.get(appName)?.loadingStatus ?? 'idle';
226
- }
227
-
228
- /**
229
- * 等待当前卸载操作完成(如果有)
230
- */
231
- export async function waitForUnmount(appName: string): Promise<void> {
232
- const entry = containers.get(appName);
233
- if (entry?.unmountPromise) {
234
- await entry.unmountPromise;
235
- }
236
- }
237
-
238
- /**
239
- * 设置微应用实例
240
- */
241
- export function setMicroApp(appName: string, microApp: MicroApp): void {
242
- const entry = containers.get(appName);
243
- if (entry) {
244
- entry.microApp = microApp;
245
- }
246
- }
247
-
248
- /**
249
- * 获取微应用实例
250
- */
251
- export function getMicroApp(appName: string): MicroApp | null {
252
- return containers.get(appName)?.microApp ?? null;
253
- }
254
-
255
- /**
256
- * 安全卸载微应用并安排容器延迟删除
257
- * 带超时机制,防止子应用 unmount 卡死
258
- */
259
- export async function unmountApp(appName: string): Promise<void> {
260
- const entry = containers.get(appName);
261
- if (!entry) return;
262
-
263
- // 如果已经在卸载中,等待卸载完成
264
- if (entry.unmountPromise) {
265
- await entry.unmountPromise;
266
- return;
267
- }
268
-
269
- const { microApp } = entry;
270
-
271
- if (microApp) {
272
- entry.loadingStatus = 'unmounting';
273
- entry.microApp = null;
274
-
275
- // 创建卸载 Promise 供其他调用方等待
276
- entry.unmountPromise = (async () => {
277
- // 等待各阶段完成再卸载(带超时)
278
- try {
279
- await withTimeout(
280
- Promise.all([
281
- safeAwait(microApp.loadPromise),
282
- safeAwait(microApp.bootstrapPromise),
283
- safeAwait(microApp.mountPromise),
284
- ]),
285
- UNMOUNT_TIMEOUT / 2,
286
- 'Waiting for micro-app lifecycle timeout',
287
- );
288
- } catch (err) {
289
- console.warn(`[container-manager] ${appName} lifecycle wait timeout:`, err);
290
- }
291
-
292
- // 执行卸载(带超时)
293
- try {
294
- await withTimeout(
295
- microApp.unmount(),
296
- UNMOUNT_TIMEOUT / 2,
297
- 'Micro-app unmount timeout',
298
- );
299
- } catch (err) {
300
- console.warn(`[container-manager] ${appName} unmount error/timeout:`, err);
301
- }
302
-
303
- entry.loadingStatus = 'idle';
304
- entry.unmountPromise = null;
305
- })();
306
-
307
- await entry.unmountPromise;
308
- }
309
-
310
- // 安排延迟删除
311
- if (!entry.deleteTimer) {
312
- entry.status = 'pending-delete';
313
- entry.deleteTimer = setTimeout(() => {
314
- deleteContainer(appName);
315
- }, DELETE_DELAY);
316
- }
317
- }
318
-
319
- /**
320
- * 删除容器
321
- */
322
- function deleteContainer(appName: string): void {
323
- const entry = containers.get(appName);
324
- if (!entry) return;
325
-
326
- // 只删除 pending-delete 状态的容器(未被重新激活)
327
- if (entry.status === 'pending-delete') {
328
- entry.container.remove();
329
- containers.delete(appName);
330
- } else {
331
- // 容器被重新激活了,清除计时器引用
332
- entry.deleteTimer = null;
333
- }
334
- }