随着 Vue 3 的正式发布,状态管理库也迎来了新的变革。Pinia 作为 Vue 官方推荐的新一代状态管理库,被誉为”事实上的 Vuex 5”,为 Vue 3 应用提供了更加简洁、类型安全的状态管理解决方案。本文将深入探讨 Pinia 的核心特性、使用方法以及在实际项目中的最佳实践。
Pinia vs Vuex:为什么选择 Pinia?
Pinia 的核心优势
1. 更简洁的 API 设计
Pinia 移除了 Vuex 中冗长的 mutations,直接通过 actions 修改状态,大大简化了代码结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| const store = new Vuex.Store({ state: { count: 0 }, mutations: { INCREMENT(state) { state.count++ } }, actions: { increment({ commit }) { commit('INCREMENT') } } })
export const useCounterStore = defineStore('counter', { state: () => ({ count: 0 }), actions: { increment() { this.count++ } } })
|
2. 完整的 TypeScript 支持
Pinia 从设计之初就考虑了 TypeScript 支持,提供了完整的类型推断和自动补全功能。
3. 模块化设计
Pinia 天然支持多个 store,无需像 Vuex 那样使用复杂的模块系统:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| export const useUserStore = defineStore('user', { state: () => ({ userInfo: null, isLoggedIn: false }), getters: { userName: (state) => state.userInfo?.name || '游客' }, actions: { async login(credentials) { const response = await api.login(credentials) this.userInfo = response.data this.isLoggedIn = true } } })
export const useCartStore = defineStore('cart', { state: () => ({ items: [], total: 0 }), getters: { itemCount: (state) => state.items.length, totalPrice: (state) => state.items.reduce((sum, item) => sum + item.price * item.quantity, 0) }, actions: { addItem(product) { const existingItem = this.items.find(item => item.id === product.id) if (existingItem) { existingItem.quantity++ } else { this.items.push({ ...product, quantity: 1 }) } }, removeItem(productId) { const index = this.items.findIndex(item => item.id === productId) if (index > -1) { this.items.splice(index, 1) } } } })
|
Pinia 核心概念详解
1. State(状态)
State 定义为返回初始状态的函数,确保服务端渲染时的状态隔离:
1 2 3 4 5 6 7 8 9 10 11 12
| export const useProductStore = defineStore('product', { state: () => ({ products: [], loading: false, error: null, filters: { category: '', priceRange: [0, 1000], sortBy: 'name' } }) })
|
2. Getters(计算属性)
Getters 类似于 Vue 的计算属性,支持参数传递和缓存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| export const useProductStore = defineStore('product', { state: () => ({ products: [] }), getters: { productCount: (state) => state.products.length, hasProducts: (state) => state.products.length > 0, getProductById: (state) => { return (productId) => state.products.find(product => product.id === productId) }, cartItemsWithDetails() { const cartStore = useCartStore() return cartStore.items.map(item => ({ ...item, product: this.getProductById(item.productId) })) } } })
|
3. Actions(操作)
Actions 可以是异步的,支持直接修改状态:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| export const useProductStore = defineStore('product', { state: () => ({ products: [], loading: false, error: null }), actions: { async fetchProducts() { this.loading = true this.error = null try { const response = await api.getProducts() this.products = response.data } catch (error) { this.error = error.message console.error('获取产品列表失败:', error) } finally { this.loading = false } }, async createProduct(productData) { try { const response = await api.createProduct(productData) this.products.push(response.data) return response.data } catch (error) { this.error = error.message throw error } }, updateProduct(productId, updates) { const index = this.products.findIndex(p => p.id === productId) if (index !== -1) { this.products[index] = { ...this.products[index], ...updates } } } } })
|
组合式 API 风格的 Store
Pinia 还支持使用组合式 API 风格定义 store:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| export const useCounterStore = defineStore('counter', () => { const count = ref(0) const name = ref('Eduardo') const doubleCount = computed(() => count.value * 2) function increment() { count.value++ } async function fetchData() { const response = await api.getData() count.value = response.count } return { count, name, doubleCount, increment, fetchData } })
|
在组件中使用 Pinia
基础使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| <template> <div> <h2>产品列表 ({{ productStore.productCount }})</h2> <div v-if="productStore.loading">加载中...</div> <div v-else-if="productStore.error" class="error"> 错误: {{ productStore.error }} </div> <div v-else> <div v-for="product in productStore.products" :key="product.id" class="product-card"> <h3>{{ product.name }}</h3> <p>价格: ¥{{ product.price }}</p> <button @click="addToCart(product)">加入购物车</button> </div> </div> <div class="cart-summary"> 购物车商品数量: {{ cartStore.itemCount }} 总价: ¥{{ cartStore.totalPrice }} </div> </div> </template>
<script setup> import { onMounted } from 'vue' import { useProductStore } from '@/stores/product' import { useCartStore } from '@/stores/cart'
const productStore = useProductStore() const cartStore = useCartStore()
// 组件挂载时获取产品列表 onMounted(() => { productStore.fetchProducts() })
// 添加到购物车 function addToCart(product) { cartStore.addItem(product) } </script>
|
使用 storeToRefs 保持响应性
当需要解构 store 中的状态时,使用 storeToRefs 保持响应性:
1 2 3 4 5 6 7 8 9 10 11 12
| <script setup> import { storeToRefs } from 'pinia' import { useProductStore } from '@/stores/product'
const productStore = useProductStore()
// 解构状态(保持响应性) const { products, loading, error } = storeToRefs(productStore)
// 解构方法(不需要 storeToRefs) const { fetchProducts, createProduct } = productStore </script>
|
高级特性
1. Store 订阅
监听 store 状态变化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| productStore.$subscribe((mutation, state) => { console.log('Store 发生变化:', mutation.type) console.log('新状态:', state) localStorage.setItem('productStore', JSON.stringify(state)) })
productStore.$onAction(({ name, store, args, after, onError }) => { console.log(`Action "${name}" 开始执行,参数:`, args) after((result) => { console.log(`Action "${name}" 执行完成,结果:`, result) }) onError((error) => { console.error(`Action "${name}" 执行失败:`, error) }) })
|
2. 插件系统
创建持久化插件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| export function createPersistedState(options = {}) { return (context) => { const { store } = context const storageKey = options.key || store.$id const savedState = localStorage.getItem(storageKey) if (savedState) { store.$patch(JSON.parse(savedState)) } store.$subscribe((mutation, state) => { localStorage.setItem(storageKey, JSON.stringify(state)) }) } }
import { createApp } from 'vue' import { createPinia } from 'pinia' import { createPersistedState } from './plugins/persistence'
const pinia = createPinia() pinia.use(createPersistedState())
const app = createApp(App) app.use(pinia) app.mount('#app')
|
3. 测试 Store
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| import { describe, it, expect, beforeEach } from 'vitest' import { setActivePinia, createPinia } from 'pinia' import { useProductStore } from '@/stores/product'
describe('Product Store', () => { beforeEach(() => { setActivePinia(createPinia()) }) it('应该正确初始化状态', () => { const store = useProductStore() expect(store.products).toEqual([]) expect(store.loading).toBe(false) expect(store.error).toBeNull() }) it('应该正确计算产品数量', () => { const store = useProductStore() store.products = [ { id: 1, name: '产品1', price: 100 }, { id: 2, name: '产品2', price: 200 } ] expect(store.productCount).toBe(2) }) it('应该正确添加产品', () => { const store = useProductStore() const newProduct = { id: 1, name: '新产品', price: 150 } store.products.push(newProduct) expect(store.products).toContain(newProduct) expect(store.productCount).toBe(1) }) })
|
最佳实践
1. Store 组织结构
1 2 3 4 5 6 7 8
| stores/ ├── index.js # 导出所有 stores ├── user.js # 用户相关状态 ├── product.js # 产品相关状态 ├── cart.js # 购物车状态 └── modules/ ├── auth.js # 认证模块 └── notification.js # 通知模块
|
2. 命名约定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| export const useUserStore = defineStore('user', { }) export const useProductStore = defineStore('product', { }) export const useShoppingCartStore = defineStore('shoppingCart', { })
actions: { fetchProducts, createProduct, updateProduct, deleteProduct }
getters: { isLoading, hasProducts, getProductById, filteredProducts }
|
3. 错误处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| export const useApiStore = defineStore('api', { state: () => ({ loading: false, error: null }), actions: { async handleApiCall(apiFunction, ...args) { this.loading = true this.error = null try { const result = await apiFunction(...args) return result } catch (error) { this.error = { message: error.message, code: error.code, timestamp: new Date().toISOString() } throw error } finally { this.loading = false } } } })
|
性能优化建议
1. 合理拆分 Store
避免创建过大的 store,按功能模块拆分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| export const useAppStore = defineStore('app', { state: () => ({ user: {}, products: [], cart: [], orders: [], notifications: [] }) })
export const useUserStore = defineStore('user', { }) export const useProductStore = defineStore('product', { }) export const useCartStore = defineStore('cart', { })
|
2. 使用 $patch 批量更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| store.loading = true store.error = null store.data = newData
store.$patch({ loading: true, error: null, data: newData })
store.$patch((state) => { state.items.push(newItem) state.hasChanged = true })
|
总结
Pinia 作为 Vue 3 时代的状态管理解决方案,以其简洁的 API、完整的 TypeScript 支持和强大的开发者体验,正在成为 Vue 开发者的首选。相比 Vuex,Pinia 不仅减少了样板代码,还提供了更好的模块化支持和类型安全。
在实际项目中,建议:
- 小型项目可以直接使用 Pinia 替代 Vuex
- 大型项目可以逐步迁移,两者可以共存
- 充分利用 Pinia 的 TypeScript 支持提升开发效率
- 合理使用插件系统扩展功能
随着 Vue 3 生态的不断完善,Pinia 将成为现代 Vue 应用状态管理的标准选择。掌握 Pinia 的使用,将为你的 Vue 3 开发之路提供强有力的支持。