Vuex 兼容性指南
本指南帮助您在现有 Vuex 项目中逐步引入 Pinia,实现平滑迁移而不破坏现有功能。
概述
Pinia 和 Vuex 可以在同一个项目中共存,这使得您可以:
- 逐步迁移现有的 Vuex 模块
- 为新功能使用 Pinia
- 在迁移过程中保持应用稳定
- 测试和验证迁移效果
设置共存环境
1. 安装 Pinia
bash
npm install pinia
# 或
yarn add pinia
# 或
pnpm add pinia
2. 同时配置 Vuex 和 Pinia
ts
// main.ts
import { createApp } from 'vue'
import { createStore } from 'vuex'
import { createPinia } from 'pinia'
import App from './App.vue'
// 现有的 Vuex store
const vuexStore = createStore({
// 您现有的 Vuex 配置
modules: {
user: userModule,
products: productsModule,
// ... 其他模块
}
})
// 新的 Pinia 实例
const pinia = createPinia()
const app = createApp(App)
// 同时使用两个状态管理库
app.use(vuexStore)
app.use(pinia)
app.mount('#app')
3. TypeScript 配置
ts
// types/store.ts
import type { Store } from 'vuex'
import type { Pinia } from 'pinia'
// 扩展 ComponentCustomProperties
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$store: Store<any>
$pinia: Pinia
}
}
渐进式迁移策略
策略 1:按模块迁移
逐个将 Vuex 模块转换为 Pinia stores。
ts
// 原 Vuex 模块
// store/modules/cart.js
const cartModule = {
namespaced: true,
state: {
items: [],
total: 0
},
getters: {
itemCount: (state) => state.items.length,
totalPrice: (state) => state.total
},
mutations: {
ADD_ITEM(state, item) {
state.items.push(item)
state.total += item.price
},
REMOVE_ITEM(state, itemId) {
const index = state.items.findIndex(item => item.id === itemId)
if (index > -1) {
state.total -= state.items[index].price
state.items.splice(index, 1)
}
}
},
actions: {
addItem({ commit }, item) {
commit('ADD_ITEM', item)
},
removeItem({ commit }, itemId) {
commit('REMOVE_ITEM', itemId)
}
}
}
// 转换为 Pinia store
// stores/cart.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export interface CartItem {
id: string
name: string
price: number
quantity: number
}
export const useCartStore = defineStore('cart', () => {
// State
const items = ref<CartItem[]>([])
const total = ref(0)
// Getters
const itemCount = computed(() => items.value.length)
const totalPrice = computed(() => total.value)
// Actions
function addItem(item: CartItem) {
items.value.push(item)
total.value += item.price
}
function removeItem(itemId: string) {
const index = items.value.findIndex(item => item.id === itemId)
if (index > -1) {
total.value -= items.value[index].price
items.value.splice(index, 1)
}
}
return {
// State
items: readonly(items),
total: readonly(total),
// Getters
itemCount,
totalPrice,
// Actions
addItem,
removeItem
}
})
策略 2:按功能迁移
为新功能使用 Pinia,保持现有功能使用 Vuex。
ts
// 新功能使用 Pinia
// stores/notifications.ts
export const useNotificationsStore = defineStore('notifications', () => {
const notifications = ref<Notification[]>([])
function addNotification(notification: Notification) {
notifications.value.push({
...notification,
id: Date.now().toString(),
timestamp: new Date()
})
}
function removeNotification(id: string) {
const index = notifications.value.findIndex(n => n.id === id)
if (index > -1) {
notifications.value.splice(index, 1)
}
}
return {
notifications: readonly(notifications),
addNotification,
removeNotification
}
})
// 在组件中同时使用两者
// components/Header.vue
<template>
<header>
<!-- Vuex 数据 -->
<div>用户: {{ $store.getters['user/fullName'] }}</div>
<!-- Pinia 数据 -->
<div>通知: {{ notificationsStore.notifications.length }}</div>
</header>
</template>
<script setup lang="ts">
import { useNotificationsStore } from '@/stores/notifications'
const notificationsStore = useNotificationsStore()
</script>
兼容性工具
1. 状态同步工具
在迁移过程中,您可能需要在 Vuex 和 Pinia 之间同步某些状态。
ts
// utils/state-sync.ts
import { watch } from 'vue'
import type { Store } from 'vuex'
import type { StoreGeneric } from 'pinia'
export function syncVuexToPinia<T>(
vuexStore: Store<any>,
vuexPath: string,
piniaStore: StoreGeneric,
piniaProperty: keyof T
) {
// 监听 Vuex 状态变化,同步到 Pinia
watch(
() => vuexStore.getters[vuexPath] || vuexStore.state[vuexPath],
(newValue) => {
(piniaStore as any)[piniaProperty] = newValue
},
{ immediate: true }
)
}
export function syncPiniaToVuex<T>(
piniaStore: StoreGeneric,
piniaProperty: keyof T,
vuexStore: Store<any>,
vuexMutation: string
) {
// 监听 Pinia 状态变化,同步到 Vuex
watch(
() => (piniaStore as any)[piniaProperty],
(newValue) => {
vuexStore.commit(vuexMutation, newValue)
}
)
}
// 使用示例
// 在组件或插件中
syncVuexToPinia(
vuexStore,
'user/currentUser',
userPiniaStore,
'currentUser'
)
2. 统一访问接口
创建一个统一的接口来访问 Vuex 和 Pinia 状态。
ts
// composables/useUnifiedStore.ts
import { computed } from 'vue'
import { useStore } from 'vuex'
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'
export function useUnifiedStore() {
const vuexStore = useStore()
const userStore = useUserStore()
const cartStore = useCartStore()
return {
// Vuex 状态(逐步迁移中)
products: computed(() => vuexStore.state.products.items),
productsLoading: computed(() => vuexStore.state.products.loading),
// Pinia 状态(已迁移)
user: computed(() => userStore.user),
cartItems: computed(() => cartStore.items),
// 统一的 actions
async fetchProducts() {
return vuexStore.dispatch('products/fetchProducts')
},
async login(credentials: LoginCredentials) {
return userStore.login(credentials)
},
addToCart(item: CartItem) {
cartStore.addItem(item)
}
}
}
3. 迁移助手
ts
// utils/migration-helper.ts
export class MigrationHelper {
private migratedModules = new Set<string>()
markAsMigrated(moduleName: string) {
this.migratedModules.add(moduleName)
console.log(`✅ 模块 "${moduleName}" 已迁移到 Pinia`)
}
isMigrated(moduleName: string): boolean {
return this.migratedModules.has(moduleName)
}
getMigrationStatus() {
return {
migrated: Array.from(this.migratedModules),
total: this.migratedModules.size
}
}
// 创建迁移检查装饰器
createMigrationCheck(moduleName: string) {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value
descriptor.value = function(...args: any[]) {
if (!this.isMigrated(moduleName)) {
console.warn(`⚠️ 模块 "${moduleName}" 尚未迁移到 Pinia`)
}
return originalMethod.apply(this, args)
}
}
}
}
export const migrationHelper = new MigrationHelper()
组件迁移模式
1. 渐进式组件迁移
vue
<!-- 迁移前:使用 Vuex -->
<template>
<div>
<h1>{{ fullName }}</h1>
<p>购物车商品数量: {{ cartItemCount }}</p>
<button @click="logout">退出登录</button>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
export default {
computed: {
...mapGetters('user', ['fullName']),
...mapGetters('cart', ['itemCount'])
},
methods: {
...mapActions('user', ['logout'])
}
}
</script>
vue
<!-- 迁移后:使用 Pinia -->
<template>
<div>
<h1>{{ userStore.fullName }}</h1>
<p>购物车商品数量: {{ cartStore.itemCount }}</p>
<button @click="userStore.logout">退出登录</button>
</div>
</template>
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'
const userStore = useUserStore()
const cartStore = useCartStore()
</script>
2. 混合使用模式
vue
<!-- 过渡期:同时使用 Vuex 和 Pinia -->
<template>
<div>
<!-- 使用 Pinia(已迁移) -->
<h1>{{ userStore.fullName }}</h1>
<!-- 使用 Vuex(待迁移) -->
<div>产品数量: {{ $store.getters['products/totalCount'] }}</div>
<button @click="handleAction">执行操作</button>
</div>
</template>
<script setup lang="ts">
import { useStore } from 'vuex'
import { useUserStore } from '@/stores/user'
const vuexStore = useStore()
const userStore = useUserStore()
function handleAction() {
// 同时调用 Vuex 和 Pinia actions
vuexStore.dispatch('products/fetchProducts')
userStore.updateLastActivity()
}
</script>
测试策略
1. 并行测试
ts
// tests/migration.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { createStore } from 'vuex'
import { setActivePinia, createPinia } from 'pinia'
import { useCartStore } from '@/stores/cart'
import cartModule from '@/store/modules/cart'
describe('购物车迁移测试', () => {
let vuexStore: any
let piniaStore: any
beforeEach(() => {
// 设置 Vuex
vuexStore = createStore({
modules: {
cart: cartModule
}
})
// 设置 Pinia
setActivePinia(createPinia())
piniaStore = useCartStore()
})
it('应该在两个实现中产生相同的结果', () => {
const testItem = { id: '1', name: '测试商品', price: 100, quantity: 1 }
// Vuex 操作
vuexStore.dispatch('cart/addItem', testItem)
// Pinia 操作
piniaStore.addItem(testItem)
// 验证结果一致
expect(vuexStore.getters['cart/itemCount']).toBe(piniaStore.itemCount)
expect(vuexStore.getters['cart/totalPrice']).toBe(piniaStore.totalPrice)
})
})
2. 兼容性测试
ts
// tests/compatibility.test.ts
import { mount } from '@vue/test-utils'
import { createStore } from 'vuex'
import { createPinia } from 'pinia'
import TestComponent from '@/components/TestComponent.vue'
describe('Vuex-Pinia 兼容性', () => {
it('应该能同时使用 Vuex 和 Pinia', () => {
const vuexStore = createStore({
state: { message: 'Hello from Vuex' }
})
const pinia = createPinia()
const wrapper = mount(TestComponent, {
global: {
plugins: [vuexStore, pinia]
}
})
expect(wrapper.vm.$store).toBeDefined()
expect(wrapper.vm.$pinia).toBeDefined()
})
})
性能考虑
1. 内存使用
ts
// utils/memory-monitor.ts
export function monitorMemoryUsage() {
if (process.env.NODE_ENV === 'development') {
setInterval(() => {
if (performance.memory) {
console.log('内存使用情况:', {
used: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024) + 'MB',
total: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024) + 'MB',
limit: Math.round(performance.memory.jsHeapSizeLimit / 1024 / 1024) + 'MB'
})
}
}, 10000) // 每10秒检查一次
}
}
2. 包大小优化
ts
// 动态导入减少初始包大小
// router/index.ts
const routes = [
{
path: '/legacy',
component: () => import('@/views/LegacyView.vue'), // 使用 Vuex
},
{
path: '/modern',
component: () => import('@/views/ModernView.vue'), // 使用 Pinia
}
]
迁移检查清单
准备阶段
- [ ] 安装 Pinia
- [ ] 配置 Vuex 和 Pinia 共存
- [ ] 设置 TypeScript 类型
- [ ] 创建迁移计划
迁移阶段
- [ ] 选择迁移策略(按模块或按功能)
- [ ] 转换第一个 Vuex 模块
- [ ] 更新相关组件
- [ ] 编写测试验证功能
- [ ] 重复迁移其他模块
验证阶段
- [ ] 运行所有测试
- [ ] 检查性能影响
- [ ] 验证 DevTools 功能
- [ ] 测试 SSR(如果适用)
清理阶段
- [ ] 移除未使用的 Vuex 代码
- [ ] 更新文档
- [ ] 卸载 Vuex 依赖
- [ ] 最终测试
常见问题
Q: 可以在同一个组件中同时使用 Vuex 和 Pinia 吗?
A: 是的,完全可以。这是渐进式迁移的核心特性。
vue
<script setup>
import { useStore } from 'vuex'
import { useUserStore } from '@/stores/user'
const vuexStore = useStore()
const piniaStore = useUserStore()
</script>
Q: 如何处理 Vuex 插件?
A: 将 Vuex 插件转换为 Pinia 插件:
ts
// Vuex 插件
const vuexPlugin = (store) => {
store.subscribe((mutation, state) => {
console.log(mutation.type)
})
}
// Pinia 插件
const piniaPlugin = ({ store }) => {
store.$subscribe((mutation, state) => {
console.log(mutation.storeId)
})
}
Q: 迁移会影响性能吗?
A: 短期内可能会有轻微的性能开销(同时运行两个状态管理库),但长期来看 Pinia 的性能更好。
Q: 如何处理 Vuex 的严格模式?
A: Pinia 默认允许直接修改状态,不需要严格模式。如果需要类似功能,可以使用开发工具监控。
最佳实践
1. 迁移顺序
- 从最简单的模块开始
- 优先迁移新功能
- 最后迁移核心业务逻辑
2. 代码组织
src/
├── store/ # Vuex(逐步移除)
│ ├── modules/
│ └── index.ts
├── stores/ # Pinia(新增)
│ ├── user.ts
│ ├── cart.ts
│ └── index.ts
└── composables/ # 统一接口
└── useUnifiedStore.ts
3. 文档更新
- 记录迁移进度
- 更新 API 文档
- 提供迁移指南给团队