@chenhui996/gg-cli 1.0.8 → 1.0.9
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/dist/template/main-app/.env.development +7 -0
- package/dist/template/main-app/.env.production +7 -0
- package/dist/template/{zhiguan → main-app}/README.md +13 -10
- package/dist/template/main-app/docs//345/276/256/345/211/215/347/253/257/346/236/266/346/236/204/350/256/276/350/256/241/344/270/216/345/274/200/345/217/221/346/214/207/345/215/227.md +285 -0
- package/dist/template/{zhiguan → main-app}/docs//351/241/271/347/233/256/347/233/256/345/275/225/347/273/223/346/236/204/350/247/204/350/214/203.md +24 -15
- package/dist/template/{zhiguan → main-app}/package.json +4 -2
- package/dist/template/{zhiguan → main-app}/src/api/user.ts +1 -1
- package/dist/template/main-app/src/components/AuthRoute/index.tsx +25 -0
- package/dist/template/{zhiguan → main-app}/src/layouts/BasicLayout.tsx +15 -7
- package/dist/template/main-app/src/main.tsx +36 -0
- package/dist/template/{zhiguan → main-app}/src/pages/404.test.tsx +1 -1
- package/dist/template/{zhiguan → main-app}/src/pages/home/index.tsx +2 -68
- package/dist/template/main-app/src/pages/login/index.tsx +97 -0
- package/dist/template/main-app/src/pages/micro-app/index.tsx +12 -0
- package/dist/template/{zhiguan → main-app}/src/router/index.tsx +18 -2
- package/dist/template/main-app/src/store/useGlobalStore.ts +29 -0
- package/dist/template/main-app/src/style.less +40 -0
- package/dist/template/main-app/src/utils/request/index.ts +113 -0
- package/dist/template/{zhiguan → main-app}/src/vite-env.d.ts +1 -0
- package/dist/template/{zhiguan → main-app}/vite.config.ts +28 -0
- package/dist/template/micro-app/.editorconfig +16 -0
- package/dist/template/micro-app/.env +1 -0
- package/dist/template/micro-app/.env.test +4 -0
- package/dist/template/micro-app/.prettierignore +34 -0
- package/dist/template/micro-app/.prettierrc +14 -0
- package/dist/template/micro-app/README.md +186 -0
- package/dist/template/micro-app/eslint.config.js +27 -0
- package/dist/template/micro-app/package-lock.json +7274 -0
- package/dist/template/micro-app/package.json +63 -0
- package/dist/template/micro-app/src/api/user.ts +21 -0
- package/dist/template/micro-app/src/assets/Frame 20.png +0 -0
- package/dist/template/micro-app/src/components/Chart/index.tsx +22 -0
- package/dist/template/micro-app/src/components/ErrorBoundary/index.tsx +82 -0
- package/dist/template/micro-app/src/layouts/BasicLayout.tsx +21 -0
- package/dist/template/micro-app/src/main.tsx +47 -0
- package/dist/template/micro-app/src/pages/404.test.tsx +20 -0
- package/dist/template/micro-app/src/pages/404.tsx +32 -0
- package/dist/template/micro-app/src/pages/home/index.less +59 -0
- package/dist/template/micro-app/src/pages/home/index.tsx +165 -0
- package/dist/template/micro-app/src/router/index.tsx +62 -0
- package/dist/template/micro-app/src/setupTests.ts +1 -0
- package/dist/template/micro-app/src/store/useGlobalStore.ts +20 -0
- package/dist/template/micro-app/src/vite-env.d.ts +16 -0
- package/dist/template/{react19 → micro-app}/tsconfig.app.json +7 -1
- package/dist/template/micro-app/vite.config.ts +116 -0
- package/package.json +1 -1
- package/dist/template/react19/README.md +0 -75
- package/dist/template/react19/eslint.config.js +0 -23
- package/dist/template/react19/gitignore +0 -24
- package/dist/template/react19/package.json +0 -34
- package/dist/template/react19/src/App.css +0 -184
- package/dist/template/react19/src/App.tsx +0 -121
- package/dist/template/react19/src/assets/hero.png +0 -0
- package/dist/template/react19/src/assets/vite.svg +0 -1
- package/dist/template/react19/src/index.css +0 -111
- package/dist/template/react19/src/main.tsx +0 -10
- package/dist/template/react19/vite.config.ts +0 -11
- package/dist/template/zhiguan/gitignore +0 -24
- package/dist/template/zhiguan/src/main.tsx +0 -38
- package/dist/template/zhiguan/src/store/useCounterStore.ts +0 -24
- /package/dist/template/{zhiguan → main-app}/.editorconfig +0 -0
- /package/dist/template/{zhiguan → main-app}/.env +0 -0
- /package/dist/template/{zhiguan → main-app}/.env.test +0 -0
- /package/dist/template/{zhiguan → main-app}/.prettierignore +0 -0
- /package/dist/template/{zhiguan → main-app}/.prettierrc +0 -0
- /package/dist/template/{zhiguan → main-app}/docs/Git/345/274/200/345/217/221/350/247/204/350/214/203.md" +0 -0
- /package/dist/template/{zhiguan → main-app}/docs/React/345/274/200/345/217/221/350/247/204/350/214/203.md" +0 -0
- /package/dist/template/{zhiguan → main-app}/docs/TypeScript/345/274/200/345/217/221/350/247/204/350/214/203.md" +0 -0
- /package/dist/template/{zhiguan → main-app}/docs//345/211/215/347/253/257/346/227/245/345/277/227/344/270/216/347/233/221/346/216/247/345/237/213/347/202/271/350/247/204/350/214/203.md" +0 -0
- /package/dist/template/{zhiguan → main-app}/docs//345/215/225/345/205/203/346/265/213/350/257/225/350/247/204/350/214/203.md" +0 -0
- /package/dist/template/{zhiguan → main-app}/eslint.config.js +0 -0
- /package/dist/template/{react19 → main-app}/index.html +0 -0
- /package/dist/template/{react19 → main-app}/public/favicon.svg +0 -0
- /package/dist/template/{react19 → main-app}/public/icons.svg +0 -0
- /package/dist/template/{zhiguan → main-app}/src/assets/Frame 20.png +0 -0
- /package/dist/template/{react19 → main-app}/src/assets/react.svg +0 -0
- /package/dist/template/{zhiguan → main-app}/src/components/Chart/index.tsx +0 -0
- /package/dist/template/{zhiguan → main-app}/src/components/ErrorBoundary/index.tsx +0 -0
- /package/dist/template/{zhiguan → main-app}/src/pages/404.tsx +0 -0
- /package/dist/template/{zhiguan → main-app}/src/pages/about/index.tsx +0 -0
- /package/dist/template/{zhiguan → main-app}/src/pages/calendar/index.tsx +0 -0
- /package/dist/template/{zhiguan → main-app}/src/pages/dashboard/index.tsx +0 -0
- /package/dist/template/{zhiguan → main-app}/src/pages/home/index.less +0 -0
- /package/dist/template/{zhiguan → main-app}/src/pages/settings/index.tsx +0 -0
- /package/dist/template/{zhiguan → main-app}/src/pages/workspace/index.tsx +0 -0
- /package/dist/template/{zhiguan → main-app}/src/setupTests.ts +0 -0
- /package/dist/template/{zhiguan → main-app}/tsconfig.app.json +0 -0
- /package/dist/template/{react19 → main-app}/tsconfig.json +0 -0
- /package/dist/template/{react19 → main-app}/tsconfig.node.json +0 -0
- /package/dist/template/{zhiguan → micro-app}/.env.development +0 -0
- /package/dist/template/{zhiguan → micro-app}/.env.production +0 -0
- /package/dist/template/{zhiguan → micro-app}/index.html +0 -0
- /package/dist/template/{zhiguan → micro-app}/public/favicon.svg +0 -0
- /package/dist/template/{zhiguan → micro-app}/public/icons.svg +0 -0
- /package/dist/template/{zhiguan → micro-app}/src/assets/react.svg +0 -0
- /package/dist/template/{zhiguan → micro-app}/src/style.less +0 -0
- /package/dist/template/{zhiguan → micro-app}/src/utils/request/index.ts +0 -0
- /package/dist/template/{zhiguan → micro-app}/tsconfig.json +0 -0
- /package/dist/template/{zhiguan → micro-app}/tsconfig.node.json +0 -0
|
@@ -18,23 +18,26 @@
|
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
src/
|
|
21
|
-
├── api/ # API 接口定义层
|
|
22
|
-
│ └── user.ts # 用户相关接口示例
|
|
21
|
+
├── api/ # API 接口定义层 (按业务领域集中管理,如 user.ts, task.ts)
|
|
23
22
|
├── assets/ # 静态资源文件 (图片、图标等)
|
|
24
|
-
├── components/ #
|
|
25
|
-
|
|
23
|
+
├── components/ # 全局公共组件库 (跨页面复用,如 Chart, ErrorBoundary)
|
|
24
|
+
├── hooks/ # 全局自定义 Hooks (通用逻辑抽象,如 user.ts、permission.ts)
|
|
26
25
|
├── layouts/ # 布局组件
|
|
27
26
|
│ └── BasicLayout.tsx # 基础布局 (侧边栏 + 顶栏 + 内容区)
|
|
28
|
-
├── pages/ # 页面组件 (
|
|
29
|
-
│ ├── home/ #
|
|
27
|
+
├── pages/ # 页面组件 (Feature-based 高内聚架构)
|
|
28
|
+
│ ├── home/ # 首页模块
|
|
29
|
+
│ │ ├── components/ # 页面级专属组件 (就近原则)
|
|
30
|
+
│ │ ├── hooks.ts # 页面级专属逻辑 (就近原则)
|
|
31
|
+
│ │ ├── store.ts # 页面级专属状态 (就近原则)
|
|
32
|
+
│ │ └── index.tsx # 首页入口
|
|
30
33
|
│ ├── dashboard/ # 数据看板 (ECharts 示例)
|
|
31
34
|
│ └── about/ # 关于页面
|
|
32
35
|
├── router/ # 路由配置
|
|
33
36
|
│ └── index.tsx # 路由表定义
|
|
34
|
-
├── store/ # 全局状态管理 (Zustand)
|
|
35
|
-
│ └──
|
|
36
|
-
├── utils/ #
|
|
37
|
-
│ └── request/ # Axios 二次封装
|
|
37
|
+
├── store/ # 全局状态管理 (Zustand - 仅存 Auth/App 等跨页面级状态)
|
|
38
|
+
│ └── user.ts # 例:user.ts、permission.ts
|
|
39
|
+
├── utils/ # 全局工具函数及基础设施
|
|
40
|
+
│ └── request/ # 例:Axios 二次封装
|
|
38
41
|
├── main.tsx # 应用入口
|
|
39
42
|
├── App.tsx # 根组件
|
|
40
43
|
└── vite-env.d.ts # Vite 环境变量类型声明
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# 微前端架构设计与开发指南
|
|
2
|
+
|
|
3
|
+
本文档旨在帮助团队成员快速理解和上手我们项目的微前端架构。无论你之前是否接触过微前端,阅读本文档后,你将能够清晰地了解项目的运行机制、主子应用的职责边界以及常见坑点的解决方案。
|
|
4
|
+
|
|
5
|
+
## 一、架构概览
|
|
6
|
+
|
|
7
|
+
本项目采用 **Garfish** 作为微前端底层框架。整体架构采用 **“基座(主应用) + 业务模块(子应用)”** 的模式。
|
|
8
|
+
|
|
9
|
+
- **主应用 (`main-app`)**:负责全局基础能力的提供。包括:系统整体 Layout 布局(侧边栏、顶栏)、用户登录与鉴权、微应用注册与调度、全局状态分发。
|
|
10
|
+
- **子应用 (`micro-app` 等)**:聚焦于具体的业务线逻辑。它们是完全独立的前端项目,支持独立开发、独立部署,但在用户体验上,它们会被无缝嵌入到主应用的界面中。
|
|
11
|
+
- **技术栈统一**:目前主子应用均采用 `React 19` + `Vite 8` + `React Router v7` 的现代化技术栈。
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 二、主应用 (基座) 的核心机制
|
|
16
|
+
|
|
17
|
+
主应用是整个系统的调度中心,它的核心配置主要在入口文件和路由中。
|
|
18
|
+
|
|
19
|
+
### 1. 子应用注册 (`main.tsx`)
|
|
20
|
+
在主应用的 `main.tsx` 中,我们通过 `Garfish.run` 初始化微前端环境,并注册了子应用清单。
|
|
21
|
+
- `basename: '/'`:主应用的路由基准路径。
|
|
22
|
+
- `domGetter: '#subApp'`:这是关键,它告诉 Garfish:“请把子应用渲染到 ID 为 `subApp` 的 DOM 节点里”。
|
|
23
|
+
- `apps` 数组:定义了子应用的名称 (`name`)、激活路由 (`activeWhen`) 和入口地址 (`entry`)。
|
|
24
|
+
- `sandbox: false`:在 Vite 开发环境下通常关闭沙箱,以支持原生的 ES Module 加载。
|
|
25
|
+
|
|
26
|
+
### 2. 路由防冲突设计 (`router/index.tsx`)
|
|
27
|
+
**坑点**:如果主应用没有匹配子应用的路由规则,React Router 会默认命中 `*` (404 页面)。
|
|
28
|
+
**解决方案**:我们在主应用路由中,为微应用配置了一个专属的通配符路由:
|
|
29
|
+
```tsx
|
|
30
|
+
{
|
|
31
|
+
path: 'micro-react-1/*',
|
|
32
|
+
element: withSuspense(MicroAppContainer), // 渲染包含 <div id="subApp"></div> 的组件
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
这样,当访问 `/micro-react-1/xxx` 时,主应用负责渲染出挂载容器,Garfish 负责把子应用塞进去,两者完美配合。
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 三、子应用 (业务模块) 的开发规范
|
|
40
|
+
|
|
41
|
+
子应用要想顺利在基座中运行,必须遵循一定的接入规范,尤其是 Vite 环境下的配置。
|
|
42
|
+
|
|
43
|
+
### 1. 导出生命周期 (`main.tsx`)
|
|
44
|
+
子应用不能像普通应用那样直接 `createRoot().render()`。必须通过 `@garfish/bridge-react-v18` 包装并 `export const provider`,向主应用暴露 `render` 和 `destroy` 等生命周期钩子:
|
|
45
|
+
```tsx
|
|
46
|
+
export const provider = reactBridge({
|
|
47
|
+
el: '#root',
|
|
48
|
+
rootComponent: () => <RouterProvider router={router} />,
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
*注:我们在 `index.html` 中保留了兜底逻辑,通过判断 `!window.__GARFISH__`,保证子应用依然可以脱离主应用**独立运行和调试**。*
|
|
52
|
+
|
|
53
|
+
### 2. 动态路由 Basename (`router/index.tsx`)
|
|
54
|
+
子应用在独立运行时,路由前缀是 `/`;而在主应用中挂载时,路由前缀必须加上主应用分配的路径(如 `/micro-react-1`),否则路由匹配会失效。
|
|
55
|
+
```tsx
|
|
56
|
+
const basename = window.__GARFISH__ ? '/micro-react-1' : '/';
|
|
57
|
+
export const router = createBrowserRouter(routes, { basename });
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 3. Layout 嵌套剔除 (`BasicLayout.tsx`)
|
|
61
|
+
为了防止在主应用中挂载时出现“侧边栏套侧边栏”的奇葩 UI,子应用需要在其 Layout 组件中做环境判断。如果是微前端环境,仅渲染 `<Outlet />` 业务内容。
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 四、🔥 Vite 微前端必配三剑客 (避坑指南)
|
|
66
|
+
|
|
67
|
+
由于 Vite 在开发环境下使用 ESM,生产环境下使用 Rollup,在微前端集成时存在极易踩坑的“资源 404”问题。子应用的 `vite.config.ts` 必须严格包含以下三大配置:
|
|
68
|
+
|
|
69
|
+
### 1. `base` (应对生产环境静态资源)
|
|
70
|
+
```typescript
|
|
71
|
+
base: 'http://localhost:3001/', // 必须是完整的绝对路径
|
|
72
|
+
```
|
|
73
|
+
**作用**:告诉 Vite 在 `npm run build` 打包时,将 `index.html` 里的 CSS、JS 引用路径写死为子应用的域名。否则使用相对路径会导致去主应用域名下请求资源而报 404。
|
|
74
|
+
|
|
75
|
+
### 2. `server.origin` (应对开发环境 HMR 和图片)
|
|
76
|
+
```typescript
|
|
77
|
+
server: {
|
|
78
|
+
port: 3001,
|
|
79
|
+
cors: true,
|
|
80
|
+
origin: 'http://localhost:3001',
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
**作用**:在开发阶段,强制 Vite 内部生成的图片 URL 和 WebSocket 热更新链接指向子应用的 3001 端口,而不是跟随浏览器地址栏跑去主应用端口。
|
|
84
|
+
|
|
85
|
+
### 3. `vite-plugin-garfish-mf` (应对 JS 动态按需加载)
|
|
86
|
+
```typescript
|
|
87
|
+
import garfishPlugin from 'vite-plugin-garfish-mf'
|
|
88
|
+
|
|
89
|
+
plugins: [
|
|
90
|
+
garfishPlugin({ base: "http://localhost:3001" })
|
|
91
|
+
]
|
|
92
|
+
```
|
|
93
|
+
**作用**:`React.lazy()` 懒加载组件底层使用的是 `import('./chunk.js')`。这是一个运行时发出的原生请求,会默认带上主应用的域名。该插件能在编译时劫持并修正这些动态 import 的路径,彻底消除 `Unexpected Application Error!` 的路由报错。
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 五、主子应用状态通信 (Props 透传)
|
|
98
|
+
|
|
99
|
+
为了实现免登体验和全局状态共享,我们采用 **Garfish Props + 子应用 Zustand** 的方案进行单向数据流通信。
|
|
100
|
+
|
|
101
|
+
### 1. 主应用下发数据
|
|
102
|
+
在主应用的 `Garfish.run` 配置中,利用 `props` 字段将当前的用户信息、Token 等全局状态注入给子应用:
|
|
103
|
+
```tsx
|
|
104
|
+
// main-app/src/main.tsx
|
|
105
|
+
apps: [
|
|
106
|
+
{
|
|
107
|
+
name: 'react',
|
|
108
|
+
// ...
|
|
109
|
+
props: {
|
|
110
|
+
globalState: {
|
|
111
|
+
user: { name: '国泰海通-张三', role: 'admin', dept: '交易部' },
|
|
112
|
+
token: 'Bearer mock-jwt-token-12345',
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 2. 子应用拦截并同步状态
|
|
120
|
+
子应用不应直接在组件层层传递 props,而应该在入口处拦截,并将其存入自身的全局状态管理库 (Zustand) 中,以便于脱离主应用时依然能独立运作。
|
|
121
|
+
|
|
122
|
+
**坑点提醒**:`@garfish/bridge-react-v18` 的 `rootComponent` 接收到的参数是**完整的应用上下文对象**,我们自定义的 `props` 嵌套在 `appInfo.props` 下,切勿直接解构报错。
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
// micro-app/src/main.tsx
|
|
126
|
+
export const provider = reactBridge({
|
|
127
|
+
el: '#root',
|
|
128
|
+
rootComponent: (appInfo: any) => {
|
|
129
|
+
// 注意:自定义数据在 appInfo.props 下
|
|
130
|
+
const hostGlobalState = appInfo?.props?.globalState;
|
|
131
|
+
if (hostGlobalState) {
|
|
132
|
+
// 同步到子应用的 Zustand Store 中
|
|
133
|
+
useGlobalStore.getState().setGlobalState(hostGlobalState);
|
|
134
|
+
}
|
|
135
|
+
return <RouterProvider router={router} />;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
随后,子应用内部的任何页面和网络请求,都可以直接从自身的 `useGlobalStore` 中读取用户信息和 Token。
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 六、样式隔离实践 (CSS Isolation)
|
|
144
|
+
|
|
145
|
+
由于目前为了支持 Vite 的原生 ESM 模块,我们在主应用中关闭了 Garfish 的沙箱 (`sandbox: false`)。这也意味着主子应用共享同一个 `document.head` 和 `window`。
|
|
146
|
+
|
|
147
|
+
如果不做处理,主子应用由于都使用 Ant Design (默认前缀 `.ant-`),它们的 CSS-in-JS 样式极易互相覆盖,导致极其诡异的 UI 错位!
|
|
148
|
+
|
|
149
|
+
### Ant Design v5/v6 样式隔离方案 (v2)
|
|
150
|
+
|
|
151
|
+
在微前端环境下,主子应用不仅会互相覆盖 CSS 类名,还会因为 Ant Design v5 默认的 **CSS Variables(CSS 变量)** 机制产生严重的副作用(例如子应用卸载时带走了全局的 `--ant-font-size` 等等,会导致主应用字体突然变大等问题)。
|
|
152
|
+
|
|
153
|
+
为了实现 **类名隔离** 和 **CSS 变量作用域隔离** 的双重保险,我们需要在主应用和子应用中都对 `ConfigProvider` 进行以下终极配置:
|
|
154
|
+
|
|
155
|
+
**1. 主应用 (`main-app/src/main.tsx`):**
|
|
156
|
+
```tsx
|
|
157
|
+
// 为主应用分配独立的 CSS Variable Key
|
|
158
|
+
<ConfigProvider theme={{ cssVar: { key: 'main-app' }, hashed: false }}>
|
|
159
|
+
<RouterProvider router={router} />
|
|
160
|
+
</ConfigProvider>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**2. 子应用 (`micro-app/src/main.tsx`):**
|
|
164
|
+
```tsx
|
|
165
|
+
import { ConfigProvider } from 'antd';
|
|
166
|
+
|
|
167
|
+
const MICRO_PREFIX_CLS = 'micro1'; // 子应用专属前缀
|
|
168
|
+
|
|
169
|
+
export const provider = reactBridge({
|
|
170
|
+
el: '#root',
|
|
171
|
+
rootComponent: (appInfo: any) => {
|
|
172
|
+
// ...状态同步逻辑
|
|
173
|
+
|
|
174
|
+
// 1. 设置专属的 prefixCls 隔离基础类名
|
|
175
|
+
// 2. 设置独立的 cssVar key 隔离 CSS 变量作用域
|
|
176
|
+
return (
|
|
177
|
+
<ConfigProvider
|
|
178
|
+
prefixCls={MICRO_PREFIX_CLS}
|
|
179
|
+
theme={{ cssVar: { key: 'micro-app' }, hashed: false }}
|
|
180
|
+
>
|
|
181
|
+
<RouterProvider router={router} />
|
|
182
|
+
</ConfigProvider>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**效果**:
|
|
189
|
+
- `prefixCls`:保证了子应用的类名从 `.ant-btn` 变成 `.micro1-btn`,防止基础样式覆盖。
|
|
190
|
+
- `cssVar: { key: 'xxx' }`:强制 Ant Design 将全局 CSS 变量(如 `--ant-font-size`)限制在各自独立的块中。当子应用卸载时,只会销毁自己那块变量,主应用的变量安然无恙,彻底消除了切换应用时的**UI 抖动和样式丢失问题**。
|
|
191
|
+
|
|
192
|
+
### Design Tokens (CSS-in-JS) 隔离机制
|
|
193
|
+
除了 `prefixCls`,Ant Design v5+ 引入的 Design Tokens(`theme.token`)机制也为微前端样式隔离提供了天然保障:
|
|
194
|
+
|
|
195
|
+
- **动态哈希值**:当在 `ConfigProvider` 中配置了各自独立的 `theme` 后,Antd 底层会根据主题配置对象计算出唯一的哈希类名(如 `.css-1hashxyz`)。
|
|
196
|
+
- **无冲突定制**:这意味着主应用可以配置一套“蓝色主题”,而子应用可以自由配置一套“绿色主题”。两者在同一个页面渲染时,由于哈希值不同,生成的 CSS-in-JS 样式天然互不干扰。
|
|
197
|
+
|
|
198
|
+
**⚠️ 自定义样式避坑**:
|
|
199
|
+
如果你在子应用中编写了针对 Ant Design 组件的全局覆盖样式(如在 `index.less` 中),请务必使用你配置的前缀(如 `.micro1-btn`)而不是 `.ant-btn`,否则样式将无法生效!
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## 七、公共依赖抽离 (CDN + External)
|
|
204
|
+
|
|
205
|
+
为了降低微前端架构下各个子应用重复打包导致的总体积膨胀,我们将体积较大的核心依赖(如 React、React-DOM、Ant Design 等)进行抽离,通过 CDN 统一加载,实现主子应用实例共享。
|
|
206
|
+
|
|
207
|
+
### Vite CDN 插件配置
|
|
208
|
+
我们在主应用和子应用中均引入了 `vite-plugin-cdn-import` 插件。该插件能在 **生产环境构建 (`npm run build`)** 时,自动:
|
|
209
|
+
1. 将指定的包设为 External,不打入最终产物。
|
|
210
|
+
2. 自动在生成的 `index.html` 中注入对应的 CDN `<script>` 标签。
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
// vite.config.ts
|
|
214
|
+
import cdn from 'vite-plugin-cdn-import'
|
|
215
|
+
|
|
216
|
+
export default defineConfig({
|
|
217
|
+
plugins: [
|
|
218
|
+
cdn({
|
|
219
|
+
modules: [
|
|
220
|
+
{ name: 'react', var: 'React', path: `https://unpkg.com/react@19.2.4/umd/react.production.min.js` },
|
|
221
|
+
{ name: 'react-dom', var: 'ReactDOM', path: `https://unpkg.com/react-dom@19.2.4/umd/react-dom.production.min.js` },
|
|
222
|
+
// Ant Design v5+ 依赖 dayjs,需一并抽离
|
|
223
|
+
{ name: 'dayjs', var: 'dayjs', path: `https://unpkg.com/dayjs@1.11.10/dayjs.min.js` },
|
|
224
|
+
{ name: 'antd', var: 'antd', path: `https://unpkg.com/antd@6.3.3/dist/antd.min.js` }
|
|
225
|
+
]
|
|
226
|
+
})
|
|
227
|
+
]
|
|
228
|
+
})
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**注意事项**:
|
|
232
|
+
- 该插件默认只在**生产构建 (build)**阶段生效。在本地开发 (`npm run dev`) 阶段,Vite 依然会使用本地 `node_modules` 进行预构建,这保证了极佳的本地 HMR(热更新)开发体验。
|
|
233
|
+
- 子应用由于同样配置了该插件,打包出来的代码不再包含 React 和 Antd 源码,从而使得单个子应用的产物体积大幅缩减(通常能减小 1MB 以上)。当主应用加载子应用时,由于主应用的 `window.React` 已经由 CDN 挂载好,子应用会直接复用主应用的 React 实例,实现真正的共享!
|
|
234
|
+
|
|
235
|
+
### 独立部署/iframe 接入的兼容性问题
|
|
236
|
+
**疑问**:如果子应用公共库被抽离了,那未来子应用需要单独部署给客户,或者通过 `iframe` 嵌入到第三方系统里,还能正常运行吗?
|
|
237
|
+
|
|
238
|
+
**答案是:完全没问题!**
|
|
239
|
+
当子应用被独立部署或通过 iframe 访问时,浏览器请求的是子应用自己的 `index.html`。
|
|
240
|
+
由于 `vite-plugin-cdn-import` 插件不仅在打包时剔除了源码,还会**自动在子应用的 `index.html` 中注入那些 CDN 的 `<script>` 标签**。
|
|
241
|
+
- **微前端挂载时**:Garfish 默认会拦截并执行子应用 HTML 中的 script。如果它发现主应用 `window` 上已经有了 `React`,它会直接复用(或者按照沙箱策略处理),达到共享的目的。
|
|
242
|
+
- **独立/iframe 运行时**:浏览器会老老实实地解析子应用的 `index.html`,去下载 CDN 上的 React 和 Antd 脚本并挂载到自己的 `window` 上,所以子应用依然是一个**五脏俱全、能够独立自举**的完整应用。
|
|
243
|
+
|
|
244
|
+
### 关于 CDN 域名 (如 unpkg.com) 的特别说明
|
|
245
|
+
在上述配置中,你可能会注意到我们使用了 `https://unpkg.com/...` 这样的链接。
|
|
246
|
+
|
|
247
|
+
**什么是 unpkg.com?**
|
|
248
|
+
- 它是一个由开源社区维护的**免费、公共的全球 CDN 静态资源加速网络**。只要 npm 上发布过的包,它都会自动抓取并提供 CDN 链接服务。
|
|
249
|
+
- 它**不是** Vite 默认生成的,而是我们在 `vite.config.ts` 的 `cdn({ modules: [{ path: '...' }] })` 配置中**手动指定**的。
|
|
250
|
+
|
|
251
|
+
**生产环境的最佳实践建议**:
|
|
252
|
+
虽然 `unpkg.com` 或 `jsdelivr.net` 这样的公共 CDN 在个人项目或开源项目中非常好用,但在**企业级商业项目**中,我们**强烈不建议直接在生产环境使用公共 CDN**,原因如下:
|
|
253
|
+
1. **稳定性风险**:公共 CDN 偶尔会因为网络波动或不可抗力导致中国大陆地区访问缓慢甚至宕机,这会直接导致你的整个微前端系统白屏瘫痪。
|
|
254
|
+
2. **安全风险**:公共 CDN 不受企业自身管控。
|
|
255
|
+
|
|
256
|
+
**我们该怎么做? (私有化 CDN)**
|
|
257
|
+
当项目准备正式上线部署时,运维或前端架构组成员应该:
|
|
258
|
+
1. 将 `react.production.min.js`、`antd.min.js` 等依赖包的构建产物下载下来。
|
|
259
|
+
2. 传到**公司自己的私有 CDN 服务器**(如阿里云 OSS、腾讯云 OSS 等)或静态资源服务器上。
|
|
260
|
+
3. 将 `vite.config.ts` 里面的 `path` 替换成公司内部的真实 CDN 地址:
|
|
261
|
+
```tsx
|
|
262
|
+
{ name: 'react', var: 'React', path: `https://static.your-company.com/libs/react@19.2.4/react.production.min.js` }
|
|
263
|
+
```
|
|
264
|
+
这样,你的微前端架构既享受了包体积抽离和共享带来的性能红利,又保证了企业级的高可用和高安全性。
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## 八、后续演进方向
|
|
269
|
+
目前的基建已经涵盖了路由、通信、样式隔离与依赖抽离。为了达到业界顶尖的微前端体验和工程化水平,未来可从以下五个方向继续演进:
|
|
270
|
+
|
|
271
|
+
1. **路由保活 (Keep-Alive)**
|
|
272
|
+
- **痛点**:目前在主子应用菜单间切换时,子应用会被销毁,导致用户填写的表单或浏览状态丢失。
|
|
273
|
+
- **方案**:利用 Garfish 的多实例缓存机制(或 React Router 结合 Offscreen API),在路由切换时仅隐藏 DOM 而不卸载组件实例。
|
|
274
|
+
2. **资源预加载 (Preload)**
|
|
275
|
+
- **痛点**:首次点击子应用菜单时需实时请求 JS/CSS 资源,网络较差时会出现白屏。
|
|
276
|
+
- **方案**:在主应用启动并处于浏览器空闲期(Idle)时,通过 Garfish 预加载策略静默下载子应用静态资源,实现页面“秒开”。
|
|
277
|
+
3. **全局消息总线 (Event Bus)**
|
|
278
|
+
- **痛点**:目前仅支持主应用向下单向派发 Props,子应用若需主动通知主应用(如 Token 失效请求重新登录)缺乏标准通道。
|
|
279
|
+
- **方案**:引入 `Garfish.channel` 或自定义 EventEmitter,建立全局发布订阅机制,实现主子应用双向解耦通信。
|
|
280
|
+
4. **全局错误监控与白屏降级**
|
|
281
|
+
- **痛点**:子应用加载失败或 JS 报错可能导致整个界面卡死,缺乏统一的监控和优雅降级。
|
|
282
|
+
- **方案**:在主应用中集成统一监控 SDK(如 Sentry),捕获加载异常并渲染友好的 404/500 兜底组件。
|
|
283
|
+
5. **动态下发应用配置 (配置中心)**
|
|
284
|
+
- **痛点**:子应用清单(入口地址、激活路由等)目前硬编码在主应用中,新增子应用需重新发版主应用。
|
|
285
|
+
- **方案**:主应用启动时,通过接口向“配置中心”动态拉取当前用户有权限访问的子应用列表,实现微应用注册的完全动态化和解耦。
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
项目根目录存放工程化配置、环境变量文件以及源代码目录。
|
|
8
8
|
|
|
9
|
-
```
|
|
9
|
+
```bash
|
|
10
10
|
├── .env.development # 本地开发环境变量配置
|
|
11
11
|
├── .env.test # 测试环境变量配置
|
|
12
12
|
├── .env.production # 生产环境变量配置
|
|
@@ -26,21 +26,30 @@
|
|
|
26
26
|
|
|
27
27
|
所有业务代码、静态资源和配置文件都应放置在 `src` 目录下,并严格按照功能模块进行划分。
|
|
28
28
|
|
|
29
|
-
```
|
|
29
|
+
```bash
|
|
30
30
|
src/
|
|
31
|
-
├── api/ # API
|
|
32
|
-
├── assets/ # 静态资源文件
|
|
33
|
-
├── components/ #
|
|
34
|
-
├── hooks/ # 全局自定义
|
|
35
|
-
├── layouts/ #
|
|
36
|
-
|
|
37
|
-
├──
|
|
38
|
-
├──
|
|
39
|
-
├──
|
|
40
|
-
├──
|
|
41
|
-
├──
|
|
42
|
-
|
|
43
|
-
├──
|
|
31
|
+
├── api/ # API 接口定义层 (按业务领域集中管理,如 user.ts, task.ts)
|
|
32
|
+
├── assets/ # 静态资源文件 (图片、图标等)
|
|
33
|
+
├── components/ # 全局公共组件库 (跨页面复用,如 Chart, ErrorBoundary)
|
|
34
|
+
├── hooks/ # 全局自定义 Hooks (通用逻辑抽象,如 user.ts、permission.ts)
|
|
35
|
+
├── layouts/ # 布局组件
|
|
36
|
+
│ └── BasicLayout.tsx # 基础布局 (侧边栏 + 顶栏 + 内容区)
|
|
37
|
+
├── pages/ # 页面组件 (Feature-based 高内聚架构)
|
|
38
|
+
│ ├── home/ # 首页模块
|
|
39
|
+
│ │ ├── components/ # 页面级专属组件 (就近原则)
|
|
40
|
+
│ │ ├── hooks.ts # 页面级专属逻辑 (就近原则)
|
|
41
|
+
│ │ ├── store.ts # 页面级专属状态 (就近原则)
|
|
42
|
+
│ │ └── index.tsx # 首页入口
|
|
43
|
+
│ ├── dashboard/ # 数据看板 (ECharts 示例)
|
|
44
|
+
│ └── about/ # 关于页面
|
|
45
|
+
├── router/ # 路由配置
|
|
46
|
+
│ └── index.tsx # 路由表定义
|
|
47
|
+
├── store/ # 全局状态管理 (Zustand - 仅存 Auth/App 等跨页面级状态)
|
|
48
|
+
│ └── user.ts # 例:user.ts、permission.ts
|
|
49
|
+
├── utils/ # 全局工具函数及基础设施
|
|
50
|
+
│ └── request/ # 例:Axios 二次封装
|
|
51
|
+
├── main.tsx # 应用入口
|
|
52
|
+
├── App.tsx # 根组件
|
|
44
53
|
└── vite-env.d.ts # Vite 环境变量类型声明
|
|
45
54
|
```
|
|
46
55
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "main-app",
|
|
3
3
|
"private": true,
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "vite --mode development",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"axios": "^1.13.6",
|
|
23
23
|
"echarts": "^6.0.0",
|
|
24
24
|
"echarts-for-react": "^3.0.6",
|
|
25
|
+
"garfish": "^1.19.8",
|
|
25
26
|
"react": "^19.2.4",
|
|
26
27
|
"react-dom": "^19.2.4",
|
|
27
28
|
"react-router-dom": "^7.13.1",
|
|
@@ -55,6 +56,7 @@
|
|
|
55
56
|
"typescript": "~5.9.3",
|
|
56
57
|
"typescript-eslint": "^8.56.1",
|
|
57
58
|
"vite": "^8.0.0",
|
|
59
|
+
"vite-plugin-cdn-import": "^1.0.1",
|
|
58
60
|
"vitest": "^4.1.4"
|
|
59
61
|
}
|
|
60
62
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Navigate, useLocation } from 'react-router-dom';
|
|
2
|
+
import { useGlobalStore } from '@/store/useGlobalStore';
|
|
3
|
+
|
|
4
|
+
interface AuthRouteProps {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 路由守卫组件 (AuthRoute)
|
|
10
|
+
* 拦截未登录用户的访问请求,重定向到登录页面
|
|
11
|
+
*/
|
|
12
|
+
export default function AuthRoute({ children }: AuthRouteProps) {
|
|
13
|
+
// 从全局 Store 获取 token
|
|
14
|
+
const token = useGlobalStore((state) => state.token);
|
|
15
|
+
const location = useLocation();
|
|
16
|
+
|
|
17
|
+
// 如果没有 token,说明未登录,重定向到登录页
|
|
18
|
+
// 并通过 state 记录用户原本想访问的页面,以便登录后跳回
|
|
19
|
+
if (!token) {
|
|
20
|
+
return <Navigate to="/login" state={{ from: location }} replace />;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 已经登录,放行,渲染原本的子组件
|
|
24
|
+
return <>{children}</>;
|
|
25
|
+
}
|
|
@@ -31,6 +31,11 @@ const MENU_ITEMS = [
|
|
|
31
31
|
icon: <AppstoreOutlined />,
|
|
32
32
|
label: '功能大厅',
|
|
33
33
|
},
|
|
34
|
+
{
|
|
35
|
+
key: '/micro-react-1',
|
|
36
|
+
icon: <AppstoreOutlined />,
|
|
37
|
+
label: 'React19 子应用',
|
|
38
|
+
},
|
|
34
39
|
{
|
|
35
40
|
key: '/workspace', // 暂未实现
|
|
36
41
|
icon: <FolderOutlined />,
|
|
@@ -84,13 +89,14 @@ export default function BasicLayout() {
|
|
|
84
89
|
);
|
|
85
90
|
|
|
86
91
|
return (
|
|
87
|
-
<Layout style={{
|
|
92
|
+
<Layout style={{ height: '100vh', overflow: 'hidden', position: 'relative' }}>
|
|
88
93
|
{/* 左侧侧边栏 */}
|
|
89
94
|
<Sider
|
|
90
95
|
width={240}
|
|
91
96
|
theme="light"
|
|
92
97
|
style={{
|
|
93
98
|
borderRight: '1px solid #f0f0f0',
|
|
99
|
+
height: '100vh',
|
|
94
100
|
}}
|
|
95
101
|
>
|
|
96
102
|
<div
|
|
@@ -117,10 +123,8 @@ export default function BasicLayout() {
|
|
|
117
123
|
mode="inline"
|
|
118
124
|
selectedKeys={[location.pathname]}
|
|
119
125
|
style={{ borderRight: 0, paddingBottom: 16, borderBottom: '1px solid #f0f0f0' }}
|
|
120
|
-
items={MENU_ITEMS
|
|
121
|
-
|
|
122
|
-
label: <Link to={item.key}>{item.label}</Link>,
|
|
123
|
-
}))}
|
|
126
|
+
items={MENU_ITEMS}
|
|
127
|
+
onClick={({ key }) => navigate(key)}
|
|
124
128
|
/>
|
|
125
129
|
</div>
|
|
126
130
|
|
|
@@ -132,7 +136,7 @@ export default function BasicLayout() {
|
|
|
132
136
|
</Sider>
|
|
133
137
|
|
|
134
138
|
{/* 右侧主体区域 */}
|
|
135
|
-
<Layout>
|
|
139
|
+
<Layout style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
|
|
136
140
|
{/* 顶部导航栏 */}
|
|
137
141
|
<Header
|
|
138
142
|
style={{
|
|
@@ -141,6 +145,7 @@ export default function BasicLayout() {
|
|
|
141
145
|
justifyContent: 'flex-end',
|
|
142
146
|
alignItems: 'center',
|
|
143
147
|
backgroundColor: 'transparent', // 透明背景融入整体
|
|
148
|
+
flexShrink: 0,
|
|
144
149
|
}}
|
|
145
150
|
>
|
|
146
151
|
<Space size={24}>
|
|
@@ -163,7 +168,10 @@ export default function BasicLayout() {
|
|
|
163
168
|
<Content
|
|
164
169
|
style={{
|
|
165
170
|
margin: '0 24px 24px',
|
|
166
|
-
|
|
171
|
+
flex: 1,
|
|
172
|
+
overflowY: 'auto', // 内容超出滚动
|
|
173
|
+
backgroundColor: '#fff',
|
|
174
|
+
borderRadius: 8,
|
|
167
175
|
}}
|
|
168
176
|
>
|
|
169
177
|
<Outlet />
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createRoot } from 'react-dom/client';
|
|
2
|
+
import { RouterProvider } from 'react-router-dom';
|
|
3
|
+
import { ConfigProvider } from 'antd';
|
|
4
|
+
import { router } from '@/router';
|
|
5
|
+
import 'antd/dist/reset.css';
|
|
6
|
+
import './style.less';
|
|
7
|
+
// index.js(主应用入口处)
|
|
8
|
+
import Garfish from 'garfish';
|
|
9
|
+
import { useGlobalStore } from '@/store/useGlobalStore';
|
|
10
|
+
|
|
11
|
+
Garfish.run({
|
|
12
|
+
basename: '/',
|
|
13
|
+
domGetter: '#subApp',
|
|
14
|
+
apps: [
|
|
15
|
+
{
|
|
16
|
+
name: 'react',
|
|
17
|
+
activeWhen: '/micro-react-1',
|
|
18
|
+
// 通过 Vite 环境变量动态获取子应用地址
|
|
19
|
+
entry: import.meta.env.VITE_MICRO_APP_REACT_ENTRY,
|
|
20
|
+
sandbox: false,
|
|
21
|
+
// 动态传递主应用的全局状态给子应用
|
|
22
|
+
props: {
|
|
23
|
+
globalState: {
|
|
24
|
+
user: useGlobalStore.getState().user,
|
|
25
|
+
token: useGlobalStore.getState().token,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
createRoot(document.getElementById('root')!).render(
|
|
33
|
+
<ConfigProvider theme={{ cssVar: { key: 'main-app' }, hashed: false }}>
|
|
34
|
+
<RouterProvider router={router} />
|
|
35
|
+
</ConfigProvider>
|
|
36
|
+
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { render, screen } from '@testing-library/react';
|
|
3
|
-
import NotFound from './404
|
|
3
|
+
import NotFound from './404';
|
|
4
4
|
import { BrowserRouter } from 'react-router-dom';
|
|
5
5
|
|
|
6
6
|
describe('NotFound (404) 页面组件', () => {
|