Vue 3 状态管理新选择:Pinia 完全指南
Orion K Lv6

随着 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
// Vuex 4.x 写法
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
INCREMENT(state) {
state.count++
}
},
actions: {
increment({ commit }) {
commit('INCREMENT')
}
}
})

// Pinia 写法
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
// 用户信息 store
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
}
}
})

// 购物车 store
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: {
// 基础 getter
productCount: (state) => state.products.length,

// 依赖其他 getter
hasProducts: (state) => state.products.length > 0,

// 返回函数的 getter(支持参数)
getProductById: (state) => {
return (productId) => state.products.find(product => product.id === productId)
},

// 访问其他 store 的 getter
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', () => {
// state
const count = ref(0)
const name = ref('Eduardo')

// getters
const doubleCount = computed(() => count.value * 2)

// actions
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
// 监听整个 store 的变化
productStore.$subscribe((mutation, state) => {
console.log('Store 发生变化:', mutation.type)
console.log('新状态:', state)

// 持久化到本地存储
localStorage.setItem('productStore', JSON.stringify(state))
})

// 监听 actions
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
// plugins/persistence.js
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))
})
}
}

// main.js
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
// tests/stores/product.test.js
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
// Store 命名:use + 功能名 + Store
export const useUserStore = defineStore('user', { /* ... */ })
export const useProductStore = defineStore('product', { /* ... */ })
export const useShoppingCartStore = defineStore('shoppingCart', { /* ... */ })

// Action 命名:动词 + 名词
actions: {
fetchProducts,
createProduct,
updateProduct,
deleteProduct
}

// Getter 命名:形容词或 get + 名词
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
// ❌ 不推荐:所有状态放在一个 store
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 开发之路提供强有力的支持。

本站由 提供部署服务