Skip to content

迁移指南

本指南将帮助您从其他状态管理解决方案迁移到 Pinia,包括详细的步骤、代码示例和最佳实践。

从 Vuex 迁移

主要差异

特性VuexPinia
语法Options APIComposition API
TypeScript需要复杂配置原生支持
模块嵌套模块扁平化 store
Mutations必需不需要
DevTools需要配置自动支持
包大小较大更小
SSR复杂简单

迁移步骤

1. 安装 Pinia

bash
npm uninstall vuex
npm install pinia

2. 替换 Vuex Store

Vuex (之前):

js
// store/index.js
import { createStore } from 'vuex'
import user from './modules/user'
import products from './modules/products'

export default createStore({
  modules: {
    user,
    products
  }
})

// store/modules/user.js
export default {
  namespaced: true,
  state: {
    currentUser: null,
    isLoggedIn: false
  },
  mutations: {
    SET_USER(state, user) {
      state.currentUser = user
      state.isLoggedIn = !!user
    },
    LOGOUT(state) {
      state.currentUser = null
      state.isLoggedIn = false
    }
  },
  actions: {
    async login({ commit }, credentials) {
      const user = await api.login(credentials)
      commit('SET_USER', user)
      return user
    },
    logout({ commit }) {
      commit('LOGOUT')
    }
  },
  getters: {
    userName: (state) => state.currentUser?.name || '',
    isAdmin: (state) => state.currentUser?.role === 'admin'
  }
}

Pinia (之后):

ts
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { User } from '@/types'

export const useUserStore = defineStore('user', () => {
  // State
  const currentUser = ref<User | null>(null)
  
  // Getters
  const isLoggedIn = computed(() => !!currentUser.value)
  const userName = computed(() => currentUser.value?.name || '')
  const isAdmin = computed(() => currentUser.value?.role === 'admin')
  
  // Actions
  const login = async (credentials: LoginCredentials) => {
    const user = await api.login(credentials)
    currentUser.value = user
    return user
  }
  
  const logout = () => {
    currentUser.value = null
  }
  
  return {
    // State
    currentUser: readonly(currentUser),
    
    // Getters
    isLoggedIn,
    userName,
    isAdmin,
    
    // Actions
    login,
    logout
  }
})

3. 更新应用入口

Vuex (之前):

js
// main.js
import { createApp } from 'vue'
import store from './store'
import App from './App.vue'

const app = createApp(App)
app.use(store)
app.mount('#app')

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')

4. 更新组件使用方式

Vuex (之前):

vue
<template>
  <div>
    <p v-if="isLoggedIn">欢迎,{{ userName }}!</p>
    <button @click="handleLogin">登录</button>
    <button @click="handleLogout">登出</button>
  </div>
</template>

<script>
import { mapState, mapGetters, mapActions } from 'vuex'

export default {
  computed: {
    ...mapState('user', ['currentUser']),
    ...mapGetters('user', ['isLoggedIn', 'userName', 'isAdmin'])
  },
  methods: {
    ...mapActions('user', ['login', 'logout']),
    
    async handleLogin() {
      await this.login({ email: 'user@example.com', password: 'password' })
    },
    
    handleLogout() {
      this.logout()
    }
  }
}
</script>

Pinia (之后):

vue
<template>
  <div>
    <p v-if="userStore.isLoggedIn">欢迎,{{ userStore.userName }}!</p>
    <button @click="handleLogin">登录</button>
    <button @click="userStore.logout">登出</button>
  </div>
</template>

<script setup>
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

const handleLogin = async () => {
  await userStore.login({ email: 'user@example.com', password: 'password' })
}
</script>

复杂迁移场景

嵌套模块迁移

Vuex 嵌套模块:

js
// store/modules/shop/index.js
export default {
  namespaced: true,
  modules: {
    cart: {
      namespaced: true,
      state: { items: [] },
      mutations: { ADD_ITEM(state, item) { state.items.push(item) } }
    },
    products: {
      namespaced: true,
      state: { list: [] },
      actions: { async fetchProducts({ commit }) { /* ... */ } }
    }
  }
}

Pinia 扁平化 Store:

