下面,我们来系统的梳理关于 Pinia 状态管理 的基本知识点:
一、Pinia 核心概念
1.1 什么是 Pinia?
Pinia(发音为 /piːnjʌ/
)是 Vue 的官方状态管理库,专为 Vue 3 设计,同时支持 Vue 2。它提供了比 Vuex 更简洁的 API 和更强大的 TypeScript 支持,是 Vue 生态中现代状态管理的首选方案。
1.2 Pinia 的核心优势
特性 | 说明 | 与 Vuex 对比 |
---|---|---|
简洁 API | 移除 mutations,只有 state/getters/actions | 减少样板代码 |
TypeScript 支持 | 一流的类型推断 | 开箱即用,无需额外配置 |
模块化 | 自动命名空间 | 无需手动管理命名空间 |
Composition API | 原生支持 | 更符合 Vue 3 设计哲学 |
体积 | 约 1KB | 比 Vuex 更轻量 |
Devtools 支持 | 完整集成 | 与 Vuex 相同体验 |
1.3 何时使用 Pinia?
- 需要跨组件共享状态
- 需要持久化应用状态
- 管理复杂业务逻辑
- 需要状态的时间旅行调试
- 在 Vue 3 项目中替代 Vuex
二、安装与基础使用
2.1 安装 Pinia
npm install pinia
# 或
yarn add pinia
2.2 创建 Pinia 实例
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.mount('#app')
2.3 创建第一个 Store
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Pinia User'
}),
getters: {
doubleCount: (state) => state.count * 2,
personalizedGreeting: (state) => `Hello, ${state.name}!`
},
actions: {
increment() {
this.count++
},
async incrementAsync() {
await new Promise(resolve => setTimeout(resolve, 1000))
this.increment()
}
}
})
2.4 在组件中使用 Store
<script setup>
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
</script>
<template>
<div>
<h1>{{ counterStore.name }}</h1>
<p>Count: {{ counterStore.count }}</p>
<p>Double: {{ counterStore.doubleCount }}</p>
<button @click="counterStore.increment">Increment</button>
<button @click="counterStore.incrementAsync">Increment Async</button>
</div>
</template>
三、核心概念详解
3.1 State
定义响应式状态
state: () => ({
user: {
id: 1,
name: 'Alice',
email: 'alice@example.com'
},
preferences: {
theme: 'dark',
notifications: true
},
items: []
})
状态操作
// 重置状态
counterStore.$reset()
// 修改状态
counterStore.count = 10
// 批量更新
counterStore.$patch({
count: counterStore.count + 1,
name: 'Bob'
})
// 函数式更新
counterStore.$patch((state) => {
state.items.push({ id: 1, name: 'Item' })
state.user.name = 'Updated'
})
3.2 Getters
计算属性
getters: {
// 基本 getter
itemCount: (state) => state.items.length,
// 使用其他 getter
itemCountMessage() {
return `Total items: ${this.itemCount}`
},
// 带参数的 getter
getItemById: (state) => (id) => {
return state.items.find(item => item.id === id)
}
}
使用
const itemCount = counterStore.itemCount
const item = counterStore.getItemById(1)
3.3 Actions
同步与异步操作
actions: {
// 同步 action
addItem(item) {
this.items.push(item)
},
// 异步 action
async fetchUser(userId) {
try {
const response = await fetch(`/api/users/${userId}`)
this.user = await response.json()
} catch (error) {
console.error('Failed to fetch user:', error)
}
},
// 组合 actions
async fetchAndProcessUser(userId) {
await this.fetchUser(userId)
this.processUserData()
},
processUserData() {
// 处理用户数据
}
}
四、高级特性
4.1 Composition API 风格 Store
export const useProductStore = defineStore('products', () => {
// 状态
const products = ref([])
const loading = ref(false)
const error = ref(null)
// getters
const featuredProducts = computed(() =>
products.value.filter(p => p.isFeatured)
)
// actions
async function fetchProducts() {
try {
loading.value = true
const response = await fetch('/api/products')
products.value = await response.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
return { products, loading, error, featuredProducts, fetchProducts }
})
4.2 Store 之间的交互
// stores/cart.js
export const useCartStore = defineStore('cart', {
actions: {
async checkout() {
const authStore = useAuthStore()
if (!authStore.isAuthenticated) {
throw new Error('User not authenticated')
}
// 结账逻辑...
}
}
})
4.3 状态订阅
// 订阅状态变化
const unsubscribe = cartStore.$subscribe((mutation, state) => {
console.log('State changed:', mutation)
localStorage.setItem('cart', JSON.stringify(state))
})
// 取消订阅
unsubscribe()
// 订阅 actions
cartStore.$onAction(({ name, store, args, after, onError }) => {
console.log(`Action "${name}" started`, args)
after((result) => {
console.log(`Action "${name}" completed`, result)
})
onError((error) => {
console.error(`Action "${name}" failed`, error)
})
})
4.4 插件开发
持久化插件示例
// plugins/persistence.js
export const piniaPersistencePlugin = ({ store }) => {
// 从 localStorage 恢复状态
const savedState = localStorage.getItem(store.$id)
if (savedState) {
store.$patch(JSON.parse(savedState))
}
// 订阅状态变化
store.$subscribe((mutation, state) => {
localStorage.setItem(store.$id, JSON.stringify(state))
})
}
// main.js
import { piniaPersistencePlugin } from '@/plugins/persistence'
const pinia = createPinia()
pinia.use(piniaPersistencePlugin)
4.5 服务端渲染 (SSR)
// server.js
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
export function createApp() {
const app = createSSRApp(App)
const pinia = createPinia()
app.use(pinia)
return { app, pinia }
}
// 在渲染上下文中附加状态
const piniaState = JSON.stringify(pinia.state.value)
// client.js
const pinia = createPinia()
app.use(pinia)
if (window.__PINIA_STATE__) {
pinia.state.value = JSON.parse(window.__PINIA_STATE__)
}
五、TypeScript 集成
5.1 类型化 Store
// stores/user.ts
interface User {
id: number
name: string
email: string
}
interface UserState {
user: User | null
loading: boolean
error: string | null
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({
user: null,
loading: false,
error: null
}),
getters: {
isAuthenticated: (state) => state.user !== null
},
actions: {
async fetchUser(id: number) {
try {
this.loading = true
const response = await fetch(`/api/users/${id}`)
this.user = await response.json()
} catch (err: any) {
this.error = err.message
} finally {
this.loading = false
}
}
}
})
5.2 类型化使用
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 自动类型推断
userStore.user?.name // string | undefined
userStore.isAuthenticated // boolean
userStore.fetchUser(1) // 期望 number 参数
六、项目结构最佳实践
6.1 推荐项目结构
src/
├── stores/
│ ├── modules/
│ │ ├── auth.store.ts
│ │ ├── cart.store.ts
│ │ └── products.store.ts
│ ├── index.ts # 导出所有 store
│ └── types.ts # 共享类型定义
├── components/
├── composables/ # 组合式函数
├── views/
└── App.vue
6.2 模块化 Store 示例
// stores/modules/auth.store.ts
import { defineStore } from 'pinia'
interface AuthState {
user: User | null
token: string | null
}
export const useAuthStore = defineStore('auth', {
state: (): AuthState => ({
user: null,
token: null
}),
getters: {
isAuthenticated: state => state.token !== null
},
actions: {
login(email: string, password: string) {
// 登录逻辑
},
logout() {
this.user = null
this.token = null
}
}
})
6.3 组合式函数封装
// composables/useCartActions.ts
import { useCartStore } from '@/stores/cart'
export function useCartActions() {
const cartStore = useCartStore()
const addToCart = (product: Product) => {
cartStore.addItem(product)
}
const removeFromCart = (productId: number) => {
cartStore.removeItem(productId)
}
const clearCart = () => {
cartStore.clear()
}
return {
addToCart,
removeFromCart,
clearCart
}
}
七、性能优化技巧
7.1 选择性状态解构
// 避免 - 破坏响应性
const { count, name } = counterStore // 失去响应性
// 推荐 - 使用 storeToRefs
import { storeToRefs } from 'pinia'
const counterStore = useCounterStore()
const { count, name } = storeToRefs(counterStore) // 保持响应性
7.2 高效状态更新
// 避免 - 多次更新
items.value.forEach(item => {
cartStore.addItem(item) // 多次触发更新
})
// 推荐 - 批量更新
cartStore.$patch(state => {
items.value.forEach(item => {
state.items.push(item)
})
})
7.3 计算属性优化
// 避免 - 复杂计算在 getter 中
getters: {
expensiveComputation() {
// 复杂计算
return bigArray.value.filter(...).map(...)
}
}
// 推荐 - 使用计算属性缓存
import { computed } from 'vue'
const expensiveResult = computed(() => {
return bigArray.value.filter(...).map(...)
})
八、Pinia 插件生态系统
8.1 官方推荐插件
插件 | 功能 | 安装 |
---|---|---|
pinia-plugin-persistedstate | 状态持久化 | npm i pinia-plugin-persistedstate |
@pinia/nuxt | Nuxt.js 集成 | npm i @pinia/nuxt |
pinia-orm | ORM 风格状态管理 | npm i pinia-orm |
8.2 持久化状态插件使用
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
// 在 store 中启用
export const useAuthStore = defineStore('auth', {
persist: true,
state: () => ({ token: null }),
// ...
})
// 自定义配置
persist: {
key: 'my-auth-store',
storage: sessionStorage,
paths: ['token'] // 只持久化 token
}
九、Pinia 与 Vuex 迁移指南
9.1 迁移步骤
- 安装 Pinia:
npm install pinia
- 创建 Pinia 实例:替换 Vuex 的创建方式
- 重构 Stores:
- 移除 mutations,将逻辑移到 actions
- 保持 getters 不变
- 简化模块结构
- 更新组件:
- 替换
mapState
/mapGetters
为useStore
- 替换
mapActions
为直接调用
- 替换
- 移除 Vuex:逐步替换直到完全移除
9.2 概念映射表
Vuex 概念 | Pinia 等效 | 说明 |
---|---|---|
state | state | 基本一致 |
getters | getters | 基本一致 |
mutations | actions | 在 actions 中直接修改状态 |
actions | actions | 处理异步操作 |
modules | 独立 store | 每个 store 自动命名空间 |
namespaced: true | 自动实现 | 无需配置 |
十、测试策略
10.1 单元测试 Store
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '@/stores/counter'
describe('Counter Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
test('increment action', () => {
const store = useCounterStore()
store.increment()
expect(store.count).toBe(1)
})
test('async increment', async () => {
const store = useCounterStore()
await store.incrementAsync()
expect(store.count).toBe(1)
})
test('doubleCount getter', () => {
const store = useCounterStore()
store.count = 4
expect(store.doubleCount).toBe(8)
})
})
10.2 测试组件中的 Store
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import CounterComponent from '@/components/Counter.vue'
test('component uses store', async () => {
const wrapper = mount(CounterComponent, {
global: {
plugins: [createTestingPinia({
initialState: {
counter: { count: 10 }
},
stubActions: false
})]
}
})
expect(wrapper.text()).toContain('Count: 10')
await wrapper.find('button').trigger('click')
const store = useCounterStore()
expect(store.increment).toHaveBeenCalledTimes(1)
})