generator-mico-cli 0.1.28 → 0.2.1
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.
- package/generators/micro-react/templates/.cursor/rules/layout-app.mdc +1 -1
- package/generators/micro-react/templates/.cursor/rules/request-auth.mdc +2 -2
- package/generators/micro-react/templates/.cursor/rules/theme-system.mdc +2 -2
- package/generators/micro-react/templates/CLAUDE.md +27 -11
- package/generators/micro-react/templates/README.md +2 -0
- package/generators/micro-react/templates/apps/layout/config/config.dev.ts +11 -4
- package/generators/micro-react/templates/apps/layout/config/config.prod.development.ts +1 -11
- package/generators/micro-react/templates/apps/layout/config/config.prod.testing.ts +0 -7
- package/generators/micro-react/templates/apps/layout/config/routes.ts +10 -0
- package/generators/micro-react/templates/apps/layout/docs/arch-/346/227/245/345/277/227/344/270/216/345/270/270/351/207/217.md +105 -0
- package/generators/micro-react/templates/apps/layout/docs/arch-/350/257/267/346/261/202/346/250/241/345/235/227.md +17 -15
- package/generators/micro-react/templates/apps/layout/docs/feature-/344/270/273/351/242/230/350/211/262/345/210/207/346/215/242.md +234 -0
- 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 +432 -0
- package/generators/micro-react/templates/apps/layout/docs/feature-/350/217/234/345/215/225/346/235/203/351/231/220/346/216/247/345/210/266.md +175 -0
- package/generators/micro-react/templates/apps/layout/mock/api.mock.ts +15 -15
- package/generators/micro-react/templates/apps/layout/src/app.tsx +43 -28
- package/generators/micro-react/templates/apps/layout/src/common/auth/{cs-auth-manager.ts → auth-manager.ts} +6 -63
- package/generators/micro-react/templates/apps/layout/src/common/auth/index.ts +1 -1
- package/generators/micro-react/templates/apps/layout/src/common/auth/tool.ts +2 -3
- package/generators/micro-react/templates/apps/layout/src/common/auth/type.ts +38 -0
- package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +109 -2
- package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +74 -1
- package/generators/micro-react/templates/apps/layout/src/common/micro/index.ts +0 -8
- package/generators/micro-react/templates/apps/layout/src/common/micro/types.ts +0 -2
- package/generators/micro-react/templates/apps/layout/src/common/request/config.ts +1 -1
- package/generators/micro-react/templates/apps/layout/src/common/request/index.ts +3 -11
- package/generators/micro-react/templates/apps/layout/src/common/request/interceptors.ts +2 -2
- package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +3 -2
- package/generators/micro-react/templates/apps/layout/src/common/request/token-refresh.ts +2 -2
- package/generators/micro-react/templates/apps/layout/src/common/request/url-resolver.ts +32 -48
- package/generators/micro-react/templates/apps/layout/src/components/AppTabs/index.less +1 -1
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +62 -14
- package/generators/micro-react/templates/apps/layout/src/global.less +5 -1
- package/generators/micro-react/templates/apps/layout/src/hooks/useAuth.ts +2 -3
- package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.less +1 -1
- package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.tsx +17 -13
- package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.less +1 -1
- package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +11 -3
- package/generators/micro-react/templates/apps/layout/src/layouts/index.less +1 -1
- package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +52 -8
- package/generators/micro-react/templates/apps/layout/src/pages/403/index.tsx +28 -0
- package/generators/micro-react/templates/apps/layout/src/pages/User/Login/index.less +275 -0
- package/generators/micro-react/templates/apps/layout/src/pages/User/Login/index.tsx +142 -0
- package/generators/micro-react/templates/apps/layout/src/services/auth.ts +1 -0
- package/generators/micro-react/templates/apps/layout/src/services/user.ts +25 -0
- package/generators/micro-react/templates/packages/shared-styles/README.md +16 -14
- package/generators/micro-react/templates/packages/shared-styles/arco-design-mobile-override.less +91 -0
- package/generators/micro-react/templates/packages/shared-styles/arco-override.less +41 -0
- package/generators/micro-react/templates/packages/shared-styles/index.d.ts +44 -0
- package/generators/micro-react/templates/packages/shared-styles/index.less +0 -1
- package/generators/micro-react/templates/packages/shared-styles/package.json +6 -3
- package/generators/micro-react/templates/packages/shared-styles/themes/dark/custom-var.less +118 -74
- package/generators/micro-react/templates/packages/shared-styles/themes/normal/custom-var.less +175 -101
- package/generators/micro-react/templates/packages/shared-styles/variables-only.less +357 -225
- package/generators/micro-react/templates/packages/shared-styles/variables.less +290 -201
- package/generators/subapp-react/templates/homepage/config/config.prod.ts +2 -2
- package/generators/subapp-react/templates/homepage/config/config.ts +6 -0
- package/generators/subapp-react/templates/homepage/mock/api.mock.ts +43 -43
- package/generators/subapp-react/templates/homepage/typings.d.ts +76 -0
- package/package.json +1 -1
- package/generators/micro-react/templates/apps/layout/src/styles/arco-override.less +0 -78
- package/generators/micro-react/templates/apps/layout/src/styles/themes/dark/custom-var.less +0 -244
- package/generators/micro-react/templates/apps/layout/src/styles/themes/normal/custom-var.less +0 -195
- package/generators/micro-react/templates/apps/layout/src/styles/variables.less +0 -5
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
# 微前端模式
|
|
2
|
+
|
|
3
|
+
> 创建时间:2025-12-26
|
|
4
|
+
|
|
5
|
+
## 功能概述
|
|
6
|
+
|
|
7
|
+
基于 qiankun 微前端框架实现动态子应用加载,支持根据菜单配置自动识别并加载外部微应用,实现主应用与子应用的解耦部署。
|
|
8
|
+
|
|
9
|
+
## 技术方案
|
|
10
|
+
|
|
11
|
+
### 技术栈
|
|
12
|
+
|
|
13
|
+
- 框架:React 18 + @umijs/max
|
|
14
|
+
- 微前端:qiankun (通过 @umijs/max 内置插件)
|
|
15
|
+
- UI 组件:Arco Design
|
|
16
|
+
- 路由:React Router 6
|
|
17
|
+
|
|
18
|
+
### 核心实现
|
|
19
|
+
|
|
20
|
+
1. 主应用通过 `window.__MICO_MENUS__` 获取菜单配置
|
|
21
|
+
2. 解析菜单时根据 `htmlUrl` 或 `jsUrls` 判断是否为微应用
|
|
22
|
+
3. 路由匹配时,微应用类型使用 `MicroAppLoader` 组件加载
|
|
23
|
+
4. `MicroAppLoader` 使用 qiankun 的 `loadMicroApp` API 动态挂载子应用
|
|
24
|
+
5. 组件卸载时自动调用 `unmount()` 清理子应用
|
|
25
|
+
|
|
26
|
+
## 文件清单
|
|
27
|
+
|
|
28
|
+
### 核心文件
|
|
29
|
+
|
|
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 配置 |
|
|
38
|
+
|
|
39
|
+
## API / 组件接口
|
|
40
|
+
|
|
41
|
+
### MicroAppLoader
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
interface MicroAppLoaderProps {
|
|
45
|
+
/** 微应用入口 URL */
|
|
46
|
+
entry: string;
|
|
47
|
+
/** 微应用名称 */
|
|
48
|
+
name: string;
|
|
49
|
+
/** 显示名称(用于加载提示) */
|
|
50
|
+
displayName?: string;
|
|
51
|
+
/** 当前路由路径,用于通知子应用进行内部路由切换 */
|
|
52
|
+
routePath?: string;
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**参数说明**:
|
|
57
|
+
|
|
58
|
+
| 参数 | 类型 | 必填 | 说明 |
|
|
59
|
+
|------|------|------|------|
|
|
60
|
+
| entry | string | 是 | qiankun 入口 URL (htmlUrl 或 jsUrls[0]) |
|
|
61
|
+
| name | string | 是 | 微应用唯一标识 |
|
|
62
|
+
| displayName | string | 否 | 显示名称,用于加载提示 |
|
|
63
|
+
| routePath | string | 否 | 当前路由路径,用于子应用内部路由切换 |
|
|
64
|
+
|
|
65
|
+
### MicroAppProps (传递给子应用)
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
interface MicroAppProps {
|
|
69
|
+
/** 主应用标识 */
|
|
70
|
+
mainApp: 'portal-web';
|
|
71
|
+
/** 运行环境 */
|
|
72
|
+
env: 'development' | 'testing' | 'production';
|
|
73
|
+
/** 认证 token */
|
|
74
|
+
authToken: string;
|
|
75
|
+
/** 用户 ID */
|
|
76
|
+
uid: string;
|
|
77
|
+
/** 用户头像 */
|
|
78
|
+
avatar: string;
|
|
79
|
+
/** 用户昵称 */
|
|
80
|
+
nickname: string;
|
|
81
|
+
/** 当前语言 */
|
|
82
|
+
locale: string;
|
|
83
|
+
/** 当前时区 */
|
|
84
|
+
timezone: string;
|
|
85
|
+
/** 在线状态 */
|
|
86
|
+
presenceStatus: string;
|
|
87
|
+
/** 当前路由路径 */
|
|
88
|
+
routePath?: string;
|
|
89
|
+
/** 共享的 request 实例,子应用可直接使用 */
|
|
90
|
+
request: <T = any>(url: string, options?: any) => Promise<T>;
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**说明**:子应用在 `mount` 生命周期中通过 `props` 接收这些数据。
|
|
95
|
+
|
|
96
|
+
### 子应用 Props 更新机制
|
|
97
|
+
|
|
98
|
+
主应用通过 qiankun 的 `update()` 方法实时通知子应用状态变化,无需重新加载:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// MicroAppLoader 内部实现
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!isMountedRef.current) return;
|
|
104
|
+
const microApp = getMicroApp(appName);
|
|
105
|
+
// 当 locale、timezone、presenceStatus、routePath 变化时,调用 update
|
|
106
|
+
microApp?.update?.(buildPropsRef.current());
|
|
107
|
+
}, [appName, locale, timezone, presenceStatus, routePath]);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
子应用需要导出 `update` 生命周期来接收更新:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// 子应用入口
|
|
114
|
+
export async function update(props: MicroAppProps) {
|
|
115
|
+
console.log('micro app updated', props);
|
|
116
|
+
// 响应主应用的状态变化,如切换语言、时区等
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### ParsedRoute
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
interface ParsedRoute {
|
|
124
|
+
path: string;
|
|
125
|
+
name: string;
|
|
126
|
+
icon: string;
|
|
127
|
+
/** 加载类型: internal(内部路由) | microapp(qiankun微应用) */
|
|
128
|
+
loadType: 'internal' | 'microapp';
|
|
129
|
+
/** 微应用入口 URL */
|
|
130
|
+
entry?: string;
|
|
131
|
+
pageConfig?: PageConfig;
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### PageConfig
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
interface PageConfig {
|
|
139
|
+
id: number;
|
|
140
|
+
name: string;
|
|
141
|
+
route: string;
|
|
142
|
+
enabled: boolean;
|
|
143
|
+
/** 微应用 HTML 入口 URL (优先使用) */
|
|
144
|
+
htmlUrl: string | null;
|
|
145
|
+
/** 微应用 JS 入口 URL 列表 (备选) */
|
|
146
|
+
jsUrls: string[];
|
|
147
|
+
/** 微应用 CSS URL 列表 */
|
|
148
|
+
cssUrls: string[];
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 辅助函数
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
/** 判断页面加载类型 */
|
|
156
|
+
function getLoadType(page: MenuItem['page']): 'internal' | 'microapp';
|
|
157
|
+
|
|
158
|
+
/** 获取微应用入口 URL,优先 htmlUrl */
|
|
159
|
+
function getEntry(page: MenuItem['page']): string | undefined;
|
|
160
|
+
|
|
161
|
+
/** 根据路径查找路由配置 */
|
|
162
|
+
function findRouteByPath(
|
|
163
|
+
routes: ParsedRoute[],
|
|
164
|
+
pathname: string,
|
|
165
|
+
): ParsedRoute | undefined;
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## 使用示例
|
|
169
|
+
|
|
170
|
+
### 菜单配置示例
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
// window.__MICO_MENUS__ 数据结构
|
|
174
|
+
const menus: MenuItem[] = [
|
|
175
|
+
{
|
|
176
|
+
id: 1,
|
|
177
|
+
name: '内部页面',
|
|
178
|
+
type: 'page',
|
|
179
|
+
enabled: true,
|
|
180
|
+
page: {
|
|
181
|
+
route: '/dashboard',
|
|
182
|
+
enabled: true,
|
|
183
|
+
htmlUrl: null,
|
|
184
|
+
jsUrls: [],
|
|
185
|
+
cssUrls: [],
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: 2,
|
|
190
|
+
name: '外部微应用',
|
|
191
|
+
type: 'page',
|
|
192
|
+
enabled: true,
|
|
193
|
+
page: {
|
|
194
|
+
route: '/external/*',
|
|
195
|
+
enabled: true,
|
|
196
|
+
htmlUrl: 'http://localhost:8001', // qiankun entry
|
|
197
|
+
jsUrls: [],
|
|
198
|
+
cssUrls: [],
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
];
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### 布局集成
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
// layouts/index.tsx
|
|
208
|
+
const renderContent = () => {
|
|
209
|
+
if (currentRoute?.loadType === 'microapp' && currentRoute.entry) {
|
|
210
|
+
return (
|
|
211
|
+
<MicroAppLoader entry={currentRoute.entry} name={currentRoute.name} />
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
return <Outlet />;
|
|
215
|
+
};
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### 子应用配置要求
|
|
219
|
+
|
|
220
|
+
子应用需要导出 qiankun 生命周期,并可使用主应用共享的 `request` 实例:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// 子应用入口
|
|
224
|
+
import type { MicroAppProps } from '<%= ProjectName %>/layout';
|
|
225
|
+
|
|
226
|
+
// 存储主应用传递的 props
|
|
227
|
+
let mainAppProps: MicroAppProps | null = null;
|
|
228
|
+
|
|
229
|
+
export async function bootstrap() {
|
|
230
|
+
console.log('micro app bootstrapped');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function mount(props: MicroAppProps) {
|
|
234
|
+
console.log('micro app mounted', props);
|
|
235
|
+
// 保存主应用传递的 props
|
|
236
|
+
mainAppProps = props;
|
|
237
|
+
|
|
238
|
+
// 使用主应用的 request 实例发起请求
|
|
239
|
+
const data = await props.request('/api/example');
|
|
240
|
+
console.log('data from main app request:', data);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function unmount() {
|
|
244
|
+
mainAppProps = null;
|
|
245
|
+
console.log('micro app unmounted');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 导出 request 供子应用内部使用
|
|
249
|
+
export const getRequest = () => mainAppProps?.request;
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## 依赖共享机制
|
|
253
|
+
|
|
254
|
+
### 问题背景
|
|
255
|
+
|
|
256
|
+
如果子应用直接安装 React、Arco Design 等大型库,会导致:
|
|
257
|
+
|
|
258
|
+
1. **包体积重复**:React (~140KB) + Arco Design (~800KB) 被打包多次
|
|
259
|
+
2. **样式冲突**:多份 CSS 可能互相覆盖
|
|
260
|
+
3. **React 实例问题**:可能出现 "Invalid hook call" 错误
|
|
261
|
+
|
|
262
|
+
### 解决方案:externals + window 共享
|
|
263
|
+
|
|
264
|
+
主应用将公共库暴露到 `window`,子应用通过 `externals` 配置引用,避免重复打包。
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
268
|
+
│ 主应用 (layout) │
|
|
269
|
+
│ ┌─────────────────────────────────────────────────────────┐│
|
|
270
|
+
│ │ 1. 打包并加载 React、ReactDOM、Arco Design ││
|
|
271
|
+
│ │ 2. 暴露到 window.React / window.ReactDOM / window.arco ││
|
|
272
|
+
│ └─────────────────────────────────────────────────────────┘│
|
|
273
|
+
│ ↓ 共享 │
|
|
274
|
+
│ ┌─────────────────────────────────────────────────────────┐│
|
|
275
|
+
│ │ 子应用 (homepage 等) ││
|
|
276
|
+
│ │ - externals 配置: react → window.React ││
|
|
277
|
+
│ │ - 运行时从 window 获取,不重复打包 ││
|
|
278
|
+
│ │ - 打包体积减少 ~1MB ││
|
|
279
|
+
│ └─────────────────────────────────────────────────────────┘│
|
|
280
|
+
└─────────────────────────────────────────────────────────────┘
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### 主应用配置
|
|
284
|
+
|
|
285
|
+
在 `src/app.tsx` 中暴露共享依赖:
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
// apps/layout/src/app.tsx
|
|
289
|
+
import * as arco from '@arco-design/web-react';
|
|
290
|
+
import React from 'react';
|
|
291
|
+
import ReactDOM from 'react-dom';
|
|
292
|
+
|
|
293
|
+
// 将公共库暴露到 window,供子应用复用
|
|
294
|
+
if (typeof window !== 'undefined') {
|
|
295
|
+
(window as any).React = React;
|
|
296
|
+
(window as any).ReactDOM = ReactDOM;
|
|
297
|
+
(window as any).arco = arco;
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### 子应用配置
|
|
302
|
+
|
|
303
|
+
#### 环境配置策略
|
|
304
|
+
|
|
305
|
+
| 环境 | 配置文件 | 策略 | 说明 |
|
|
306
|
+
| ---- | ---------------- | -------------- | -------------------------- |
|
|
307
|
+
| 开发 | `config.dev.ts` | 直接打包依赖 | 独立运行方便,无需配置 CDN |
|
|
308
|
+
| 生产 | `config.prod.ts` | externals 排除 | 复用主应用依赖,包体积最小 |
|
|
309
|
+
|
|
310
|
+
#### 1. config.prod.ts - 生产环境 externals 配置
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
// apps/[子应用]/config/config.prod.ts
|
|
314
|
+
import { defineConfig } from '@umijs/max';
|
|
315
|
+
|
|
316
|
+
const config: ReturnType<typeof defineConfig> = {
|
|
317
|
+
// 生产环境:将所有代码打包到一个文件
|
|
318
|
+
extraBabelPlugins: ['babel-plugin-dynamic-import-node'],
|
|
319
|
+
|
|
320
|
+
// 禁用代码分割,只输出一个 JS 和一个 CSS
|
|
321
|
+
chainWebpack(memo) {
|
|
322
|
+
memo.optimization.splitChunks(false);
|
|
323
|
+
memo.optimization.runtimeChunk(false);
|
|
324
|
+
return memo;
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* @name 外部依赖配置
|
|
329
|
+
* @description 将大型公共库排除打包,运行时从主应用获取
|
|
330
|
+
*/
|
|
331
|
+
externals: {
|
|
332
|
+
react: 'window.React',
|
|
333
|
+
'react-dom': 'window.ReactDOM',
|
|
334
|
+
'@arco-design/web-react': 'window.arco',
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
export default defineConfig(config);
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
#### 2. config.dev.ts - 开发环境配置
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
// apps/[子应用]/config/config.dev.ts
|
|
345
|
+
import { defineConfig } from '@umijs/max';
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* 开发环境配置
|
|
349
|
+
* - 不配置 externals,直接打包依赖,方便独立运行调试
|
|
350
|
+
* - 生产环境通过 config.prod.ts 配置 externals 复用主应用依赖
|
|
351
|
+
*/
|
|
352
|
+
const config: ReturnType<typeof defineConfig> = {
|
|
353
|
+
publicPath: '/',
|
|
354
|
+
base: '/',
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
export default defineConfig(config);
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
#### 3. package.json - 开发依赖
|
|
361
|
+
|
|
362
|
+
将 Arco Design 放在 `devDependencies`(用于类型提示和开发),生产环境从主应用获取:
|
|
363
|
+
|
|
364
|
+
```json
|
|
365
|
+
{
|
|
366
|
+
"devDependencies": {
|
|
367
|
+
"@arco-design/web-react": "^2.66.6"
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### 子应用使用 Arco Design
|
|
373
|
+
|
|
374
|
+
配置完成后,子应用可以正常导入和使用 Arco Design 组件:
|
|
375
|
+
|
|
376
|
+
```tsx
|
|
377
|
+
// apps/[子应用]/src/pages/index.tsx
|
|
378
|
+
import { Button, Card, Message, Tag } from '@arco-design/web-react';
|
|
379
|
+
|
|
380
|
+
export default function HomePage() {
|
|
381
|
+
return (
|
|
382
|
+
<Card title="子应用示例">
|
|
383
|
+
<Button type="primary" onClick={() => Message.success('成功!')}>
|
|
384
|
+
点击按钮
|
|
385
|
+
</Button>
|
|
386
|
+
<Tag color="arcoblue">标签</Tag>
|
|
387
|
+
</Card>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### 打包体积对比
|
|
393
|
+
|
|
394
|
+
| 配置方式 | 子应用 JS 体积 | 说明 |
|
|
395
|
+
| ------------------ | -------------- | ------------------------ |
|
|
396
|
+
| 直接安装依赖 | ~1.2MB | 包含 React + Arco Design |
|
|
397
|
+
| **externals 共享** | **~50KB** | 仅业务代码 |
|
|
398
|
+
|
|
399
|
+
### 运行环境说明
|
|
400
|
+
|
|
401
|
+
| 环境 | 依赖来源 | 配置文件 |
|
|
402
|
+
| ---------------------- | -------------------- | ---------------- |
|
|
403
|
+
| 开发环境(独立运行) | 直接打包到子应用 | `config.dev.ts` |
|
|
404
|
+
| 生产环境(微前端模式) | 从主应用 window 获取 | `config.prod.ts` |
|
|
405
|
+
|
|
406
|
+
> **设计理念**:开发时优先调试便利性(直接打包),生产时优先性能(externals 共享)。
|
|
407
|
+
|
|
408
|
+
## 注意事项
|
|
409
|
+
|
|
410
|
+
- 微应用路由建议使用通配符 `/path/*` 以支持子路由
|
|
411
|
+
- `htmlUrl` 优先级高于 `jsUrls[0]`
|
|
412
|
+
- 子应用需要配置 CORS 允许主应用域名访问
|
|
413
|
+
- 子应用需要导出 qiankun 标准生命周期函数
|
|
414
|
+
- 组件会在 `entry` 或 `name` 变化时重新加载微应用
|
|
415
|
+
- 加载失败会显示错误信息,便于调试
|
|
416
|
+
- **子应用应使用主应用传递的 `request` 实例**,确保认证 token 和拦截器一致
|
|
417
|
+
- **子应用使用 externals 共享依赖**,避免重复打包 React 和 Arco Design
|
|
418
|
+
|
|
419
|
+
## 相关文档
|
|
420
|
+
|
|
421
|
+
- [主题色切换](./feature-主题色切换.md) - 主题系统实现与子应用适配
|
|
422
|
+
- [请求模块架构](./arch-请求模块.md) - HTTP 请求层模块化设计
|
|
423
|
+
- [日志与常量](./arch-日志与常量.md) - Logger 工具与常量管理
|
|
424
|
+
- [shared-styles 包](../../../packages/shared-styles/README.md) - 共享样式包使用指南
|
|
425
|
+
- [提交规范](../../../docs/commit-message.md) - Git Commit 规范
|
|
426
|
+
|
|
427
|
+
## 加载类型判断逻辑
|
|
428
|
+
|
|
429
|
+
```
|
|
430
|
+
有 htmlUrl 或 jsUrls → microapp (使用 qiankun 加载)
|
|
431
|
+
无 htmlUrl 且无 jsUrls → internal (使用 Outlet 渲染)
|
|
432
|
+
```
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# 菜单权限控制
|
|
2
|
+
|
|
3
|
+
> 创建时间:2026-01-24
|
|
4
|
+
|
|
5
|
+
## 功能概述
|
|
6
|
+
|
|
7
|
+
基于用户信息中的 `side_menus` 字段实现菜单和路由的白名单权限控制。非超级用户只能看到和访问 `side_menus` 中配置的菜单项,访问无权限路由时显示 403 页面。
|
|
8
|
+
|
|
9
|
+
## 技术方案
|
|
10
|
+
|
|
11
|
+
### 技术栈
|
|
12
|
+
|
|
13
|
+
- 框架:React 18 + @umijs/max
|
|
14
|
+
- UI 组件:Arco Design (Result 组件)
|
|
15
|
+
- 状态管理:Umi initialState
|
|
16
|
+
|
|
17
|
+
### 核心实现
|
|
18
|
+
|
|
19
|
+
1. 用户信息接口返回 `side_menus` 字段(白名单)
|
|
20
|
+
2. `filterMenuItems` 根据白名单过滤菜单项
|
|
21
|
+
3. Layout 组件对比"所有路由"和"有权限路由"判断 403
|
|
22
|
+
4. 无权限时在 Layout 内直接渲染 403 组件(无跳转)
|
|
23
|
+
|
|
24
|
+
### 权限判断逻辑
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
用户访问 /some-path
|
|
28
|
+
↓
|
|
29
|
+
是否是超级用户?
|
|
30
|
+
├── 是 → 允许访问所有菜单
|
|
31
|
+
└── 否 → 检查 side_menus 白名单
|
|
32
|
+
├── 精确匹配:menuPath === side_menus[i]
|
|
33
|
+
├── 前缀匹配:side_menus[i].startsWith(menuPath + '.')
|
|
34
|
+
└── 都不匹配 → 禁止访问,显示 403
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 文件清单
|
|
38
|
+
|
|
39
|
+
### 新增文件
|
|
40
|
+
|
|
41
|
+
| 文件路径 | 说明 |
|
|
42
|
+
|----------|------|
|
|
43
|
+
| `src/pages/403/index.tsx` | 403 无权限页面组件 |
|
|
44
|
+
|
|
45
|
+
### 修改文件
|
|
46
|
+
|
|
47
|
+
| 文件路径 | 修改内容 |
|
|
48
|
+
|----------|----------|
|
|
49
|
+
| `src/common/menu/parser.ts` | 新增 `filterMenuItems`、`isMenuAllowed` 等权限过滤函数 |
|
|
50
|
+
| `src/layouts/index.tsx` | 新增 `isForbidden` 判断和 403 渲染逻辑 |
|
|
51
|
+
| `src/layouts/components/menu/index.tsx` | 集成菜单过滤 |
|
|
52
|
+
| `config/routes.ts` | 新增 `/403` 路由 |
|
|
53
|
+
|
|
54
|
+
## API / 组件接口
|
|
55
|
+
|
|
56
|
+
### MenuFilterOptions
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
interface MenuFilterOptions {
|
|
60
|
+
/** 是否是超级用户 */
|
|
61
|
+
isSuperuser?: boolean | number;
|
|
62
|
+
/** 允许访问的菜单路径列表(白名单) */
|
|
63
|
+
sideMenus?: string[];
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### filterMenuItems
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
/**
|
|
71
|
+
* 根据权限过滤菜单项(白名单逻辑)
|
|
72
|
+
*/
|
|
73
|
+
function filterMenuItems(
|
|
74
|
+
items: MenuItem[],
|
|
75
|
+
options?: MenuFilterOptions,
|
|
76
|
+
parentPath?: string,
|
|
77
|
+
): MenuItem[];
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**参数说明**:
|
|
81
|
+
|
|
82
|
+
| 参数 | 类型 | 必填 | 说明 |
|
|
83
|
+
|------|------|------|------|
|
|
84
|
+
| items | MenuItem[] | 是 | 原始菜单数据 |
|
|
85
|
+
| options | MenuFilterOptions | 否 | 过滤选项 |
|
|
86
|
+
| parentPath | string | 否 | 父级路径(递归用) |
|
|
87
|
+
|
|
88
|
+
### isRouteAllowed
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
/**
|
|
92
|
+
* 检查路由是否允许访问
|
|
93
|
+
*/
|
|
94
|
+
function isRouteAllowed(
|
|
95
|
+
menuPath: string,
|
|
96
|
+
options?: MenuFilterOptions,
|
|
97
|
+
): boolean;
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 用户信息相关字段
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
interface IUserInfo {
|
|
104
|
+
is_superuser: boolean | number;
|
|
105
|
+
/** 允许访问的菜单路径列表 */
|
|
106
|
+
side_menus: string[];
|
|
107
|
+
/** 缺失的操作权限(用于按钮级别控制,非菜单) */
|
|
108
|
+
miss_permissions: string[];
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## 使用示例
|
|
113
|
+
|
|
114
|
+
### 用户信息接口返回
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"is_superuser": false,
|
|
119
|
+
"side_menus": ["列队管理.配置队列", "工作台"],
|
|
120
|
+
"miss_permissions": ["列队管理.配置队列.查看"]
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 菜单过滤结果
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
原始菜单:
|
|
128
|
+
├── 工作台 ✅ 精确匹配 "工作台"
|
|
129
|
+
├── 列队管理 ✅ 前缀匹配 "列队管理.配置队列"
|
|
130
|
+
│ └── 配置队列 ✅ 精确匹配 "列队管理.配置队列"
|
|
131
|
+
├── 质量管理 ❌ 不在白名单
|
|
132
|
+
│ └── 抽样检查 ❌ 不在白名单
|
|
133
|
+
└── 权限管理 ❌ 硬编码禁止(非超级用户)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Layout 中的权限判断
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
// layouts/index.tsx
|
|
140
|
+
const isForbidden = useMemo(() => {
|
|
141
|
+
if (currentRoute) return false; // 在有权限的路由中找到了
|
|
142
|
+
const routeInAll = findRouteByPath(allRoutes, location.pathname);
|
|
143
|
+
return !!routeInAll; // 在所有路由中存在但无权限
|
|
144
|
+
}, [currentRoute, allRoutes, location.pathname]);
|
|
145
|
+
|
|
146
|
+
const renderContent = () => {
|
|
147
|
+
if (isForbidden) {
|
|
148
|
+
return <ForbiddenPage />;
|
|
149
|
+
}
|
|
150
|
+
// ... 正常渲染
|
|
151
|
+
};
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## 设计决策
|
|
155
|
+
|
|
156
|
+
| 决策点 | 选择 | 理由 |
|
|
157
|
+
|--------|------|------|
|
|
158
|
+
| 权限模型 | 白名单 (`side_menus`) | 后端返回的 `side_menus` 是允许列表,比黑名单更安全 |
|
|
159
|
+
| 403 处理 | 原地渲染组件 | 保持 URL 不变,用户体验更好,便于分享链接 |
|
|
160
|
+
| 父级菜单显示 | 前缀匹配 | 子菜单有权限时,父级菜单需要作为容器显示 |
|
|
161
|
+
| 超级用户 | 跳过所有检查 | 管理员需要完整访问权限 |
|
|
162
|
+
| 权限管理菜单 | 硬编码禁止 | 非超级用户不应看到权限管理入口 |
|
|
163
|
+
|
|
164
|
+
## 注意事项
|
|
165
|
+
|
|
166
|
+
- `side_menus` 为空时,非超级用户没有任何菜单权限
|
|
167
|
+
- `side_menus` 格式为菜单路径,如 `"列队管理.配置队列"`
|
|
168
|
+
- `miss_permissions` 用于按钮级别权限控制,不影响菜单显示
|
|
169
|
+
- 403 页面在 Layout 内渲染,不会触发路由跳转
|
|
170
|
+
- 调试时可在控制台搜索 `isForbidden check` 查看权限判断日志
|
|
171
|
+
|
|
172
|
+
## 相关文档
|
|
173
|
+
|
|
174
|
+
- [微前端模式](./feature-微前端模式.md) - 路由和菜单解析
|
|
175
|
+
- [用户信息接口](./feature-用户信息接口.md) - 用户数据结构
|
|
@@ -6,17 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
export default {
|
|
8
8
|
// 获取用户信息
|
|
9
|
-
'GET /api/user/info': {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
},
|
|
9
|
+
// 'GET /api/user/info': {
|
|
10
|
+
// success: true,
|
|
11
|
+
// data: {
|
|
12
|
+
// id: 1001,
|
|
13
|
+
// name: '张三',
|
|
14
|
+
// email: 'zhangsan@example.com',
|
|
15
|
+
// avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=homepage',
|
|
16
|
+
// role: 'admin',
|
|
17
|
+
// department: '技术部',
|
|
18
|
+
// },
|
|
19
|
+
// },
|
|
20
20
|
|
|
21
21
|
// 获取统计数据
|
|
22
22
|
'GET /api/dashboard/stats': {
|
|
@@ -37,25 +37,25 @@ export default {
|
|
|
37
37
|
list: [
|
|
38
38
|
{
|
|
39
39
|
id: 1,
|
|
40
|
-
title: '
|
|
40
|
+
title: '待审核内容 A',
|
|
41
41
|
status: 'pending',
|
|
42
42
|
createdAt: '2025-12-27 10:00:00',
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
id: 2,
|
|
46
|
-
title: '
|
|
46
|
+
title: '待审核内容 B',
|
|
47
47
|
status: 'pending',
|
|
48
48
|
createdAt: '2025-12-27 09:30:00',
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
51
|
id: 3,
|
|
52
|
-
title: '
|
|
52
|
+
title: '已通过内容 C',
|
|
53
53
|
status: 'approved',
|
|
54
54
|
createdAt: '2025-12-26 15:00:00',
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
id: 4,
|
|
58
|
-
title: '
|
|
58
|
+
title: '已拒绝内容 D',
|
|
59
59
|
status: 'rejected',
|
|
60
60
|
createdAt: '2025-12-26 14:00:00',
|
|
61
61
|
},
|