Vue 3 与 Pinia 高级状态管理模式
概述
Pinia 是 Vue 3 官方推荐的状态管理库,提供了更现代、更简洁的 API,支持 TypeScript,并且完全兼容 Composition API。与 Vuex 相比,Pinia 具有更少的样板代码、更好的类型支持和更灵活的架构。本集将深入探讨 Pinia 的高级状态管理模式,包括模块化设计、持久化状态、插件系统、性能优化和最佳实践,帮助你构建可扩展、可维护的 Vue 3 应用。
核心知识点
1. Pinia 基本概念
1.1 核心组件
Pinia 包含以下核心组件:
- Store:状态管理的核心,包含 state、getters、actions
- State:应用的状态数据
- Getters:计算属性,用于从 state 派生数据
- Actions:用于修改 state 的方法,可以是异步的
1.2 创建基本 Store
// src/stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Counter'
}),
getters: {
doubleCount: (state) => state.count * 2,
// 使用其他 getters
doubleCountPlusOne: (state, getters) => getters.doubleCount + 1
},
actions: {
increment() {
this.count++
},
// 异步 actions
async fetchData() {
const data = await fetch('https://api.example.com/data')
this.name = (await data.json()).name
}
}
})2. 模块化状态管理
2.1 按功能划分模块
// src/stores/index.js
import { createPinia } from 'pinia'
const pinia = createPinia()
export default pinia
// src/stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
token: null
}),
getters: {
isAuthenticated: (state) => !!state.token
},
actions: {
async login(credentials) {
const response = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify(credentials),
headers: { 'Content-Type': 'application/json' }
})
const data = await response.json()
this.user = data.user
this.token = data.token
},
logout() {
this.user = null
this.token = null
}
}
})
// src/stores/cart.js
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
items: [],
total: 0
}),
getters: {
itemCount: (state) => state.items.length
},
actions: {
addItem(item) {
this.items.push(item)
this.calculateTotal()
},
removeItem(itemId) {
this.items = this.items.filter(item => item.id !== itemId)
this.calculateTotal()
},
calculateTotal() {
this.total = this.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
}
})2.2 使用 setup 语法
// src/stores/product.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useProductStore = defineStore('product', () => {
// state
const products = ref([])
const loading = ref(false)
// getters
const featuredProducts = computed(() => {
return products.value.filter(product => product.featured)
})
const getProductById = computed(() => (id) => {
return products.value.find(product => product.id === id)
})
// actions
async function fetchProducts() {
loading.value = true
try {
const response = await fetch('/api/products')
products.value = await response.json()
} catch (error) {
console.error('Failed to fetch products:', error)
} finally {
loading.value = false
}
}
async function fetchProduct(id) {
loading.value = true
try {
const response = await fetch(`/api/products/${id}`)
const product = await response.json()
const index = products.value.findIndex(p => p.id === id)
if (index !== -1) {
products.value[index] = product
} else {
products.value.push(product)
}
return product
} catch (error) {
console.error(`Failed to fetch product ${id}:`, error)
} finally {
loading.value = false
}
}
return {
products,
loading,
featuredProducts,
getProductById,
fetchProducts,
fetchProduct
}
})3. 状态持久化
3.1 使用插件实现持久化
// src/plugins/piniaPersist.js
import { ref, computed } from 'vue'
export function piniaPersist() {
return {
install(pinia) {
pinia.use(({ store }) => {
// 初始化时从 localStorage 加载数据
const savedState = localStorage.getItem(`pinia-${store.$id}`)
if (savedState) {
store.$patch(JSON.parse(savedState))
}
// 订阅状态变化,保存到 localStorage
store.$subscribe((mutation, state) => {
localStorage.setItem(`pinia-${store.$id}`, JSON.stringify(state))
})
})
}
}
}
// src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import { piniaPersist } from './plugins/piniaPersist'
const app = createApp(App)
const pinia = createPinia()
// 使用持久化插件
pinia.use(piniaPersist())
app.use(pinia)
app.use(router)
app.mount('#app')3.2 使用第三方库
# 安装 pinia-plugin-persistedstate
npm install pinia-plugin-persistedstate// src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'
import router from './router'
const app = createApp(App)
const pinia = createPinia()
// 使用持久化插件
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.use(router)
app.mount('#app')
// src/stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
token: null
}),
getters: {
isAuthenticated: (state) => !!state.token
},
actions: {
// ...
},
persist: true // 启用持久化
})
// 高级配置
export const useCartStore = defineStore('cart', {
state: () => ({
items: [],
total: 0
}),
// ...
persist: {
key: 'custom-cart-key',
storage: sessionStorage, // 使用 sessionStorage 而非默认的 localStorage
paths: ['items'] // 只持久化 items 字段
}
})4. Pinia 插件系统
4.1 自定义插件
// src/plugins/piniaLogger.js
export function piniaLogger() {
return {
install(pinia) {
pinia.use(({ store }) => {
console.log(`[Pinia] Store ${store.$id} initialized`)
// 监听 actions
store.$onAction(({ name, args, after, onError }) => {
console.log(`[Pinia] ${store.$id}.${name} starting with args:`, args)
after((result) => {
console.log(`[Pinia] ${store.$id}.${name} completed with result:`, result)
})
onError((error) => {
console.error(`[Pinia] ${store.$id}.${name} failed with error:`, error)
})
})
// 监听状态变化
store.$subscribe((mutation, state) => {
console.log(`[Pinia] ${store.$id} state changed:`, mutation, state)
})
})
}
}
}
// src/main.js
import { piniaLogger } from './plugins/piniaLogger'
// 使用日志插件
pinia.use(piniaLogger())4.2 插件之间的通信
// src/plugins/piniaPluginA.js
export function piniaPluginA() {
return {
install(pinia) {
pinia.use(({ store }) => {
store.$customPropertyA = 'custom value A'
})
}
}
}
// src/plugins/piniaPluginB.js
export function piniaPluginB() {
return {
install(pinia) {
pinia.use(({ store }) => {
// 可以访问其他插件添加的属性
console.log(store.$customPropertyA) // 'custom value A'
store.$customPropertyB = 'custom value B'
})
}
}
}5. 与 Vue Router 集成
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '../stores/user'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('../views/HomeView.vue')
},
{
path: '/about',
name: 'About',
component: () => import('../views/AboutView.vue')
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('../views/DashboardView.vue'),
meta: { requiresAuth: true }
},
{
path: '/login',
name: 'Login',
component: () => import('../views/LoginView.vue')
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!userStore.isAuthenticated) {
next({ name: 'Login' })
} else {
next()
}
} else {
next()
}
})
export default router6. 调试技巧
6.1 浏览器开发者工具
Pinia 提供了 Vue DevTools 集成,可以在开发者工具中查看和修改状态。
6.2 使用 $patch 批量更新
// 单个更新
store.count++
store.name = 'New Name'
// 批量更新
store.$patch({
count: store.count + 1,
name: 'New Name'
})
// 使用函数批量更新
store.$patch((state) => {
state.items.push({ id: 1, name: 'Product 1', price: 100 })
state.total += 100
})6.3 重置状态
// 重置整个状态
store.$reset()7. 性能优化
7.1 使用 shallowRef
// src/stores/product.js
import { defineStore } from 'pinia'
import { shallowRef, computed } from 'vue'
export const useProductStore = defineStore('product', () => {
// 使用 shallowRef 处理大型数组或对象
const products = shallowRef([])
const loading = shallowRef(false)
// ...
})7.2 按需获取状态
// src/components/ProductList.vue
<script setup lang="ts">
import { useProductStore } from '@/stores/product'
// 使用 storeToRefs 避免不必要的重新渲染
import { storeToRefs } from 'pinia'
const productStore = useProductStore()
const { products, loading } = storeToRefs(productStore)
const { fetchProducts } = productStore
</script>7.3 限制订阅频率
// src/plugins/piniaPersist.js
import { ref, computed } from 'vue'
export function piniaPersist() {
return {
install(pinia) {
pinia.use(({ store }) => {
// 初始化时从 localStorage 加载数据
const savedState = localStorage.getItem(`pinia-${store.$id}`)
if (savedState) {
store.$patch(JSON.parse(savedState))
}
// 使用防抖函数限制保存频率
let timeoutId
store.$subscribe((mutation, state) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
localStorage.setItem(`pinia-${store.$id}`, JSON.stringify(state))
}, 1000) // 1秒防抖
})
})
}
}
}最佳实践
1. 遵循单一职责原则
- 每个 Store 只负责一个功能领域
- 避免在一个 Store 中包含不相关的状态
- 保持 Store 的大小适中,便于维护
2. 使用 TypeScript
// src/stores/user.ts
import { defineStore } from 'pinia'
interface User {
id: string
name: string
email: string
role: 'admin' | 'user'
}
interface UserState {
user: User | null
token: string | null
loading: boolean
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({
user: null,
token: null,
loading: false
}),
getters: {
isAuthenticated: (state): boolean => !!state.token,
isAdmin: (state): boolean => state.user?.role === 'admin'
},
actions: {
async login(credentials: { email: string; password: string }): Promise<void> {
this.loading = true
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify(credentials),
headers: { 'Content-Type': 'application/json' }
})
const data = await response.json()
this.user = data.user
this.token = data.token
} catch (error) {
console.error('Login failed:', error)
} finally {
this.loading = false
}
}
}
})3. 避免直接修改状态
// 不推荐
store.count++
store.items.push(newItem)
// 推荐
store.increment() // 通过 action 修改
store.addItem(newItem) // 通过 action 修改4. 使用组合式 API
对于复杂的状态管理,使用组合式 API 可以更好地组织代码:
// src/stores/complex.js
import { defineStore } from 'pinia'
import { ref, computed, watch } from 'vue'
export const useComplexStore = defineStore('complex', () => {
// 基础状态
const count = ref(0)
const items = ref([])
// 计算属性
const total = computed(() => {
return items.value.reduce((sum, item) => sum + item.value, 0)
})
// 监听变化
watch(count, (newCount) => {
console.log(`Count changed to: ${newCount}`)
})
// 异步 action
async function fetchData() {
const response = await fetch('/api/data')
const data = await response.json()
items.value = data.items
}
return {
count,
items,
total,
fetchData
}
})5. 测试 Store
// src/stores/__tests__/user.test.js
import { describe, it, expect, beforeEach } from 'vitest'
import { createPinia } from 'pinia'
import { useUserStore } from '../user'
describe('User Store', () => {
let userStore
beforeEach(() => {
const pinia = createPinia()
userStore = useUserStore(pinia)
})
it('should initialize with null user and token', () => {
expect(userStore.user).toBeNull()
expect(userStore.token).toBeNull()
expect(userStore.isAuthenticated).toBe(false)
})
it('should login successfully', async () => {
// Mock fetch
global.fetch = vi.fn(() =>
Promise.resolve({
json: () => Promise.resolve({
user: { id: '1', name: 'Test User', email: 'test@example.com' },
token: 'test-token'
})
})
)
await userStore.login({ email: 'test@example.com', password: 'password' })
expect(userStore.user).toEqual({ id: '1', name: 'Test User', email: 'test@example.com' })
expect(userStore.token).toBe('test-token')
expect(userStore.isAuthenticated).toBe(true)
})
it('should logout successfully', () => {
userStore.user = { id: '1', name: 'Test User', email: 'test@example.com' }
userStore.token = 'test-token'
userStore.logout()
expect(userStore.user).toBeNull()
expect(userStore.token).toBeNull()
expect(userStore.isAuthenticated).toBe(false)
})
})常见问题和解决方案
1. 状态不更新
问题:修改状态后,组件没有重新渲染
解决方案:
- 确保使用
storeToRefs来解构状态 - 避免直接修改对象或数组,使用
$patch或创建新的引用 - 检查是否使用了
shallowRef而没有正确处理
2. 持久化不生效
问题:状态没有被正确持久化到 localStorage
解决方案:
- 检查插件是否正确安装
- 检查 localStorage 是否可用
- 确保状态是可序列化的(不包含函数、Symbol 等)
3. 类型错误
问题:TypeScript 类型错误
解决方案:
- 确保为状态和 getters 定义了正确的类型
- 使用
defineStore的泛型参数 - 检查是否正确导入了类型
4. 性能问题
问题:Store 过大导致性能问题
解决方案:
- 拆分 Store 为多个小模块
- 使用
shallowRef处理大型数据 - 避免不必要的状态订阅
- 按需加载 Store
5. 路由守卫中使用 Store
问题:在路由守卫中无法使用 Store
解决方案:
- 确保在创建路由之前初始化 Pinia
- 在路由守卫中使用
useUserStore()而不是通过依赖注入
进阶学习资源
1. 官方文档
2. 工具和库
3. 最佳实践指南
4. 学习案例
实践练习
练习 1:基础 Store 创建
- 创建一个 Vue 3 + TypeScript 项目
- 安装和配置 Pinia
- 创建一个简单的 Counter Store
- 在组件中使用 Store
- 测试状态更新和 getters
练习 2:模块化设计
- 创建用户 Store(user.js)
- 创建购物车 Store(cart.js)
- 创建产品 Store(product.js)
- 在组件中使用多个 Store
- 实现 Store 之间的交互
练习 3:状态持久化
- 使用 pinia-plugin-persistedstate 插件
- 配置用户 Store 的持久化
- 配置购物车 Store 的持久化
- 测试页面刷新后状态是否保留
练习 4:路由集成
- 创建需要认证的页面
- 实现登录和登出功能
- 配置路由守卫
- 测试认证流程
练习 5:性能优化
- 使用
shallowRef处理大型数据 - 使用
storeToRefs避免不必要的重新渲染 - 实现 Store 的按需加载
- 测试性能优化效果
总结
Pinia 是 Vue 3 官方推荐的状态管理库,提供了更现代、更简洁的 API,支持 TypeScript,并且完全兼容 Composition API。通过掌握 Pinia 的高级状态管理模式,包括模块化设计、持久化状态、插件系统、路由集成、调试技巧和性能优化,你可以构建可扩展、可维护的 Vue 3 应用。
下一集我们将学习 Vue 3 与 Vue Router 高级路由模式,敬请期待!