5.5 KiB
5.5 KiB
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) |
每次打开重置 |
二、目标架构
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 官方推荐):
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。