Vuex 迁移指南 #

概述 #

迁移准备 #

1. 评估迁移成本 #

text
迁移评估清单
├── Store 数量 ──────── 多少个 module
├── 依赖关系 ────────── Store 之间的依赖
├── TypeScript 使用 ─── 是否使用 TS
├── 插件使用 ────────── 使用了哪些 Vuex 插件
└── 组件引用 ────────── 组件中如何使用 Store

2. 安装 Pinia #

bash
npm install pinia

3. 配置 Pinia #

ts
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)
app.mount('#app')

渐进式迁移 #

方案一:并行运行 #

Vuex 和 Pinia 可以同时存在,允许渐进式迁移:

ts
// main.ts
import { createStore } from 'vuex'
import { createPinia } from 'pinia'

const store = createStore({ /* Vuex 配置 */ })
const pinia = createPinia()

const app = createApp(App)
app.use(store)   // 保留 Vuex
app.use(pinia)   // 添加 Pinia

方案二:逐个迁移 #

按模块逐个迁移,每次迁移一个 Store。

迁移步骤 #

1. 转换单个 Module #

Vuex Module #

ts
// store/modules/user.ts
import { Module } from 'vuex'
import { RootState } from '../index'

interface UserState {
  user: User | null
  token: string | null
}

const userModule: Module<UserState, RootState> = {
  namespaced: true,
  
  state: () => ({
    user: null,
    token: null
  }),
  
  getters: {
    isAuthenticated: (state) => !!state.token,
    displayName: (state) => state.user?.name || 'Guest'
  },
  
  mutations: {
    SET_USER(state, user) {
      state.user = user
    },
    
    SET_TOKEN(state, token) {
      state.token = token
    },
    
    CLEAR_USER(state) {
      state.user = null
      state.token = null
    }
  },
  
  actions: {
    async login({ commit }, { email, password }) {
      const response = await api.login({ email, password })
      commit('SET_USER', response.user)
      commit('SET_TOKEN', response.token)
    },
    
    logout({ commit }) {
      commit('CLEAR_USER')
    }
  }
}

export default userModule

Pinia Store #

ts
// stores/user.ts
import { defineStore } from 'pinia'

interface UserState {
  user: User | null
  token: string | null
}

export const useUserStore = defineStore('user', {
  state: (): UserState => ({
    user: null,
    token: null
  }),
  
  getters: {
    isAuthenticated: (state) => !!state.token,
    displayName: (state) => state.user?.name || 'Guest'
  },
  
  actions: {
    async login(email: string, password: string) {
      const response = await api.login({ email, password })
      this.user = response.user
      this.token = response.token
    },
    
    logout() {
      this.user = null
      this.token = null
    }
  }
})

2. 转换规则 #

State #

ts
// Vuex
state: () => ({
  count: 0
})

// Pinia(相同)
state: () => ({
  count: 0
})

Getters #

ts
// Vuex
getters: {
  doubleCount: (state) => state.count * 2,
  tripleCount: (state, getters) => state.count + getters.doubleCount
}

// Pinia
getters: {
  doubleCount: (state) => state.count * 2,
  tripleCount(): number {
    return this.count + this.doubleCount
  }
}

Mutations → Actions #

ts
// Vuex
mutations: {
  INCREMENT(state) {
    state.count++
  },
  SET_VALUE(state, value) {
    state.count = value
  }
}

// Pinia(合并到 actions)
actions: {
  increment() {
    this.count++
  },
  setValue(value: number) {
    this.count = value
  }
}

Actions #

ts
// Vuex
actions: {
  async fetchData({ commit }) {
    const data = await api.getData()
    commit('SET_DATA', data)
  }
}

// Pinia
actions: {
  async fetchData() {
    this.data = await api.getData()
  }
}

3. 更新组件引用 #

Options API #

ts
// Vuex
export default {
  computed: {
    user() {
      return this.$store.state.user.user
    },
    isAuthenticated() {
      return this.$store.getters['user/isAuthenticated']
    }
  },
  methods: {
    login() {
      this.$store.dispatch('user/login', { email, password })
    }
  }
}

// Pinia
import { useUserStore } from '@/stores/user'

export default {
  computed: {
    user() {
      return useUserStore().user
    },
    isAuthenticated() {
      return useUserStore().isAuthenticated
    }
  },
  methods: {
    login() {
      useUserStore().login(this.email, this.password)
    }
  }
}

Composition API #

ts
// Vuex
import { useStore } from 'vuex'
import { computed } from 'vue'

export default {
  setup() {
    const store = useStore()
    
    const user = computed(() => store.state.user.user)
    const isAuthenticated = computed(() => store.getters['user/isAuthenticated'])
    
    function login() {
      store.dispatch('user/login', { email, password })
    }
    
    return { user, isAuthenticated, login }
  }
}

// Pinia
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'

