Files
gongxue-base/docs/zustand-migration.md

99 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 + statusfail-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`