学完这篇你能拿到一套明确的判断标准:新项目该直接上 Pinia,还是继续用 Vuex 4,以及在需要迁移时按什么顺序把老代码改过来而不把页面改崩。
先说结论:**Vue 3 新项目用 Pinia,这是官方推荐路线;只有存量项目仍在 Vuex 3/4 上稳定运行的情况下,才有"要不要现在动"的问题。** 下面按步骤拆开讲。
第一步:先确认版本前提,这一步最容易混
Vue 3 里能用的 Vuex 版本只有 Vuex 4.x,它本质是 Vuex 3 的 Vue 3 适配版,API 思路没变。Pinia 则是 Vue 3 时代重写的新库,Vue 官方文档已把 Pinia 列为推荐的状态管理方案,Vuex 4 处于维护状态(修 bug,不再加新特性)。
安装命令:
# 新项目
npm install pinia
npm install vuex@4
注意:不要在一个项目里同时装 Pinia 和 Vuex 管理同一批数据。两套响应式容器互不感知,调试时会非常痛苦。迁移期可以短暂共存,但一个模块只能归一边。
第二步:用同一份计数器对比写法
Vuex 4 的注册入口在 src/main.js:
import { createApp } from 'vue'
import { createStore } from 'vuex'
import App from './App.vue'
const store = createStore({
state: { count: 0 },
getters: { double: (s) => s.count * 2 },
mutations: { INCREMENT(state) { state.count++ } },
actions: { increment({ commit }) { commit('INCREMENT') } }
})
createApp(App).use(store).mount('#app')
Pinia 的注册入口:
import { createPinia } from 'pinia'
createApp(App).use(createPinia()).mount('#app')
单看入口差别不大,区别在"定义"和"使用":
// Pinia:选项式写法,没有 mutations
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: { double: (state) => state.count * 2 },
actions: {
increment() { this.count++ }, // 同步
async load() { this.count = await api.getCount() } // 异步也放这里
}
})
调用处对比最直观:
// Vuex
store.dispatch('increment')
store.commit('INCREMENT')
// Pinia
useCounterStore().increment()
Pinia 没有 commit/dispatch 的字符串,改函数直接调,改错了 TS 和编辑器立刻报红。
第三步:按能力逐项对照
| 能力 | Pinia | Vuex 4 |
|---|
| 变更方式 | actions 统一处理,同步异步都行 | mutations 只能同步,异步走 actions 再 commit |
| 模块化 | 每个 defineStore 天然独立,互相引用 | modules 嵌套,要 namespaced: true 才有局部命名空间 |
| TypeScript | 类型自动推导,基本不用手写 | state 类型需额外声明,commit 字符串类型弱 |
| 代码分割 | store 首次调用才注册,自动按需 | 通常一次性注册全部模块 |
| 体积 | 更小 | 相对更大 |
| DevTools | 支持,时间线可追溯 | 支持 |
嵌套模块是 Vuex 最劝退的地方:三层 modules 之后,dispatch('user/profile/updateName') 这种长路径满天飞,重构时全局搜索都容易漏。
第四步:按你的场景做选择
- 全新 Vue 3 项目:直接 Pinia,别犹豫。用
<script setup> 的话,setup store 写法更顺手:
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() { count.value++ }
return { count, double, increment }
})
- 存量 Vuex 项目,运行稳定:不急。把
vuex 升到 4.x 让它跑在 Vue 3 上,新模块再考虑 Pinia。
- 团队大、模块多、要上 TS:迁移收益明显,早迁早省心。
- 只是父子传值、跨两三层组件:两个都不用,
provide/inject 或 props 够了。状态管理不是越大越好。
第五步:从 Vuex 迁移到 Pinia 的操作顺序
- 装依赖:
npm install pinia,main.js 里把 createStore 换成 createPinia()。
- 挑一个最简单的模块先转:把
state 原样搬到 state: () => ({...}),getters 直接搬。
- mutations 全部删掉,逻辑并进 actions,这是工作量最大的一步,但也是收益最大的。
- 全局搜索
dispatch( 和 commit(,逐个改成直接调用 store 方法。
mapState / mapGetters 换成 storeToRefs(store) + 直接解构方法。
- 老 Vuex store 保留到最后一步再删除,跑通全站再清理。
注意:Pinia 的 store 是响应式代理,直接解构会丢响应性。必须这样写:
> import { storeToRefs } from 'pinia'
> const store = useCounterStore()
> const { count, double } = storeToRefs(store) // state/getters 用这个
> const { increment } = store // 方法直接解构没问题
>
注意:setup 写法的 store 没有 $reset(),需要自己实现:
> function reset() { count.value = 0 }
> return { count, reset }
>
选项式写法才有内置 $reset()。
注意:在组件外(比如路由守卫、axios 拦截器)用 store,要确保 app.use(createPinia()) 已经执行,否则会报 "no active Pinia"。SSR 场景要把 pinia 实例显式传进去。
小结
- Vue 3 新项目选 Pinia,官方推荐、TS 友好、无 mutations、模块天然隔离。
- Vuex 4 只是 Vuex 3 的兼容版,API 没变,属于维护状态,适合存量项目过渡。
- 迁移的核心动作是把 mutations 合并进 actions,然后清理
dispatch/commit 字符串调用。
- 两个高频坑:解构 state 必须用
storeToRefs;setup store 要手写 reset。
- 状态管理只解决"跨组件共享",局部状态别硬塞进 store。