文章

Vue 状态管理与复杂业务建模实战

Vue 状态管理与复杂业务建模实战

很多后端同学写 Vue 时,最开始会把状态当作“页面变量”:当前页面需要什么,就在当前页面定义什么。这个模式在简单页面有效,但在复杂业务中很快会失控,主要表现为:

  1. 同一份业务数据在多个页面各存一份,修改后难以同步。
  2. 页面切换后状态残留,出现“上个用户操作影响下个用户”的脏数据问题。
  3. 组件通信链路越来越长,props 和 emits 层层传递,维护成本急剧上升。
  4. 一旦需求变成“跨页面联动 + 缓存 + 回滚”,代码几乎无法稳定演进。

本质原因是:没有把状态管理当成“业务建模问题”,而只是“组件技巧问题”。

本文目标是帮你建立一套可工程化落地的状态管理体系,核心基于 Vue3 + Pinia,并借鉴后端常用的分层与领域建模思想,让你在复杂需求下依然可控。

一、先给状态分层:不是所有状态都该进 store

最常见误区是“全量进 Pinia”或“全量留页面”。正确做法是分层。

1. 视图临时状态(View Local State)

例如弹窗开关、当前 tab、输入框临时值。生命周期通常只在当前页面或组件。

建议:优先使用组件内 ref/reactive

2. 页面级业务状态(Page Domain State)

例如当前页面的筛选条件、分页信息、表格选中项,可能在页面内多个子组件共享。

建议:可在页面 composable 中集中管理,必要时再入 store。

3. 跨页面共享状态(App Shared State)

例如用户信息、权限、租户信息、全局字典、主题配置。

建议:放 Pinia,保证单一数据源。

4. 服务端权威状态(Server Source of Truth)

例如订单详情、库存、审批流节点。前端只是缓存或投影,不是最终真相。

建议:明确缓存策略,不要把前端状态当数据库。

这四层划分能解决 80% 的状态混乱问题。

二、Pinia 不是“全局变量仓库”,而是领域状态容器

后端同学可以把每个 store 理解为“领域上下文”。

1. 推荐按业务域拆 store

不要按技术类型拆(如一个 commonStore 放所有东西)。建议:

  • useAuthStore
  • useUserStore
  • useOrderStore
  • useDictStore

每个 store 只负责一个明确领域,接口和状态边界清晰。

2. Store 的职责边界

  • state:持久业务状态。
  • getters:纯衍生计算。
  • actions:状态变更和异步流程。

不要在 getters 里做副作用,也不要在 actions 里直接操作 DOM。

三、一个可落地的 store 目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
src/
  stores/
    modules/
      auth.ts
      user.ts
      order.ts
      dict.ts
    index.ts
  services/
    order-service.ts
    user-service.ts
  types/
    order.ts
    user.ts

说明:

  • stores 管状态编排。
  • services 管外部交互(接口、缓存策略)。
  • types 管契约定义。

这样做能避免 store 既当控制器又当 DAO 的“大一统混乱”。

四、状态建模的关键:先定义不变量

后端做领域设计时会先定义不变量,前端也一样。

举例:订单页状态可定义为:

  1. listtotal 必须来自同一次查询结果。
  2. filters 变更后分页应重置到第一页。
  3. selectedIds 只能属于当前 list
  4. detailCache 的 key 必须是订单 id,且可失效。

把这些规则写进 action 逻辑,系统稳定性会高很多。

五、Pinia 进阶:组合式 store 与可测试性

1. 组合式定义(setup store)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export const useOrderStore = defineStore('order', () => {
  const list = ref<OrderItem[]>([])
  const total = ref(0)
  const loading = ref(false)

  const hasData = computed(() => list.value.length > 0)

  async function queryOrders(params: QueryParams) {
    loading.value = true
    try {
      const res = await fetchOrderPage(params)
      list.value = res.list
      total.value = res.total
    } finally {
      loading.value = false
    }
  }

  return { list, total, loading, hasData, queryOrders }
})

优势:类型推导自然、复用组合式 API 更灵活。

2. 可测试性增强

把外部依赖(接口函数)注入或封装到 service,store 单测就更容易做 mock。

六、缓存策略:前端状态不是越“全”越好

前端缓存必须回答三个问题:

  1. 缓存什么(对象、列表、字典)?
  2. 缓多久(TTL)?
  3. 何时失效(主动失效、被动失效)?

