前言
随着 Vue 3 的普及,官方推荐的状态管理库已经从 Vuex 转向了 Pinia。Pinia 提供了更简洁的 API、更好的 TypeScript 支持以及更符合组合式 API 的设计理念。最近我将一个中型后台管理系统从 Vuex 4 迁移到了 Pinia,整个过程并非一帆风顺。这篇文章记录了我在迁移过程中踩过的坑和对应的解决方案,希望能帮助正在或计划迁移的开发者少走弯路。
为什么要从 Vuex 迁移到 Pinia
在开始踩坑之前,先简单说下迁移的动机:
- 更简洁的 API:去掉了
mutations,只有state、getters和actions - 完美的 TypeScript 支持:类型推断几乎零配置
- 模块化更自然:每个 store 独立文件,无需嵌套模块
- 更好的 DevTools 体验:支持时间旅行调试和热更新
- 更小的体积:约 1KB,相比 Vuex 更轻量
迁移前的准备工作
版本确认
确保你的项目满足以下条件:
{
"vue": "^3.2.0",
"pinia": "^2.1.0"
}
如果你的项目还在 Vue 2,需要使用 @vue/composition-api 并安装 Pinia 的兼容版本,但强烈建议先升级到 Vue 3。
安装 Pinia
npm install pinia
在 main.js 中注册:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
核心迁移步骤与踩坑记录
坑一:mutations 的消失
这是迁移中最直观的变化。Vuex 中修改 state 必须通过 mutations,而 Pinia 中可以直接在 actions 里修改 state。
Vuex 写法:
const store = {
state: { count: 0 },
mutations: {
INCREMENT(state, payload) {
state.count += payload
}
},
actions: {
incrementAsync({ commit }, payload) {
setTimeout(() => commit('INCREMENT', payload), 1000)
}
}
}
Pinia 写法:
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
actions: {
increment(payload) {
this.count += payload
},
incrementAsync(payload) {
setTimeout(() => {
this.count += payload
}, 1000)
}
}
})
踩坑点:原来在组件中调用 commit('INCREMENT', 1) 的地方,需要全部改成调用 action 或直接修改 state。如果直接修改 state(store.count++),在 Pinia 中是允许的,但为了可维护性,建议统一走 actions。
坑二:getters 的 this 指向问题
Vuex 的 getters 接收 state 作为第一个参数,而 Pinia 的 getters 使用 this 访问 state 和其他 getters。
Vuex:
getters: {
doubleCount: (state) => state.count * 2,
tripleCount: (state, getters) => getters.doubleCount + state.count
}
Pinia:
getters: {
doubleCount: (state) => state.count * 2,
tripleCount() {
return this.doubleCount + this.count
}
}
踩坑点:如果使用箭头函数定义 getter,this 会指向 undefined。必须使用普通函数。另外,在 getter 中访问其他 getter 时,TypeScript 类型推断可能会失效,需要手动标注返回类型:
tripleCount(): number {
return this.doubleCount + this.count
}
坑三:模块拆分的思维转变
Vuex 使用 modules 嵌套,而 Pinia 是扁平的,每个 store 独立定义。
Vuex 模块:
const userModule = {
namespaced: true,
state: () => ({ name: '' }),
mutations: { SET_NAME(state, name) { state.name = name } }
}
const store = createStore({
modules: { user: userModule }
})
// 调用
store.commit('user/SET_NAME', '张三')
Pinia:
// stores/user.js
export const useUserStore = defineStore('user', {
state: () => ({ name: '' }),
actions: {
setName(name) {
this.name = name
}
}
})
// 组件中调用
const userStore = useUserStore()
userStore.setName('张三')
踩坑点:原来通过 store.state.user.name 访问的方式全部失效。Pinia 中需要先获取对应的 store 实例。如果不同 store 之间需要互相访问,可以直接在 action 中引入其他 store:
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', {
actions: {
checkout() {
const userStore = useUserStore()
console.log(userStore.name)
}
}
})
坑四:持久化插件的替换
Vuex 时代常用 vuex-persistedstate,迁移到 Pinia 后需要换成 pinia-plugin-persistedstate。
npm install pinia-plugin-persistedstate
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
在 store 中配置:
export const useUserStore = defineStore('user', {
state: () => ({ token: '', name: '' }),
persist: {
key: 'user-store',
storage: localStorage,
paths: ['token'] // 只持久化 token
}
})
踩坑点:paths 配置项在旧版本中叫 pick,升级后需要注意。另外,如果 state 中有不可序列化的数据(如 Date 对象、Map),持久化后会变成字符串,需要手动处理。
坑五:TypeScript 类型推断的细节
Pinia 对 TypeScript 的支持非常好,但有几个地方需要注意。
state 的类型推断:
interface UserState {
name: string
age: number
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({
name: '',
age: 0
})
})
踩坑点:如果 state 中某个字段可能为 null 或 undefined,需要显式标注:
state: (): { user: User | null } => ({
user: null
})
否则 TypeScript 会推断为 null 类型,后续赋值会报错。
坑六:在组件外使用 store
在路由守卫、axios 拦截器等组件外的地方使用 store 时,必须确保 Pinia 已经安装。
// router/index.js
import { useUserStore } from '@/stores/user'
router.beforeEach((to, from, next) => {
const userStore = useUserStore() // 这里必须在 app.use(pinia) 之后执行
if (!userStore.token && to.meta.requiresAuth) {
next('/login')
} else {
next()
}
})
踩坑点:如果在 main.js 中先引入 router,再 app.use(pinia),路由守卫中调用 useUserStore() 会报错 “getActivePinia was called with no active Pinia”。解决办法是调整顺序,先安装 Pinia,再引入 router,或者将 store 的调用放到守卫函数内部(延迟执行)。
迁移后的收益
完成迁移后,项目获得了以下提升:
- 代码量减少约 30%:去掉了大量模板化的 mutations
- 类型提示完善:几乎不需要手动标注类型
- 调试体验更好:DevTools 中 store 结构更清晰
- 开发效率提升:新增 store 只需几行代码
总结
从 Vuex 迁移到 Pinia 整体上是值得的,但需要注意几个关键点:
- mutations 不复存在,逻辑统一收敛到 actions
- getters 使用普通函数,避免箭头函数的 this 问题
- 模块拆分要扁平化,跨 store 调用直接引入
- 持久化插件需要替换,注意配置项的变化
- 组件外使用 store 要注意安装顺序
- TypeScript 类型标注要细致,尤其是可空字段
建议迁移时按模块逐步进行,先迁移简单的 store,积累经验后再处理复杂的。同时保留一段时间的双轨运行,确保业务不受影响。希望这篇踩坑记录能帮助你顺利完成迁移。
未经允许不得转载:任鹏个人博客 » Pinia 实战:从 Vuex 迁移到 Pinia 的完整踩坑记录


朋友圈点赞图在线生成源码