99 lines
5.5 KiB
Markdown
99 lines
5.5 KiB
Markdown
# Zustand 全局状态迁移方案
|
||
|
||
> 目标:将 `apps/admin` 中分散的全局状态管理统一迁移到 Zustand,建立清晰、可维护、单向数据流的状态架构。
|
||
|
||
## 一、现状分析(迁移前)
|
||
|
||
迁移前,全局状态散落在多个位置:
|
||
|
||
| 状态 | 原实现 | 问题 |
|
||
| --- | --- | --- |
|
||
| token / user | 各组件直接读写 `localStorage`(Login、MainLayout、api 拦截器、下载、SSE 等) | 无类型约束、无订阅、重复解析 |
|
||
| permissions | `auth/permission-store.ts` 模块单例 + `window` 自定义事件 + `usePermission` 手动订阅 | 事件驱动易遗漏、无法细粒度订阅 |
|
||
| 布局 UI(侧边栏/抽屉/AI 抽屉/菜单展开) | `MainLayout` 内多个 `useState` | 局部状态无法跨组件共享、不持久化 |
|
||
| 路由页签(RouteDock) | 组件内 `useState` + 直接写 `localStorage('gongxue-route-dock')` | 持久化逻辑与 UI 耦合 |
|
||
| AI 聊天偏好(深度思考) | `AiChatDrawer` 内 `useState(false)` | 每次打开重置 |
|
||
|
||
## 二、目标架构
|
||
|
||
```text
|
||
apps/admin/src/store/
|
||
├── index.ts # 统一出口
|
||
├── types.ts # 通用类型(StoreStatus 等)
|
||
├── middleware/
|
||
│ └── persist.ts # 持久化适配器(兼容旧 localStorage key)
|
||
├── user/
|
||
│ ├── userTypes.ts # UserInfo / UserState / UserActions
|
||
│ ├── userActions.ts # 会话 actions(与 state 分离)
|
||
│ └── userStore.ts # create()(devtools(persist(...)))
|
||
├── app/
|
||
│ ├── appTypes.ts # 布局/抽屉/路由页签状态
|
||
│ └── appStore.ts
|
||
├── permission/
|
||
│ ├── permissionTypes.ts # permissions + status(fail-closed)
|
||
│ └── permissionStore.ts
|
||
└── settings/
|
||
├── settingsTypes.ts # 用户偏好(AI 深度思考等)
|
||
└── settingsStore.ts
|
||
```
|
||
|
||
统一写法(Zustand 官方推荐):
|
||
|
||
```ts
|
||
export const useUserStore = create<UserStore>()(
|
||
devtools(
|
||
persist(
|
||
(set) => ({ token: null, user: null, ...createUserActions(set) }),
|
||
{ name: 'gongxue-auth', storage: authPersistStorage, version: 1 },
|
||
),
|
||
{ name: 'user-store', enabled: import.meta.env.DEV },
|
||
),
|
||
);
|
||
```
|
||
|
||
设计要点:
|
||
|
||
- **state 与 actions 分离**:`userActions.ts` 独立成文件,其余 Store 的 actions 量小,随 Store 内联,避免无限膨胀。
|
||
- **单向数据流**:组件通过 selector 订阅;仅通过 action 修改状态;持久化由 middleware 统一处理。
|
||
- **devtools 仅在开发环境启用**,生产不产生额外开销。
|
||
- **持久化版本化**:所有 Store `version: 1`,后续 schema 变更通过 `migrate` 平滑升级。
|
||
|
||
## 三、持久化兼容策略
|
||
|
||
为了平滑迁移且不破坏已有浏览器缓存:
|
||
|
||
| Store | persist name | 兼容的旧 key | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| user | `gongxue-auth` | `token`、`user` | 适配器继续读写旧 key,格式不变 |
|
||
| permission | `permissions` | `permissions`(原始 JSON 数组) | 同时兼容旧数组与 zustand 信封;rehydrate 后强制 `status: 'unknown'`,保持 fail-closed |
|
||
| app | `gongxue-app-ui` | `gongxue-route-dock` | 首次读取自动迁移旧页签数据 |
|
||
| settings | `gongxue-settings` | 无 | 新 key |
|
||
|
||
权限状态特别说明:旧实现“缓存权限但未校验前不使用”。迁移后依然如此——持久化只保存权限码,`status` 恢复后一律为 `unknown`,必须等待 `/auth/profile` 校验成功后(`writePermissions`)才变为 `ready`。
|
||
|
||
## 四、已迁移的消费方
|
||
|
||
- `pages/Login`:登录后写入 user Store,权限写入 permission Store。
|
||
- `App.tsx`:`PrivateRoute` 从 user Store 读取 token(响应式)。
|
||
- `api/index.ts`:请求拦截器与 401 处理改读 Store / 调用 `logout()`。
|
||
- `layouts/MainLayout`:用户信息、权限校验、布局 UI、AI 抽屉开关全部迁移到 Store。
|
||
- `hooks/usePermission`:改为 Zustand selector 订阅,删除自定义事件。
|
||
- `components/RouteDock`:页签状态迁移到 app Store(持久化仍生效)。
|
||
- `components/AiChat/AiChatDrawer`:深度思考偏好迁移到 settings Store(持久化)。
|
||
- 下载、SSE、AiChat provider 等所有 token 读取统一走 `useUserStore.getState().token`。
|
||
- `auth/permission-store.ts` 已删除;测试 helper/setup 同步迁移。
|
||
|
||
## 五、验证
|
||
|
||
- `npx tsc -b apps/admin/tsconfig.app.json --noEmit` ✅
|
||
- `npx vitest run --root apps/admin`(142 个浏览器集成测试)✅
|
||
- `npm run build --workspace @gongxue/admin` ✅
|
||
- `npm run lint --workspace @gongxue/admin` ✅(无新增告警)
|
||
|
||
## 六、后续可选优化(本次未纳入)
|
||
|
||
- **页面级缓存**:`integration-config-cache.ts`、`schedule-visibility.ts`、`unavailable-dates-cache.ts`、`inspection-state.ts` 等仍是模块级单例,属于页面内缓存,可按需迁移为 Zustand(或保持现状,配合 React Query/SWR)。
|
||
- **请求去重**:若多个页面出现同一资源重复请求,可引入 SWR 统一缓存(当前各页数据获取均为单次请求,收益有限)。
|
||
- **包体积**:`@ant-design/icons` 为 barrel 导出但 `sideEffects: false`,Vite 构建可正确 tree-shake;如需进一步压缩 dev 冷启动,可改为深路径导入。`lucide-react` 已安装但未被引用,可择机移除。
|
||
- **大列表渲染**:消息列表/大表格可补充 `content-visibility` 或虚拟滚动;搜索大列表时可用 `useDeferredValue`。
|