Pinia 实战:从 Vuex 迁移到 Pinia 的完整踩坑记录

前言

随着 Vue 3 的普及,官方推荐的状态管理库已经从 Vuex 转向了 Pinia。Pinia 提供了更简洁的 API、更好的 TypeScript 支持以及更符合组合式 API 的设计理念。最近我将一个中型后台管理系统从 Vuex 4 迁移到了 Pinia,整个过程并非一帆风顺。这篇文章记录了我在迁移过程中踩过的坑和对应的解决方案,希望能帮助正在或计划迁移的开发者少走弯路。

为什么要从 Vuex 迁移到 Pinia

在开始踩坑之前,先简单说下迁移的动机:

  • 更简洁的 API:去掉了 mutations,只有 stategettersactions
  • 完美的 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 整体上是值得的,但需要注意几个关键点:

  1. mutations 不复存在,逻辑统一收敛到 actions
  2. getters 使用普通函数,避免箭头函数的 this 问题
  3. 模块拆分要扁平化,跨 store 调用直接引入
  4. 持久化插件需要替换,注意配置项的变化
  5. 组件外使用 store 要注意安装顺序
  6. TypeScript 类型标注要细致,尤其是可空字段

建议迁移时按模块逐步进行,先迁移简单的 store,积累经验后再处理复杂的。同时保留一段时间的双轨运行,确保业务不受影响。希望这篇踩坑记录能帮助你顺利完成迁移。

未经允许不得转载:任鹏个人博客 » Pinia 实战:从 Vuex 迁移到 Pinia 的完整踩坑记录

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