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 的主要好处:
- 更简洁的代码:减少约 40% 的代码量
- 更好的 TypeScript 支持:开箱即用的类型推断
- 更灵活的结构:无需嵌套模块
- 更好的开发体验:简化的 API 和调试
迁移完成后,你将享受到 Pinia 带来的所有优势!
最后更新:2026-03-28