Skip to content

03|Vuex 存量代码 ​

需要先在 Vuex 3 项目里增加状态、修复 action 或处理取消时,从Vuex 3 连续维护开始。本篇保留为状态命令边界与迁移比较的专题,不把换成 Pinia 当作维护的先决条件。

第五章概览 / 03。目标:维护旧数据流时先读懂 dispatch → action → commit → mutation → state → getter,再决定是否迁移。

Vuex 仍可工作,但已进入维护模式;新项目使用 Pinia。Vuex 3 通常配 Vue 2,Vuex 4 配 Vue 3,两者核心模型相近,Vuex 4 的创建入口改为 createStore()。

最小 Vuex 4 模块 ​

ts
// src/store/index.ts
import { createStore } from 'vuex'

export interface CartState {
  items: string[]
  status: 'idle' | 'loading' | 'error'
}

export const store = createStore({
  strict: import.meta.env.DEV,
  modules: {
    cart: {
      namespaced: true,
      state: (): CartState => ({ items: [], status: 'idle' }),
      getters: {
        count: (state: CartState) => state.items.length,
      },
      mutations: {
        add(state: CartState, productId: string) {
          state.items.push(productId)
        },
        setStatus(state: CartState, status: CartState['status']) {
          state.status = status
        },
      },
      actions: {
        async addFromServer({ commit }, productId: string) {
          commit('setStatus', 'loading')
          try {
            await Promise.resolve(productId)
            commit('add', productId)
            commit('setStatus', 'idle')
          } catch (error) {
            commit('setStatus', 'error')
            throw error
          }
        },
      },
    },
  },
})
ts
// src/main.ts
createApp(App).use(store).mount('#app')

组件里的两个入口含义不同:

ts
store.commit('cart/add', 'book-1')                    // 同步执行 mutation
await store.dispatch('cart/addFromServer', 'book-2') // action 可异步并返回 Promise

mutation 必须同步,开发工具才能把“某次 commit 前后状态”对应起来。异步流程属于 action;action 不直接替代服务端事务,它只负责编排请求和 commit。

命名空间与 owner ​

namespaced: true 让模块的 getter、mutation、action 使用 cart/... 身份,减少大型仓库中的名称冲突。模块只修改自己的 state;跨模块读取使用 rootState/rootGetters,跨模块提交要显式 { root: true }。如果大量模块互相读取并提交,问题通常不是少写了一个 helper,而是领域 owner 不清楚。

动态注册模块还带来生命周期责任:registerModule() 后若页面离开应 unregisterModule(),并决定是否用 preserveState 保留 SSR/hydration 已有状态。

Vuex 3 与 4 的主要维护差异 ​

场景Vuex 3 / Vue 2 常见写法Vuex 4 / Vue 3
创建new Vuex.Store(options)createStore(options)
安装Vue.use(Vuex) 后传 storeapp.use(store)
组件访问this.$store、map helpers仍可用 Options API;Composition API 可 useStore()
TypeScript常需手写包装仍需处理 injection key;不是迁移到 Vue 3 就自动类型安全

不要把“Vuex 3 → Vuex 4”和“Vuex → Pinia”混成一次大替换。前者是 Vue 平台升级的一部分,后者会改变 mutation、module namespace、插件和测试接口。

迁到 Pinia 时怎样保持行为 ​

VuexPinia迁移时要核对
module + namespace一个 defineStore + 唯一 id原字符串路径的所有调用者
statestate() 或 Setup Store 的 ref初值是否每实例隔离
gettergetter / computed参数化 getter、跨模块依赖
mutation直接写、$patch 或 action同步不变量与开发工具时间线
actionaction返回值、异常、取消和并发行为
plugin / subscribePinia plugin、$subscribe/$onAction持久化格式、埋点、清理时机

一个安全迁移单元是“一个 Vuex namespace + 它的调用者 + 回归测试”。Pinia 官方迁移方案允许新 Pinia store 暂时读取旧 Vuex store,因此可以逐模块替换;不要同时改接口名称、缓存格式和业务规则。

ts
// 迁移后的 Pinia action:mutation 消失,但业务动作名称保留
export const useCartStore = defineStore('cart', {
  state: () => ({ items: [] as string[] }),
  getters: { count: state => state.items.length },
  actions: {
    add(productId: string) {
      this.items.push(productId)
    },
  },
})

从旧模块调用还原迁移边界 ​

旧工程常把user/article拆成modules,把mutation名称集中到types文件。迁移时按一次动作追踪:组件派发哪个字符串,哪个模块接收,哪些字段写入,插件订阅哪个名称。模块目录相同并不意味着接口相同;原先commit('user/SAVE_USER', payload)的调用者不能仅靠把模块文件移成defineStore就自动找到新action。

类型层也要分开:Vue 2 class装饰器和vue/types/vue的实例扩展属于旧运行模型,不是Vue 3 <script setup>的前置。迁移后依赖应从useStore/props显式取得,而不继续给this声明越来越多字段。旧代码若把token放全局state,迁移时先保持可观测登录行为,再按敏感状态章节收缩数据面,不能把旧存储选择当成新项目推荐。

失败路径清单 ​

  • 去掉 namespace 后漏改字符串调用,导致未知 action/mutation。
  • 把 mutation 机械变成异步 action,调用者原本依赖的同步可见性改变。
  • 忽略 Vuex 插件、subscribe、subscribeAction 与持久化格式,页面能显示但审计/恢复失效。
  • SSR 继续复用单例 store,出现跨请求状态污染。
  • Vuex strict mode 报“outside mutation”时直接关闭 strict,而没有定位越权写入。

练习:迁移计数模块 ​

给定 namespace 为 counter 的 Vuex 模块:state { count, status },mutation increment,异步 action incrementLater,getter doubled。

初始count为0、status为idle;increment(step=1)同步加step;incrementLater(delayMs, step=1)在delayMs后加step并返回新count。delayMs非有限数或小于0时reject RangeError,status变error,count保持原值。这样新旧实现共享同一失败契约。

要求:先写三条行为断言(同步加一、异步拒绝时 count 不变、getter 等于两倍),再写等价 Pinia Option Store。保留 incrementLater(delayMs) 的 Promise 与失败契约;列出至少两个还需人工核对的插件/SSR 风险。

参考答案:第 03 题。

本页结论 ​

旧项目维护的第一目标是保持数据流可预测。迁移的完成证据是调用合同、异常、插件、SSR 和测试都一致,不是 import 已经换成 pinia。

用心记录每一步成长