export default {
  setup() {
    const userStore = useUserStore()
    
    const { user, isAuthenticated } = storeToRefs(userStore)
    const { login } = userStore
    
    return { user, isAuthenticated, login }
  }
}

4. 处理模块嵌套 #

Vuex 嵌套模块 #

ts
// Vuex
const store = createStore({
  modules: {
    user: {
      namespaced: true,
      modules: {
        profile: profileModule,
        settings: settingsModule
      }
    }
  }
})

// 访问
this.$store.state.user.profile.name
this.$store.dispatch('user/profile/updateName')

Pinia 扁平化 #

ts
// Pinia(扁平化)
// stores/userProfile.ts
export const useUserProfileStore = defineStore('userProfile', { /* ... */ })

// stores/userSettings.ts
export const useUserSettingsStore = defineStore('userSettings', { /* ... */ })

// 使用
const profileStore = useUserProfileStore()
const settingsStore = useUserSettingsStore()

profileStore.name
profileStore.updateName()

5. 迁移插件 #

Vuex 插件 #

ts
// Vuex 插件
const vuexPlugin = (store) => {
  store.subscribe((mutation, state) => {
    localStorage.setItem('state', JSON.stringify(state))
  })
}

Pinia 插件 #

ts
// Pinia 插件
const piniaPlugin = ({ store }) => {
  store.$subscribe((mutation, state) => {
    localStorage.setItem(store.$id, JSON.stringify(state))
  })
}

迁移清单 #

迁移前 #

  • [ ] 备份项目代码
  • [ ] 列出所有 Vuex modules
  • [ ] 检查插件依赖
  • [ ] 确定迁移顺序

迁移中 #

  • [ ] 安装 Pinia
  • [ ] 配置 Pinia
  • [ ] 逐个转换 modules
  • [ ] 更新组件引用
  • [ ] 迁移插件
  • [ ] 更新测试

迁移后 #

  • [ ] 移除 Vuex 依赖
  • [ ] 清理旧代码
  • [ ] 更新文档
  • [ ] 全面测试

常见迁移问题 #

1. mapState/mapGetters/mapActions #

ts
// Vuex
import { mapState, mapGetters, mapActions } from 'vuex'

export default {
  computed: {
    ...mapState('user', ['name', 'email']),
    ...mapGetters('user', ['isAuthenticated'])
  },
  methods: {
    ...mapActions('user', ['login', 'logout'])
  }
}

// Pinia
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'

export default {
  setup() {
    const userStore = useUserStore()
    const { name, email, isAuthenticated } = storeToRefs(userStore)
    const { login, logout } = userStore
    
    return { name, email, isAuthenticated, login, logout }
  }
}

2. 动态模块注册 #

ts
// Vuex
store.registerModule('dynamic', dynamicModule)

// Pinia
const useDynamicStore = defineStore('dynamic', { /* ... */ })
const dynamicStore = useDynamicStore()  // 自动注册

3. 热更新 #

ts
// Vuex
if (import.meta.hot) {
  import.meta.hot.accept(['./store'], () => {
    store.hotUpdate(newStore)
  })
}

// Pinia
if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useUserStore, import.meta.hot))
}

4. 严格模式 #

ts
// Vuex
const store = createStore({
  strict: process.env.NODE_ENV !== 'production'
})

// Pinia(无需配置,开发模式自动警告)

迁移脚本示例 #

ts
// scripts/migrate-vuex-to-pinia.ts
import * as fs from 'fs'
import * as path from 'path'

function convertVuexToPinia(content: string): string {
  // 转换 state
  content = content.replace(
    /state:\s*\(\)\s*=>\s*\(\{([^}]*)\}\)/g,
    'state: () => ({$1})'
  )
  
  // 转换 mutations 到 actions
  content = content.replace(
    /mutations:\s*\{([^}]*)\}/g,
    'actions: {$1}'
  )
  
  // 移除 namespaced
  content = content.replace(/namespaced:\s*true,?\s*/g, '')
  
  return content
}

function processFile(filePath: string) {
  const content = fs.readFileSync(filePath, 'utf-8')
  const converted = convertVuexToPinia(content)
  fs.writeFileSync(filePath, converted)
}

// 遍历 store 目录
function migrateStores(storeDir: string) {
  const files = fs.readdirSync(storeDir)
  
  files.forEach(file => {
    const filePath = path.join(storeDir, file)
    if (file.endsWith('.ts')) {
      processFile(filePath)
    }
  })
}

迁移时间估算 #

项目规模 Store 数量 预计时间
小型 1-5 1-2 天
中型 5-15 3-5 天
大型 15+ 1-2 周

总结 #

迁移到 Pinia 的主要好处:

  1. 更简洁的代码:减少约 40% 的代码量
  2. 更好的 TypeScript 支持:开箱即用的类型推断
  3. 更灵活的结构:无需嵌套模块
  4. 更好的开发体验:简化的 API 和调试

迁移完成后,你将享受到 Pinia 带来的所有优势!

最后更新:2026-03-28