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

5.5 KiB
Raw Blame History

Zustand 全局状态迁移方案

目标:将 apps/admin 中分散的全局状态管理统一迁移到 Zustand建立清晰、可维护、单向数据流的状态架构。

一、现状分析(迁移前)

迁移前,全局状态散落在多个位置:

状态 原实现 问题
token / user 各组件直接读写 localStorageLogin、MainLayout、api 拦截器、下载、SSE 等) 无类型约束、无订阅、重复解析
permissions auth/permission-store.ts 模块单例 + window 自定义事件 + usePermission 手动订阅 事件驱动易遗漏、无法细粒度订阅
布局 UI侧边栏/抽屉/AI 抽屉/菜单展开) MainLayout 内多个 useState 局部状态无法跨组件共享、不持久化
路由页签RouteDock 组件内 useState + 直接写 localStorage('gongxue-route-dock') 持久化逻辑与 UI 耦合
AI 聊天偏好(深度思考) AiChatDraweruseState(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 + statusfail-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 tokenuser 适配器继续读写旧 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.tsxPrivateRoute 从 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/admin142 个浏览器集成测试)
  • npm run build --workspace @gongxue/admin
  • npm run lint --workspace @gongxue/admin (无新增告警)

六、后续可选优化(本次未纳入)

  • 页面级缓存integration-config-cache.tsschedule-visibility.tsunavailable-dates-cache.tsinspection-state.ts 等仍是模块级单例,属于页面内缓存,可按需迁移为 Zustand或保持现状配合 React Query/SWR
  • 请求去重:若多个页面出现同一资源重复请求,可引入 SWR 统一缓存(当前各页数据获取均为单次请求,收益有限)。
  • 包体积@ant-design/icons 为 barrel 导出但 sideEffects: falseVite 构建可正确 tree-shake如需进一步压缩 dev 冷启动,可改为深路径导入。lucide-react 已安装但未被引用,可择机移除。
  • 大列表渲染:消息列表/大表格可补充 content-visibility 或虚拟滚动;搜索大列表时可用 useDeferredValue