📦 Vue3 状态管理方案:Pinia 全指南
Pinia 是 Vue 官方推荐的下一代状态管理库,完全替代 Vuex,支持 Vue3 Composition API、TypeScript 友好、轻量灵活。本文从基础使用、核心 API、高级技巧、注意事项、常见坑五个维度全面讲解。
一、快速上手:安装与基础使用
1. 安装方式
(1)Vite/Vue CLI 项目
bash
# npm
npm install pinia
# yarn
yarn add pinia
# pnpm
pnpm add pinia
(2)CDN 引入
html
<script src="https://unpkg.com/pinia@2.1.7/dist/pinia.iife.js"></script>
<script>
const { createPinia } = Pinia
</script>
2. 初始化 Pinia
在 main.ts 中注册 Pinia:
typescript
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia) // 注册 Pinia
app.mount('#app')
3. 创建第一个 Store
Pinia 中没有 modules,每个 Store 都是独立的,通过 defineStore 定义:
(1)选项式写法(类似 Vuex)
typescript
// src/stores/counter.ts
import { defineStore } from 'pinia'
// 第一个参数是 Store 的唯一 ID(必须全局唯一)
export const useCounterStore = defineStore('counter', {
// 状态:返回初始状态的函数
state: () => ({
count: 0,
name: 'Pinia'
}),
// 计算属性:类似 Vue 的 computed,自动缓存
getters: {
doubleCount: (state) => state.count * 2,
// 访问其他 getters 用 this(需指定返回值类型)
doubleCountPlusOne(): number {
return this.doubleCount + 1
}
},
// 动作:支持同步/异步,修改状态的唯一推荐方式
actions: {
increment() {
this.count++
},
async fetchData() {
const res = await fetch('/api/data')
const data = await res.json()
this.name = data.name
}
}
})
(2)组合式写法(Vue3 Composition API)
更灵活,适合复杂场景:
typescript
// src/stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// 状态:用 ref/reactive 定义
const userInfo = ref({ name: '', age: 0 })
const token = ref('')
// 计算属性:用 computed 替代 getters
const isAdult = computed(() => userInfo.value.age >= 18)
// 动作:普通函数,支持同步/异步
const updateUser = (info: { name: string; age: number }) => {
userInfo.value = info
}
const login = async (username: string, password: string) => {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password })
})
const data = await res.json()
token.value = data.token
}
// 返回需要暴露的属性和方法
return { userInfo, token, isAdult, updateUser, login }
})
二、核心 API 详解
1. Store 实例属性
| API | 作用 | 示例 |
|---|---|---|
$state |
获取/替换整个状态对象 | store.$state = { count: 10 } |
$patch |
批量修改状态(推荐) | store.$patch({ count: store.count + 1 }) 或函数式:store.$patch(state => state.count++) |
$reset |
重置状态到初始值(仅选项式写法支持,组合式需手动实现) | store.$reset() |
$subscribe |
监听状态变化 | store.$subscribe((mutation, state) => console.log('状态变化', mutation)) |
$onAction |
监听 actions 调用 | store.$onAction(({ name, args, after, onError }) => {}) |
2. Getters 特性
-
缓存机制:只有依赖的状态变化时才重新计算;
-
访问其他 Store :在 getters 中可以调用其他 Store 实例;
typescriptimport { useCounterStore } from './counter' getters: { crossStoreCount(state) { const counterStore = useCounterStore() return state.count + counterStore.count } } -
传递参数 :getters 返回函数实现参数传递(此时不会缓存结果);
typescriptgetters: { getCountByMultiplier: (state) => (multiplier: number) => state.count * multiplier } // 组件中调用:store.getCountByMultiplier(3)
3. Actions 特性
- 支持异步 :直接在 actions 中使用
async/await; - 修改状态 :可直接修改
this.count或用$patch; - 访问其他 Store:同 getters,直接在 actions 中调用其他 Store;
- 错误处理 :可通过
try/catch或$onAction监听错误。
三、组件中使用 Store
1. Setup 语法糖(推荐)
vue
<template>
<div>{{ count }}</div>
<button @click="increment">+1</button>
</template>
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
// 获取 Store 实例
const counterStore = useCounterStore()
// 直接访问状态/动作
const increment = () => counterStore.increment()
// 解构状态时必须用 storeToRefs 保持响应式!
const { count, doubleCount } = storeToRefs(counterStore)
</script>
2. 选项式 API 兼容
适合老项目迁移:
vue
<script lang="ts">
import { useCounterStore } from '@/stores/counter'
import { mapState, mapActions, mapGetters } from 'pinia'
export default {
computed: {
...mapState(useCounterStore, ['count', 'name']),
...mapGetters(useCounterStore, ['doubleCount'])
},
methods: {
...mapActions(useCounterStore, ['increment'])
},
mounted() {
this.increment()
}
}
</script>
四、高级技巧
1. Store 持久化(插件)
使用 pinia-plugin-persistedstate 实现状态持久化到 localStorage/sessionStorage:
安装
bash
npm install pinia-plugin-persistedstate
配置
typescript
// main.ts
import { createPinia } from 'pinia'
import persistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(persistedstate)
在 Store 中启用
typescript
// 选项式写法
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
persist: true // 全局默认配置(localStorage)
// 自定义配置
// persist: {
// key: 'counter-store',
// storage: sessionStorage,
// paths: ['count'] // 仅持久化count字段
// }
})
// 组合式写法
export const useUserStore = defineStore('user', () => {
const userInfo = ref({})
return { userInfo }
}, { persist: true })
2. Store 模块化拆分
按业务模块拆分 Store,如 user.ts、cart.ts、setting.ts,通过目录结构管理:
src/stores/
├── index.ts # 导出所有Store
├── user.ts # 用户模块
├── cart.ts # 购物车模块
└── setting.ts # 设置模块
index.ts 统一导出:
typescript
export * from './user'
export * from './cart'
export * from './setting'
3. Pinia 插件开发
自定义插件扩展 Store 功能,比如添加日志、全局状态监听:
typescript
// src/plugins/pinia-logger.ts
export const piniaLogger = ({ store }) => {
store.$subscribe((mutation, state) => {
console.log(`[Pinia Logger] ${mutation.storeId} 状态变化:`, mutation.payload, state)
})
store.$onAction(({ name, args, after, onError }) => {
console.log(`[Pinia Logger] 调用动作: ${name}, 参数:`, args)
after((result) => {
console.log(`[Pinia Logger] 动作完成: ${name}, 返回值:`, result)
})
onError((error) => {
console.error(`[Pinia Logger] 动作失败: ${name}, 错误:`, error)
})
})
}
// main.ts 注册插件
pinia.use(piniaLogger)
五、注意事项
1. 响应式处理
- 禁止直接解构 Store :直接解构会丢失响应式,必须用
storeToRefs; - 修改状态的推荐方式 :优先用
actions,其次用$patch,不推荐直接修改store.count = 10(虽然语法允许,但不利于调试和维护); - 组合式写法中的响应式 :用
ref/reactive定义状态,确保响应式。
2. TypeScript 支持
-
状态类型自动推断 :选项式写法中 Pinia 会自动推断
state的类型; -
组合式写法需手动定义类型 :复杂对象建议用接口定义:
typescriptinterface UserInfo { name: string; age: number; } const userInfo = ref<UserInfo>({ name: '', age: 0 }) -
Actions 参数类型:明确参数类型,避免隐式 any。
3. 性能优化
- Getters 缓存:合理利用 getters 的缓存特性,避免重复计算;
- 批量修改状态 :用
$patch批量修改,减少响应式更新次数; - 避免在 actions 中执行大量同步计算:复杂计算建议放到 getters 或单独的工具函数中。
六、开发中常见的坑
1. 解构 Store 丢失响应式
❌ 错误写法:
typescript
const { count } = useCounterStore() // count 是普通值,不会响应式更新
✅ 正确写法:
typescript
import { storeToRefs } from 'pinia'
const { count } = storeToRefs(useCounterStore()) // count 是 ref,保持响应式
2. 组合式写法中无法使用 $reset
选项式写法的 $reset 是自动生成的,组合式写法需手动实现:
typescript
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
// 手动实现重置函数
const reset = () => {
count.value = 0
}
return { count, reset }
})
3. 持久化插件不生效
-
确保插件已正确注册到 Pinia;
-
组合式写法中,必须在
defineStore的第三个参数中配置persist: true; -
如果状态包含复杂对象(如 Date、Map),需配置
serializer自定义序列化:typescriptpersist: { serializer: { serialize: (value) => JSON.stringify(value, (key, val) => { if (val instanceof Date) return val.toISOString() return val }), deserialize: (value) => JSON.parse(value, (key, val) => { if (key === 'createTime') return new Date(val) return val }) } }
4. 在组件外使用 Store 实例
❌ 错误写法(组件外直接调用 Store,可能导致 Pinia 未初始化):
typescript
// src/utils/api.ts
import { useUserStore } from '@/stores/user'
const userStore = useUserStore() // 组件外调用,可能报错
async function fetchData() {
const token = userStore.token
}
✅ 正确写法(在函数内部调用 Store):
typescript
async function fetchData() {
const userStore = useUserStore() // 函数内部调用,确保 Pinia 已初始化
const token = userStore.token
}
5. Getters 中使用 this 丢失类型
选项式写法中,getters 用箭头函数会丢失 this 类型,需用普通函数:
❌ 错误写法:
typescript
getters: {
doubleCount: () => this.count * 2 // this 类型为 any
}
✅ 正确写法:
typescript
getters: {
doubleCount(): number { // 必须指定返回值类型
return this.count * 2
}
}
七、Pinia vs Vuex 对比
| 特性 | Pinia | Vuex |
|---|---|---|
| 模块化 | 每个 Store 都是独立模块,无嵌套 | 需嵌套 modules,结构复杂 |
| Mutations | 无,直接在 actions 中修改状态 | 必须通过 mutations 修改状态 |
| TypeScript | 天生支持,自动推断类型 | 需手动定义大量类型,繁琐 |
| 体积 | 轻量(约 1KB) | 相对笨重 |
| DevTools | 支持 Vue3 DevTools,调试更友好 | 对 Vue3 支持有限 |
Pinia 完全替代 Vuex,是 Vue3 状态管理的首选方案!