出处:掘金

原作者:金泽宸


前端状态不是“能用就放 store”,而是“局部用局部管,模块用模块管,全局才上全局 store”

写在前面

状态管理是前端架构中最容易“滥用”,却最影响维护性与性能的模块

在实际开发中,你是否遇到这些问题:

  • 页面状态和全局状态混乱在一起
  • store 巨大无比,一个值改动引发全局重渲
  • 表单页退出再进来,数据全部丢失
  • 组件之间 propsemit 层层传递,毫无乐趣

本篇我们将:

  • 拆解状态管理的分层思维
  • 统一 Vue/Pinia/Vuex/Redux 的状态分布结构
  • 给出状态归属、持久化、模块复用的实践方案
  • 给出不同项目适用的架构模型

状态管理的分类与归属建议

将状态拆成 4 层:

层级状态类型举例建议位置
① 页面状态表单输入、分页、loadingformData、currentPage组件内部 / setup()
② 业务状态当前用户信息、购物车内容userInfo、cartList模块 store(Pinia)
③ 全局状态多模块共享、应用级别主题、token、权限全局 store
④ 派发逻辑状态之间的数据流A 模块更新后通知 BEventBus / 中间 store / 联动处理

推荐架构结构

以 Vue3 + Pinia 为例

src/
├── modules/
│   └── user/
│       ├── views/...
│       ├── store.ts         # 用户业务状态
│       ├── api.ts
├── store/                   # 全局状态
│   ├── app.ts               # 主题、语言
│   ├── auth.ts              # 登录 token、权限列表
│   └── index.ts

示例:业务模块 store

// modules/user/store.ts
import { defineStore } from 'pinia'
 
export const useUserStore = defineStore('user', {
  state: () => ({
    info: null,
  }),
  actions: {
    async fetch() {
      this.info = await getUserInfo()
    },
  },
})

不同状态场景的最佳放置方案

场景 1:表单输入数据(页面私有)

const formData = reactive({ name: '', age: '' })
// 不应放入 store

场景 2:用户登录态(全局共享)

const authStore = useAuthStore()
const token = authStore.token

场景 3:商品选择列表(多页面共享)

// 使用模块级 store:useGoodsStore()

场景 4:页面跳转后状态还原

  1. 方法 1:store 缓存 + 页面 onMounted 时恢复
  2. 方法 2:使用 useRouteQuery(),在 query 中保持关键状态
  3. 方法 3:sessionStorage 临时保存页面缓存数据

状态更新联动与事件分发策略

当模块 A 修改状态后,希望模块 B 响应更新,可以用:

方法 1:订阅监听(Pinia)

// 在模块 B 中监听
userStore.$subscribe((mutation, state) => {
  if (mutation.storeId === 'user' && mutation.events.key === 'info') {
    console.log('用户信息变化了')
  }
})

方法 2:中间状态派发器(推荐)

创建一个通用的 eventStore

export const useEventStore = defineStore('event', {
  state: () => ({ events: {} }),
  actions: {
    fire(event, payload) {
      this.events[event] = payload
    },
    clear(event) {
      delete this.events[event]
    },
  },
})

模块 A:

eventStore.fire('refresh-table', { force: true })

模块 B:

watch(() => eventStore.events['refresh-table'], (val) => {
  if (val?.force) refreshTable()
})

状态持久化方案

如:持久登录 / 记住用户

使用 pinia-plugin-persistedstate 插件:

npm i pinia-plugin-persistedstate

在入口注册:

import { createPinia } from 'pinia'
import piniaPersist from 'pinia-plugin-persistedstate'
 
const pinia = createPinia()
pinia.use(piniaPersist)

定义持久化字段:

export const useAuthStore = defineStore('auth', {
  state: () => ({ token: '' }),
  persist: true,
})

统一状态管理实践建议

  1. 所有页面状态优先使用 setup 内部 ref/reactive 管理
  2. 所有共享业务状态都用模块化 store
  3. 所有跨模块状态联动使用 eventStore / subscribe
  4. 不建议把“短生命周期状态”放入 store,例如临时分页配置、tabIndex
  5. store 命名统一采用功能名(useUserStoreuseAuthStore

封装建议(高级实践)

  • useRouteQueryState():自动将分页等状态绑定到 URL
  • usePersistedRef():封装 localStorage/SessionStorage 自动存取
  • createScopedStore():为多 tab 设计私有状态
  • pinia-auto-module:支持自动生成模块 + 动态加载 store