ts
// stores/cart.ts
export const useCartStore = defineStore('cart', () => {
  const items = ref([])
  
  const addItem = (item) => {
    items.value.push(item)
  }
  
  return { items, addItem }
})

// stores/products.ts
export const useProductsStore = defineStore('products', () => {
  const list = ref([])
  
  const fetchProducts = async () => {
    const data = await api.getProducts()
    list.value = data
  }
  
  return { list, fetchProducts }
})

// stores/shop.ts (组合 store)
export const useShopStore = defineStore('shop', () => {
  const cartStore = useCartStore()
  const productsStore = useProductsStore()
  
  const addToCart = (productId) => {
    const product = productsStore.list.find(p => p.id === productId)
    if (product) {
      cartStore.addItem(product)
    }
  }
  
  return {
    cartStore,
    productsStore,
    addToCart
  }
})

插件迁移

Vuex 插件:

js
// vuex-persist.js
const vuexPersist = (store) => {
  store.subscribe((mutation, state) => {
    localStorage.setItem('vuex', JSON.stringify(state))
  })
}

// store/index.js
export default createStore({
  plugins: [vuexPersist]
})

Pinia 插件:

ts
// plugins/persist.ts
import type { PiniaPluginContext } from 'pinia'

export function piniaPersistedState(context: PiniaPluginContext) {
  const { store } = context
  
  // 恢复状态
  const saved = localStorage.getItem(`pinia-${store.$id}`)
  if (saved) {
    store.$patch(JSON.parse(saved))
  }
  
  // 监听变化
  store.$subscribe((mutation, state) => {
    localStorage.setItem(`pinia-${store.$id}`, JSON.stringify(state))
  })
}

// main.ts
const pinia = createPinia()
pinia.use(piniaPersistedState)

从 Redux 迁移

主要差异

特性ReduxPinia
样板代码大量最少
不可变性手动自动
中间件复杂简单
时间旅行内置DevTools
学习曲线陡峭平缓

迁移示例

Redux (之前):

js
// actions/user.js
export const LOGIN_REQUEST = 'LOGIN_REQUEST'
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'
export const LOGIN_FAILURE = 'LOGIN_FAILURE'
export const LOGOUT = 'LOGOUT'

export const loginRequest = () => ({ type: LOGIN_REQUEST })
export const loginSuccess = (user) => ({ type: LOGIN_SUCCESS, payload: user })
export const loginFailure = (error) => ({ type: LOGIN_FAILURE, payload: error })
export const logout = () => ({ type: LOGOUT })

export const login = (credentials) => async (dispatch) => {
  dispatch(loginRequest())
  try {
    const user = await api.login(credentials)
    dispatch(loginSuccess(user))
  } catch (error) {
    dispatch(loginFailure(error.message))
  }
}

// reducers/user.js
const initialState = {
  user: null,
  loading: false,
  error: null
}

export default function userReducer(state = initialState, action) {
  switch (action.type) {
    case LOGIN_REQUEST:
      return { ...state, loading: true, error: null }
    case LOGIN_SUCCESS:
      return { ...state, loading: false, user: action.payload }
    case LOGIN_FAILURE:
      return { ...state, loading: false, error: action.payload }
    case LOGOUT:
      return { ...state, user: null }
    default:
      return state
  }
}

// selectors/user.js
export const selectUser = (state) => state.user.user
export const selectIsLoading = (state) => state.user.loading
export const selectError = (state) => state.user.error
export const selectIsLoggedIn = (state) => !!state.user.user

Pinia (之后):

ts
// stores/user.ts
export const useUserStore = defineStore('user', () => {
  // State
  const user = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  // Getters
  const isLoggedIn = computed(() => !!user.value)
  
  // Actions
  const login = async (credentials) => {
    loading.value = true
    error.value = null
    
    try {
      const userData = await api.login(credentials)
      user.value = userData
    } catch (err) {
      error.value = err.message
      throw err
    } finally {
      loading.value = false
    }
  }
  
  const logout = () => {
    user.value = null
    error.value = null
  }
  
  return {
    // State
    user: readonly(user),
    loading: readonly(loading),
    error: readonly(error),
    
    // Getters
    isLoggedIn,
    
    // Actions
    login,
    logout
  }
})

渐进式迁移策略

1. 并行运行