1. 常见缓存模式

  • 字典类:长 TTL,启动时预加载。
  • 列表类:短 TTL,按筛选条件分 key 缓存。
  • 详情类:读多写少可缓存,写后要精准失效。

2. 切忌“永不过期缓存”

很多“数据不一致” bug 都来自没有失效策略。

七、并发请求治理:避免状态回写覆盖

复杂页面经常出现并发请求。

场景:用户快速修改筛选条件,连续触发请求 A/B/C,结果 C 先回,A 后回,A 把 C 的新结果覆盖了。

解决方案:

  1. 请求序列号(只接受最新序号响应)。
  2. AbortController 取消旧请求。
  3. 在 action 内做“最后写入保护”。

这和后端并发写冲突处理思路一致。

八、乐观更新与回滚:提升交互体验的关键

比如切换用户状态开关:

  1. 前端先更新 UI(乐观更新)。
  2. 异步调用接口。
  3. 失败则回滚状态并提示。

建议封装一个通用 helper:

  • 接收旧值、新值、提交函数、回滚函数。
  • 统一处理 loading、错误提示和重试。

这样在多个页面复用时行为一致。

九、跨 store 协作:避免环依赖

复杂系统里 store 之间会互相依赖。常见错误是直接互调造成环依赖。

建议:

  1. 尽量通过 service 层协调。
  2. 必要时在 action 内延迟获取对方 store。
  3. 把共享逻辑提取为 composable 或 domain service。

不要让 store 彼此强耦合,否则后续改动风险很大。

十、和后端接口契约对齐:状态模型先行

后端同学有个天然优势:能主动推进契约一致性。

建议做法:

  1. 先定义 TS 类型再写页面。
  2. 分页结构统一(list, total)。
  3. 错误码语义统一(业务错误 vs 系统错误)。
  4. 时间字段统一(时区、格式、空值语义)。

这些规范会直接降低状态处理复杂度。

十一、持久化策略:哪些状态该落 localStorage

不是所有 store 都要持久化。

推荐:

  • 可以持久化:token、用户偏好、主题配置、非敏感字典。
  • 不建议持久化:临时查询结果、敏感业务数据、一次性流程状态。

持久化前先问:刷新后恢复它,对用户是收益还是风险?

十二、调试与观测:状态系统也需要“可观测性”

后端有日志和链路追踪,前端状态也要有。

建议:

  1. 对关键 action 打结构化日志(开发环境)。
  2. 记录状态变更前后摘要(不要全量打印大对象)。
  3. 关键失败路径上报埋点(如回滚次数、请求冲突次数)。

这样排查“偶发状态错乱”会快很多。

十三、一个实战案例:审批工作台状态建模

需求:审批工作台包含“待办、已办、抄送”三栏,支持批量审批、详情侧栏、跨标签切换保留筛选条件。

常见失败实现:

  • 三栏各写一套状态,代码重复。
  • 筛选条件混在全局 store,互相污染。
  • 详情缓存没有失效,导致旧数据残留。

更稳的实现:

  1. 抽象统一 TaskListState 模型,按 tab key 存储。
  2. 筛选条件按 tab 维度隔离。
  3. 列表缓存和详情缓存分离管理。
  4. 批量操作后精准失效当前 tab 数据。
  5. 侧栏详情加载走独立 action,避免阻塞列表交互。

收益:代码复用高、边界清晰、回归可控。

十四、从页面驱动到领域驱动

初级写法通常是“页面上有什么就写什么”。进阶后要变成“业务规则驱动状态结构”。

你可以把前端状态层理解成一个轻量领域模型:

  • 实体:订单、用户、任务。
  • 值对象:筛选条件、分页参数。
  • 领域服务:查询、提交、回滚、同步。
  • 应用服务:跨模块编排。

这种建模思维会让大型前端系统更稳。

十五、五个反模式(尽量避免)

  1. 一个超大 store 管全部状态。
  2. 页面随意改 store 内部结构。
  3. getter 中做异步请求。
  4. action 里混杂 UI 副作用。
  5. 没有缓存失效策略。

出现任意两项,维护成本会迅速上升。

十六、迁移旧项目的最小策略

如果你接手的是老项目,不要一次性重构。

推荐节奏:

  1. 新需求先按新分层落地。
  2. 提炼重复逻辑到 service/composable。
  3. 把高频模块优先迁移到领域化 store。
  4. 每个迭代迁移一小块并补回归。

这样能持续改善,不会因“大重构”拖垮交付。

十七、测试建议:状态管理必须可回归

