Page Runtime Context 设计模式

6 阅读3分钟

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 正是用来填补这一层的设计模式。