在迁移期间,可以让 Vuex 和 Pinia 并行运行:

ts
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { createStore } from 'vuex'
import vuexStore from './store/vuex'

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

// 同时使用两个状态管理库
app.use(vuexStore)
app.use(pinia)

app.mount('#app')

2. 逐模块迁移

ts
// 迁移计划
const migrationPlan = {
  phase1: ['user', 'auth'],           // 第一阶段:核心模块
  phase2: ['products', 'cart'],       // 第二阶段:业务模块
  phase3: ['ui', 'settings']          // 第三阶段:辅助模块
}

// 创建迁移助手
const createMigrationHelper = () => {
  const migratedStores = new Set()
  
  return {
    markAsMigrated(storeName) {
      migratedStores.add(storeName)
    },
    
    isMigrated(storeName) {
      return migratedStores.has(storeName)
    },
    
    getProgress() {
      const total = Object.values(migrationPlan).flat().length
      return (migratedStores.size / total) * 100
    }
  }
}

3. 状态同步

在迁移期间保持 Vuex 和 Pinia 状态同步:

ts
// utils/stateSyncPlugin.ts
export function createStateSyncPlugin(vuexStore) {
  return (context: PiniaPluginContext) => {
    const { store } = context
    
    // Pinia -> Vuex
    store.$subscribe((mutation, state) => {
      if (vuexStore.hasModule(store.$id)) {
        vuexStore.commit(`${store.$id}/SYNC_FROM_PINIA`, state)
      }
    })
    
    // Vuex -> Pinia
    vuexStore.subscribe((mutation) => {
      if (mutation.type.startsWith(`${store.$id}/`) && 
          !mutation.type.includes('SYNC_FROM_PINIA')) {
        const vuexState = vuexStore.state[store.$id]
        store.$patch(vuexState)
      }
    })
  }
}

迁移检查清单

准备阶段

  • [ ] 分析现有 Vuex/Redux 代码结构
  • [ ] 识别依赖关系和模块边界
  • [ ] 制定迁移计划和时间表
  • [ ] 设置测试环境
  • [ ] 准备回滚策略

迁移阶段

  • [ ] 安装 Pinia 并配置
  • [ ] 创建对应的 Pinia store
  • [ ] 迁移状态定义
  • [ ] 迁移 getters/computed
  • [ ] 迁移 actions/mutations
  • [ ] 更新组件使用方式
  • [ ] 迁移插件和中间件
  • [ ] 更新类型定义

测试阶段

  • [ ] 单元测试通过
  • [ ] 集成测试通过
  • [ ] E2E 测试通过
  • [ ] 性能测试通过
  • [ ] 兼容性测试通过

清理阶段

  • [ ] 移除旧的状态管理代码
  • [ ] 清理未使用的依赖
  • [ ] 更新文档
  • [ ] 团队培训
  • [ ] 监控生产环境

常见迁移模式

异步 Actions

迁移前 (Vuex):

js
actions: {
  async fetchUserData({ commit, state }, userId) {
    if (state.users[userId]) {
      return state.users[userId]
    }
    
    commit('SET_LOADING', true)
    try {
      const user = await api.getUser(userId)
      commit('SET_USER', { userId, user })
      return user
    } catch (error) {
      commit('SET_ERROR', error.message)
      throw error
    } finally {
      commit('SET_LOADING', false)
    }
  }
}

迁移后 (Pinia):

ts
const fetchUserData = async (userId: string) => {
  if (users.value[userId]) {
    return users.value[userId]
  }
  
  loading.value = true
  try {
    const user = await api.getUser(userId)
    users.value[userId] = user
    return user
  } catch (err) {
    error.value = err.message
    throw err
  } finally {
    loading.value = false
  }
}

跨 Store 通信

迁移前 (Vuex):

js
// 通过 rootState 访问其他模块
actions: {
  addToCart({ commit, rootState }, productId) {
    const product = rootState.products.items.find(p => p.id === productId)
    if (product && rootState.user.isLoggedIn) {
      commit('ADD_ITEM', product)
    }
  }
}

迁移后 (Pinia):

