Vue 应用缺失的一层状态模型
在大多数 Vue 项目中,我们习惯把状态分为两类:
- 组件状态(Component State)
- 全局状态(Global State)
例如:
组件状态
- input 值
- hover 状态
- 弹窗开关
全局状态
- 用户信息
- 权限
- 主题
- 配置
但当系统规模变大之后,你会发现一类状态越来越难管理:
页面状态(Page State)
一个常见却被忽视的问题
想象一个典型的数据分析页面:
- 漏斗分析
- 用户行为分析
- 数据仪表盘
页面里通常会有:
- 筛选条件
- 日期范围
- Tab
- 分页
- 图表数据
这些状态具有几个特点:
1. 多组件共享
筛选条件变化:
- 图表刷新
- 表格刷新
- 指标刷新
多个组件同时响应。
2. 生命周期 = 页面
用户进入页面:
初始化状态
用户离开页面:
状态应该被清理
3. 不属于全局
这些状态只属于:
当前页面
而不是整个应用。
Vue 应用其实有三层状态
如果从系统结构看,Vue 应用其实有三层状态:
Component State
↓
Page Runtime State
↓
Global State
第一层:Component State
组件内部状态。
特点:
- 生命周期 = 组件
- 不需要共享
例如:
- dropdown 展开
- input 输入值
- loading 状态
通常写在:
data
setup
第二层:Page Runtime State
页面级运行时状态。
特点:
- 多组件共享
- 生命周期 = 页面
例如:
filters
tab
pagination
chartData
第三层:Global State
应用级状态。
特点:
- 全局共享
- 生命周期 = 应用
例如:
user
permission
theme
appConfig
为什么 Vuex / Pinia 管不好页面状态
Vuex / Pinia 本质上是:
Application Store
生命周期:
应用启动 → 应用关闭
而页面状态生命周期是:
进入页面 → 离开页面
生命周期不匹配。
这会带来几个问题。
1. 状态残留
用户:
页面A → 页面B → 再回到页面A
store 里的筛选条件还在。
2. Store Module 膨胀
每个复杂页面一个 module:
store/
funnel
dashboard
analysis
report
store 越来越臃肿。
但这些状态 99% 时间不需要存在。
3. 过度仪式感
Vuex 修改状态:
commit
↓
mutation
↓
state
对于页面内部交互来说太重。
Page Runtime Context
为了解决这个问题,可以引入一个概念:
Page Runtime Context
定义:
页面级运行时上下文
它包含:
- state
- communication
- side effects
Page Runtime Context 设计原则
一个合理的 Page Runtime Context 应该满足几个原则。
1. 生命周期绑定
页面创建 → store 创建
页面销毁 → store 销毁
2. 作用域隔离
不同页面之间不互相影响。
pageA store
pageB store
完全隔离。
3. 直接状态修改
页面状态不需要 mutation ceremony。
store.filters = newFilters
4. 内置通信
页面内组件需要通信能力:
componentA → componentB
例如:
- filter change
- tab change
5. 副作用管理
页面副作用应该绑定生命周期:
- watch
- data fetch
- subscriptions
页面销毁自动清理。
一个简单实现思路
Page Runtime Context 可以用非常简单的方式实现:
隐藏 Vue 实例
↓
响应式 state
↓
computed getters
↓
作用域事件总线
整个实现通常只需要:
100 行代码左右。
总结
Vue 应用的状态模型其实应该是:
Component State
↓
Page Runtime Context
↓
Global Store
Vuex / Pinia 解决的是:
Global State
但中间这一层:
Page Runtime State
一直是空白。
Page Runtime Context 正是用来填补这一层的设计模式。