状态同步
本页说明前端在本地状态、URL 状态、查询缓存和 SSE 之间的职责分层,以及当前系统在实时性与一致性之间的取舍。
Zustand 状态职责
当前全局状态不是一个大 store,而是几类职责明确的状态源:
useAuthStore:负责登录态、authStatus状态机、服务端会话探测和退出流程。useUIStore:负责侧边栏折叠状态。useSSEStore:负责 SSE 连接状态、clientId、重连次数、每个房间的最新序号和 stale 标记。
useAuthStore 有几个关键点:
- 本地持久化键是
auth-storage。 - 持久化内容只有
user和isAuthenticated,不是完整认证状态机。 - TTL 为 3 天,过期后会自动清理。
- 应用启动时必须走一次
bootstrapAuth(),不能直接信任本地快照。
表格状态与 URL 同步
列表页状态分成三层:
useTableState:管理搜索输入、防抖后的全局搜索词、状态筛选、搜索字段、模糊搜索、排序、分页、列宽和展开状态。useTableUrlState:负责把筛选状态同步到地址栏,并在 URL 变化时把状态回流到页面。- TanStack Query:负责真正的分页数据缓存。
这套设计里有两个重要约束:
- 搜索输入和实际查询词不是一回事。输入框先写
searchInput,防抖后才同步到globalFilter。 - URL 同步采用 merge patch,而不是整体覆盖
location.search,避免把页面其他 URL 状态一并抹掉。
SSE 与实时状态
useSSE 和 useListSSE 共同组成了当前前端的实时同步层:
useSSE建立/api/events?rooms=...连接,监听connected、auth.invalid和业务事件。useSSEStore.processSeq()会逐房间记录lastSeqByRoom,用于检测重复事件和断档。- 重连后,如果已经不是第一次打开流,前端会把当前房间标为 stale。
- 如果发现序号断档,也会直接 stale,而不是尝试猜测丢失的数据。
useListSSE 的策略可以概括成一句话:只有在“局部 patch 不会破坏当前列表语义”时才 patch,否则就 stale。
patch 与 stale 的边界
当前列表 SSE 处理遵循下面的规则:
created事件只有在列表正处于第一页起点、没有搜索词、没有排序冲突,并且新项能安全 prepend 时才会直接插入。updated事件只有在目标行已经加载进列表、更新字段不影响当前搜索字段、不影响当前排序字段、并且合并后仍满足当前筛选时才会 patch。deleted事件默认直接 stale。- 任何“字段变更可能影响搜索命中”“排序字段被改”“目标行当前没加载出来”的场景,都直接 stale。
这套取舍故意偏保守,因为它优先保证“页面不会静默展示错误顺序或错误过滤结果”。
认证状态与 SSE 的衔接
认证和 SSE 是联动的:
api/client.ts会把当前clientId写入普通 API 请求头X-SSE-Client-Id。- 后端广播时会回写
actor_client_id,前端收到后会跳过自己触发的回声事件。 - 如果 SSE 收到
auth.invalid,前端会调用triggerSessionInvalidation(),统一收敛到重新登录流程。 bootstrapAuth()对启动探测请求会带X-Skip-Auth-Invalidation: 1,避免一次启动探测失败就弹全局失效提示。
本地存储分层
当前本地存储大致分成四类:
app-ui:主题、字体来源、仪表盘激活页签、公告已读/关闭、Bug 按钮隐藏状态。app-table:各表格的expandAll、fuzzySearch、列宽。app-auth-meta:设备id/name和 remembered user。- 独立键:
auth-storage、sidebar-storage、chemical_properties_cache、cart_import_batch_latest。
这几层的设计重点是“让同类偏好写到同一个规范化对象里”,而不是每个功能各自散落在多个 localStorage key 里。
主题与壳层状态
useTheme会优先读取app-ui.theme;没有保存值时才回退到系统主题偏好。- 主题切换时会短暂禁用全局 CSS transition,避免亮暗模式切换时整页动画闪烁。
- 侧边栏折叠状态存储在
sidebar-storage,由useUIStore持久化。 - Layout 打开移动端侧边栏时会锁住
document.body.style.overflow,避免抽屉和页面双滚动。
当前同步模型
前端整体采用“快照 + 增量通知 + 明确失效”的同步模型:
- 页面先通过 HTTP 获取权威快照。
- SSE 尽量对当前缓存做安全 patch。
- 只要 patch 的正确性不再可证,就把列表标为 stale。
- 页面根据 stale 标记重新拉完整快照。
这意味着 stale 不是失败,而是当前实现里一种主动的正确性保护机制。
改动入口
- 登录态相关问题:优先改
useStore.ts和api/client.ts - SSE 建连、重连、断档:优先改
useSSE.ts和sseStore.ts - 列表级 patch 策略:优先改
useListSSE.ts - 搜索、防抖、分页、列宽:优先改
useTableState.tsx - URL 查询参数联动:优先改
useTableUrlState.ts - 主题持久化和公告已读:优先改
appUiStorage.ts
验证建议
- 刷新已登录页面时,
authStatus是否从checking正常过渡到authenticated。 - 强制断网或重连时,列表是否进入 stale 流程,而不是默默停在旧数据。
- 同一条数据被当前客户端修改后,是否能正确跳过自己的 SSE 回声事件。
- 搜索命中、排序字段更新、删除事件这些场景下,是否都按预期选择 patch 或 stale。
- 更改列宽、展开状态、模糊搜索后,是否能在刷新后恢复。
参考代码
- frontend/src/api/client.ts
- frontend/src/hooks/useListSSE.ts
- frontend/src/hooks/useSSE.ts
- frontend/src/hooks/useTableState.tsx
- frontend/src/hooks/useTableUrlState.ts
- frontend/src/hooks/useTheme.ts
- frontend/src/store/sseStore.ts
- frontend/src/store/useStore.ts
- frontend/src/lib/storage/appAuthMetaStorage.ts
- frontend/src/lib/storage/appTableStorage.ts
- frontend/src/lib/storage/appUiStorage.ts