ts
const addToCart = (productId: string) => {
  const productsStore = useProductsStore()
  const userStore = useUserStore()
  
  const product = productsStore.items.find(p => p.id === productId)
  if (product && userStore.isLoggedIn) {
    items.value.push(product)
  }
}

迁移工具

自动化迁移脚本

bash
#!/bin/bash
# migrate-to-pinia.sh

echo "开始 Vuex 到 Pinia 迁移..."

# 1. 安装 Pinia
npm install pinia
npm uninstall vuex

# 2. 创建 stores 目录
mkdir -p src/stores

# 3. 运行迁移工具
node scripts/vuex-to-pinia-migrator.js

echo "迁移完成!请检查生成的文件并进行必要的调整。"

迁移助手函数

ts
// utils/migrationHelpers.ts

// 将 Vuex mapState 转换为 Pinia
export function convertMapState(storeId: string, states: string[]) {
  return states.reduce((acc, state) => {
    acc[state] = computed(() => {
      const store = useStore(storeId)
      return store[state]
    })
    return acc
  }, {})
}

// 将 Vuex mapGetters 转换为 Pinia
export function convertMapGetters(storeId: string, getters: string[]) {
  return getters.reduce((acc, getter) => {
    acc[getter] = computed(() => {
      const store = useStore(storeId)
      return store[getter]
    })
    return acc
  }, {})
}

// 将 Vuex mapActions 转换为 Pinia
export function convertMapActions(storeId: string, actions: string[]) {
  return actions.reduce((acc, action) => {
    acc[action] = (...args: any[]) => {
      const store = useStore(storeId)
      return store[action](...args)
    }
    return acc
  }, {})
}

性能考虑

包大小对比

bash
# 分析包大小
npm run build -- --analyze

# Vuex vs Pinia 包大小对比
# Vuex: ~2.6KB (gzipped)
# Pinia: ~1.3KB (gzipped)

运行时性能

ts
// 性能测试
const performanceTest = () => {
  const iterations = 10000
  
  // 测试状态更新性能
  console.time('Pinia state updates')
  for (let i = 0; i < iterations; i++) {
    store.updateCounter(i)
  }
  console.timeEnd('Pinia state updates')
  
  // 测试计算属性性能
  console.time('Pinia computed access')
  for (let i = 0; i < iterations; i++) {
    const value = store.computedValue
  }
  console.timeEnd('Pinia computed access')
}

故障排除

常见问题

1. TypeScript 类型错误

ts
// 问题:类型推断失败
// 解决:明确类型定义
interface UserState {
  user: User | null
  loading: boolean
}

export const useUserStore = defineStore('user', (): UserState => {
  // 实现
})

2. 响应式丢失

ts
// 问题:解构导致响应式丢失
const { user, isLoggedIn } = userStore // ❌ 错误

// 解决:使用 storeToRefs
const { user, isLoggedIn } = storeToRefs(userStore) // ✅ 正确

3. SSR 水合问题

ts
// 问题:服务端和客户端状态不一致
// 解决:正确处理 SSR 状态序列化
if (process.client) {
  // 客户端特定逻辑
}

调试技巧

ts
// 开发环境调试
if (import.meta.env.DEV) {
  // 添加全局 store 访问
  window.__PINIA_STORES__ = {}
  
  pinia.use(({ store }) => {
    window.__PINIA_STORES__[store.$id] = store
    
    // 添加调试信息
    store.$onAction(({ name, args }) => {
      console.log(`Action ${name} called with:`, args)
    })
  })
}

最佳实践

迁移策略

  1. 小步快跑:逐个模块迁移,避免大爆炸式重构
  2. 保持向后兼容:在迁移期间保持 API 兼容性
  3. 充分测试:每个迁移步骤都要有对应的测试
  4. 文档更新:及时更新开发文档和 API 文档
  5. 团队培训:确保团队成员了解新的状态管理方式

代码组织

src/
├── stores/
│   ├── index.ts          # Store 导出
│   ├── user.ts           # 用户 store
│   ├── products.ts       # 产品 store
│   └── utils/            # Store 工具
│       ├── api.ts
│       └── cache.ts
├── composables/          # 组合式函数
│   ├── useAuth.ts
│   └── useApi.ts
└── types/                # 类型定义
    ├── store.ts
    └── api.ts

相关资源

Released under the MIT License.