至少覆盖:

  1. action 成功/失败分支。
  2. 并发请求覆盖保护。
  3. 缓存命中与失效逻辑。
  4. 乐观更新回滚路径。

即使你暂时没有完善的前端测试基础设施,也建议先对关键 store 做最小单测。

十八、后端同学如何快速建立优势

你可以把后端经验直接迁移:

  • 用不变量思维建模状态。
  • 用事务思维设计提交与回滚。
  • 用缓存思维设计状态失效。
  • 用并发思维处理竞态覆盖。

这会让你在复杂前端业务里比“只会写组件”的路径更快进入核心区。

十九、总结

状态管理不是技术选型题,而是系统设计题。Pinia 只是工具,真正决定质量的是:

  1. 是否做了清晰的状态分层。
  2. 是否把业务不变量落实到 action。
  3. 是否有可观测、可回滚、可失效的机制。

当你做到这三点,你的 Vue 项目就能从“能跑”走向“可持续演进”。

二十、建议练习题(状态进阶)

  1. 用 Pinia 重构一个复杂页面,把状态分为四层并写说明文档。
  2. 给一个列表查询 action 加入并发覆盖保护。
  3. 为一个开关操作实现乐观更新 + 回滚通用封装。
  4. 把一个 commonStore 拆成按领域划分的多个 store。
  5. 给关键 store 增加 3 个单测(成功/失败/并发)。

完成这组练习,你会明显感受到复杂业务的可控性提升。

二十一、状态版本化:给复杂页面加“可回溯能力”

在一些关键业务(例如审批、报价、配置平台)中,用户经常会提出“撤销上一步”“恢复草稿”“比较两次修改差异”。如果状态没有版本化策略,需求会非常痛苦。

可行方案:

  1. 为关键状态维护 history 栈(限制长度,避免内存膨胀)。
  2. 每次提交前记录快照摘要,而不是全量深拷贝。
  3. 提供 undo/redo action,统一处理状态回放。

注意:快照要基于业务语义抽取,不要盲目 clone 全对象,否则性能和内存都顶不住。

二十二、表单状态建模:草稿态与持久态分离

很多页面的 bug 都来自“编辑态和已保存态混在一起”。建议分离:

  1. persistedModel:服务端确认过的数据。
  2. draftModel:用户当前正在编辑的数据。
  3. dirtyFields:记录变更字段,支持差异提交。

收益:

  • 能做“仅提交变更字段”。
  • 能准确提示“有未保存修改”。
  • 能在保存失败后保留编辑上下文。

这套模型非常适合后端同学,和 ORM 实体的脏检查思路高度一致。

二十三、状态迁移与兼容:版本升级别破坏旧数据

当你持久化了一些前端状态(本地缓存),后续结构升级时要考虑兼容。建议在 store 增加 schemaVersion

  1. 启动时检测本地版本。
  2. 旧版本执行迁移函数。
  3. 迁移失败就回退默认值并清理旧缓存。

这和后端数据库 migration 思路一致。没有版本迁移,线上会出现“新版本读不了旧缓存”的隐蔽问题。

二十四、状态观测面板:降低协作沟通成本

复杂系统里,前后端和测试经常对“当前状态到底是什么”理解不一致。建议在开发环境提供轻量状态观测面板:

  1. 展示核心 store 快照。
  2. 展示最近 action 调用序列。
  3. 展示关键缓存命中/失效记录。

这会极大提升联调效率。很多“我这边复现不了”的问题,本质是状态上下文不同。

二十五、状态治理的团队规范建议

为了避免项目后期失控,建议落地以下规范:

  1. 新增 store 必须有职责说明(一句话定义边界)。
  2. getter 必须保持纯函数语义。
  3. action 命名统一动词前缀(fetch/update/reset/apply)。
  4. 跨 store 依赖必须说明原因,避免隐式耦合。
  5. 关键 action 必须覆盖失败路径。

这些规范看似简单,但会显著提升多人协作稳定性。

二十六、结语补充:状态管理是前端系统设计能力的核心

当业务变复杂,组件技巧的价值会下降,系统设计能力的价值会上升。状态管理就是这个分界点。你只要能把“状态分层、边界治理、并发控制、回滚恢复”做好,前端系统就能长期可维护。

对后端工程师来说,这并不是陌生领域,而是你擅长能力的迁移:把分层、事务、缓存、一致性、观测这些工程原则落到前端。做到这一点,你会成为团队里少数能稳定处理复杂前端业务的人。

本文由作者按照 CC BY 4.0 进行授权