Vuex 管全局很好,但为什么一到页面状态就失控?

2 阅读3分钟

很多 Vue 项目都会遇到一个奇怪的问题:

Vuex 管 全局状态 很好用。

但一旦开始管理 页面状态,事情就变得越来越混乱。

例如:

  • 用户离开页面再回来,筛选条件还在
  • store 里出现一堆页面 module
  • mutation 越来越多,但很多只是 UI 状态

慢慢地 Vuex store 变成:

一个巨大的状态仓库

但这些状态其实只属于:

当前页面

问题其实不在 Vuex。

而在于:

Vue 应用里缺了一层状态模型。


Vue 应用其实有三类状态

如果从架构角度看,一个 Vue 应用的状态其实可以分为三类:

Component State

Page State

Global State


Component State

组件内部状态。

例如:

  • dropdown 展开
  • input 值
  • hover 状态

特点:

  • 生命周期 = 组件
  • 不需要共享

通常写在:

data
setup

Global State

全局状态。

例如:

  • 用户信息
  • 权限
  • 主题
  • 应用配置

特点:

  • 全局共享
  • 生命周期 = 应用

这类状态非常适合 Vuex / Pinia。


Page State

页面级状态。

例如:

  • 筛选条件
  • tab
  • 分页
  • 图表数据
  • 当前查询结果

特点:

  • 多组件共享
  • 生命周期 = 页面

一个典型例子:

筛选条件变化时:

  • 图表刷新
  • 表格刷新
  • 指标刷新

多个组件同时响应。


Vuex 的问题:生命周期不匹配

Vuex 本质上是:

Application Store

生命周期:

应用启动 → 应用关闭

而页面状态生命周期是:

进入页面 → 离开页面

这两者不匹配。

这会导致几个常见问题。


问题一:状态残留

用户操作流程:

页面A → 页面B → 再回到页面A

如果页面状态在 Vuex 中:

筛选条件仍然存在。

有时这是 bug。

例如:

用户打开一个新的分析页面,却继承了旧筛选条件。

解决方法通常是:

beforeDestroy() {
  resetStore()
}

但问题是:

每个页面都要写 reset。

写漏一次就是 bug。


问题二:store module 膨胀

当复杂页面越来越多时,store 会变成:

store/
  funnel
  dashboard
  analysis
  report

问题是:

这些 module 99% 的时间不需要存在

但 Vuex store 是应用级的。

所以它们会一直存在。

久而久之 store 会变成:

一个巨大的状态仓库。


问题三:Vuex 仪式感过重

Vuex 修改状态的流程:

commit

mutation

state

这对于全局状态来说是合理的。

但对于页面内部交互来说:

太重。

例如:

筛选条件变化

其实只需要:

store.filters = newFilters

但 Vuex 要写:

commit('SET_FILTERS')

这种 ceremony 在复杂页面中会迅速膨胀。


问题四:页面通信不方便

在复杂页面中,组件之间经常需要通信。

例如:

  • filter change
  • tab change
  • chart update

如果用 Vuex:

组件 A:

commit('setFilters')

组件 B:

watch(() => store.filters)

很多时候只是组件之间简单通信。

Vuex 在这里显得过于笨重。


Vuex 并没有错

需要说明一点:

Vuex 并没有设计错误。

Vuex 的设计目标是:

Global State Management

也就是:

应用级状态管理。

它解决的是:

Application State

而不是:

Page Runtime State


Vue 应用缺失的一层

如果从系统结构看,Vue 应用其实应该是三层:

Component State

Page Runtime State

Global State

Vuex / Pinia 解决了:

Global State

但中间这一层:

Page Runtime State

一直没有标准方案。

这也是为什么很多团队会:

  • 把页面状态塞进 Vuex
  • 使用 EventBus
  • 或者 props drilling

每种方案都不完美。


一个更合理的模型

如果引入一个概念:

Page Runtime Context

结构会变成:

Component State

Page Runtime Context

Global Store

也就是:

  • 组件状态 → 组件内部
  • 页面状态 → 页面上下文
  • 全局状态 → Vuex

每一层只管理属于自己的状态。


总结

Vuex 管不好页面状态,并不是因为 Vuex 设计不好。

而是因为:

Vuex 解决的是 Global State,而不是 Page State。

Vue 应用其实有三层状态:

Component State

Page Runtime State

Global State

Vuex / Pinia 只覆盖了第三层。

中间这一层:

Page Runtime Context

一直是空白。

而复杂系统的问题,往往就出现在这里。