Pinia 状态管理
大白话解释: Pinia 是 Vuex 的"升级版",更简单、更好用。如果说 Vuex 是"绕弯子"(修改数据要经过 Mutation),Pinia 就是"直来直去"(直接修改数据)。
为什么推荐 Pinia 替代 Vuex?
- 没有 Mutation:直接修改 state,不用绕弯子写 mutation
- TypeScript 支持更好:类型推导完美,写代码时有智能提示
- 模块化更简单:每个 Store 天然独立,不用嵌套 modules
- 体积更小:只有 ~1KB,Vuex 有 ~10KB
什么时候用 Pinia?
- Vue 3 新项目(官方推荐)
- 需要多个组件共享状态
- 需要状态持久化(如用户登录信息)
Vue 3 官方推荐的状态管理库,是 Vuex 的继任者。API 更简洁,完美支持 TypeScript,没有 mutations 的概念。
与 Vuex 对比
| 特性 | Pinia | Vuex |
|---|---|---|
| mutations | 无,直接修改 state | 需要 mutations |
| TypeScript | 原生支持,类型推导完美 | 支持较弱 |
| 模块化 | 天然独立,无需 modules | 需要 modules 嵌套 |
| 体积 | ~1KB | ~10KB |
| DevTools | 完整支持 | 完整支持 |
| SSR | 支持 | 支持 |
| 插件 | 支持 | 支持 |
安装与基本配置
bash
yarn add piniats
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia' // 导入 createPinia
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia() // 创建 Pinia 实例
app.use(pinia) // 安装 Pinia 插件
app.mount('#app')创建 Store
Options API 风格
ts
// stores/counter.ts
import { defineStore } from 'pinia' // 导入 defineStore
export const useCounterStore = defineStore('counter', {
// 状态:返回一个对象,包含所有响应式状态
state: () => ({
count: 0, // 计数器
name: '计数器', // 名称
list: [] as string[], // 列表
}),
// 计算属性:基于 state 自动计算
getters: {
doubleCount: (state) => state.count * 2, // 双倍计数
// 使用其他 getter
doubleCountPlusOne(): number {
return this.doubleCount + 1 // 双倍 + 1
},
// 带参数的 getter(返回函数)
findById: (state) => {
return (id: number) => state.list.find((item) => item === String(id))
},
},
// 方法(同步 + 异步):修改 state 的唯一方式
actions: {
increment() {
this.count++ // 直接修改 state
},
decrement() {
this.count--
},
reset() {
this.count = 0
},
async fetchCount() {
try {
const res = await api.getCount() // 异步请求
this.count = res.data // 更新 state
} catch (err) {
console.error('获取计数失败', err)
}
},
},
})Composition API 风格(推荐)
ts
// stores/user.ts
import { ref, computed } from 'vue' // 导入 ref 和 computed
import { defineStore } from 'pinia' // 导入 defineStore
export const useUserStore = defineStore('user', () => {
// ---- state:用 ref 定义响应式状态 ----
const name = ref('') // 用户名
const token = ref('') // 登录 token
const roles = ref<string[]>([]) // 角色列表
const permissions = ref<string[]>([]) // 权限列表
// ---- getters:用 computed 定义计算属性 ----
const isLoggedIn = computed(() => !!token.value) // 是否已登录
const isAdmin = computed(() => roles.value.includes('admin')) // 是否管理员
const hasPermission = (perm: string) => permissions.value.includes(perm) // 检查权限
// ---- actions:普通函数,直接修改 state ----
async function login(credentials: { username: string; password: string }) {
const res = await api.login(credentials) // 调用登录 API
token.value = res.data.token // 保存 token
name.value = res.data.name // 保存用户名
roles.value = res.data.roles // 保存角色
permissions.value = res.data.permissions // 保存权限
return res.data // 返回数据供组件使用
}
async function getUserInfo() {
if (!token.value) return // 没有 token,跳过
try {
const res = await api.getUserInfo() // 获取用户信息
name.value = res.data.name
roles.value = res.data.roles
permissions.value = res.data.permissions
} catch (err) {
logout() // token 失效,退出登录
}
}
function logout() {
token.value = '' // 清空 token
name.value = '' // 清空用户名
roles.value = [] // 清空角色
permissions.value = [] // 清空权限
}
// 返回所有需要暴露的状态和方法
return {
name,
token,
roles,
permissions,
isLoggedIn,
isAdmin,
hasPermission,
login,
getUserInfo,
logout,
}
})在组件中使用
vue
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter' // 导入 counter store
import { useUserStore } from '@/stores/user' // 导入 user store
import { storeToRefs } from 'pinia' // 导入 storeToRefs
const counterStore = useCounterStore() // 获取 counter store 实例
const userStore = useUserStore() // 获取 user store 实例
// ✅ state 和 getters 用 storeToRefs 解构(保持响应性)
const { count, doubleCount } = storeToRefs(counterStore)
const { name, isLoggedIn, isAdmin } = storeToRefs(userStore)
// ✅ actions 可以直接解构(不需要 storeToRefs)
const { increment, decrement, reset } = counterStore
const { login, logout } = userStore
</script>
<template>
<div>
<p>count: {{ count }}, double: {{ doubleCount }}</p>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
<button @click="reset">重置</button>
<div v-if="isLoggedIn">
<p>欢迎,{{ name }}</p>
<button @click="logout">退出</button>
</div>
</div>
</template>⚠️ 直接解构
counterStore.count会丢失响应性,必须用storeToRefs。
Store 之间互相调用
ts
// stores/order.ts
import { defineStore } from 'pinia' // 导入 defineStore
import { useUserStore } from './user' // 导入 user store
export const useOrderStore = defineStore('order', () => {
const userStore = useUserStore() // 获取 user store 实例
const list = ref<any[]>([]) // 订单列表,需要类型标注
async function createOrder(product: any) {
// 使用其他 store 检查登录状态
if (!userStore.isLoggedIn) {
throw new Error('请先登录')
}
const res = await api.createOrder({
userId: userStore.name, // 使用 user store 的数据
product,
})
list.value.push(res.data) // 添加到订单列表
return res.data
}
async function fetchOrders() {
const res = await api.getOrders() // 获取订单列表
list.value = res.data // 更新列表
}
return { list, createOrder, fetchOrders }
})常用 API 详解
$reset —— 重置为初始值
ts
const store = useCounterStore()
store.count = 100 // 修改状态
store.name = '自定义' // 修改名称
store.$reset() // 恢复为 state() 的初始值⚠️ Composition API 风格的 store 没有
$reset,需要手动实现:
ts
export const useUserStore = defineStore('user', () => {
const name = ref('') // 用户名
const token = ref('') // token
// 手动实现 $reset 方法
function $reset() {
name.value = '' // 重置用户名
token.value = '' // 重置 token
}
return { name, token, $reset }
})$patch —— 批量更新
ts
const store = useCounterStore()
// 对象形式:一次性更新多个字段
store.$patch({ count: 10, name: '新名字' })
// 函数形式(适合复杂更新,如数组操作)
store.$patch((state) => {
state.count += 10 // 修改计数
state.name = '更新' // 修改名称
state.list.push('new item') // 添加数组项
})$subscribe —— 监听状态变化
ts
const store = useCounterStore()
const unsubscribe = store.$subscribe(
(mutation, state) => {
// mutation.type: 'direct' | 'patch object' | 'patch function'
// mutation.events: 具体的变更事件
console.log('状态变化:', mutation.type, mutation.events)
console.log('新状态:', state)
},
{ detached: true } // detached: true 表示组件卸载后继续监听(默认 false,组件卸载自动取消)
)
// 取消监听
unsubscribe()$onAction —— 监听 action 调用
ts
const store = useCounterStore()
// 监听所有 action 的调用
store.$onAction(({ name, args, after, onError }) => {
console.log(`action "${name}" 被调用,参数:`, args) // 记录调用
after((result) => {
console.log(`action "${name}" 完成,结果:`, result) // action 成功
})
onError((error) => {
console.error(`action "${name}" 失败:`, error) // action 失败
})
})$state —— 替换整个状态
ts
const store = useCounterStore()
// 替换整个 state(谨慎使用,会丢失响应性引用)
store.$state = { count: 0, name: '初始', list: [] }
// 或获取当前 state 的快照
const currentState = store.$state$dispose —— 销毁 store 实例
ts
const store = useCounterStore()
// 手动销毁,清除所有订阅
store.$dispose()
// 销毁后 store 不再响应式,也不能再使用
// 如需重新使用,需要重新调用 useCounterStore()💡
$dispose后 store 会从 Pinia 实例中移除。如果再次调用useCounterStore(),会创建一个全新的实例。
插件系统
基本插件
ts
// plugins/logger.ts
import type { PiniaPluginContext } from 'pinia' // 导入插件上下文类型
// 日志插件:记录所有状态变化和 action 调用
export function loggerPlugin({ store }: PiniaPluginContext) {
// 监听状态变化
store.$subscribe((mutation, state) => {
console.log(`[Pinia] ${store.$id}`, mutation.type, state)
})
// 监听 action 调用
store.$onAction(({ name, args }) => {
console.log(`[Pinia] ${store.$id}.${name}()`, args)
})
}ts
// main.ts
import { createPinia } from 'pinia'
import { loggerPlugin } from './plugins/logger' // 导入日志插件
const pinia = createPinia()
pinia.use(loggerPlugin) // 注册插件
app.use(pinia)持久化插件
ts
// plugins/persist.ts
import type { PiniaPluginContext } from 'pinia' // 导入插件上下文类型
// 持久化插件:自动将状态保存到 localStorage
export function persistPlugin({ store }: PiniaPluginContext) {
// 从 localStorage 恢复状态
const key = `pinia-${store.$id}` // 存储 key
const saved = localStorage.getItem(key)
if (saved) {
try {
store.$patch(JSON.parse(saved)) // 恢复状态
} catch (e) {
console.error('恢复状态失败', e)
}
}
// 监听变化并自动保存
store.$subscribe((mutation, state) => {
localStorage.setItem(key, JSON.stringify(state)) // 保存到 localStorage
})
}bash
# 或使用社区插件
yarn add pinia-plugin-persistedstatets
// main.ts
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' // 导入持久化插件
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate) // 注册持久化插件ts
// stores/user.ts
export const useUserStore = defineStore('user', () => {
const token = ref('') // token
const name = ref('') // 用户名
return { token, name }
}, {
persist: {
key: 'user-store', // 存储 key
storage: localStorage, // 存储方式
paths: ['token'], // 只持久化 token,不持久化 name
},
})给 store 添加全局属性
ts
// plugins/api.ts
import type { PiniaPluginContext } from 'pinia'
// 扩展 PiniaStore 类型,避免 TS 报错
declare module 'pinia' {
export interface PiniaCustomProperties {
$api: {
get: (url: string) => Promise<any>
post: (url: string, data: any) => Promise<any>
}
}
}
export function apiPlugin({ store }: PiniaPluginContext) {
// 给每个 store 注入 $api
store.$api = {
get: (url: string) => fetch(url).then((r) => r.json()),
post: (url: string, data: any) =>
fetch(url, { method: 'POST', body: JSON.stringify(data) }).then((r) => r.json()),
}
}ts
// 使用
const store = useUserStore()
const data = await store.$api.get('/api/user')在组件外使用 store
ts
// router/guards.ts
import { useUserStore } from '@/stores/user'
export function setupRouterGuards(router: any) {
router.beforeEach((to: any) => {
// ✅ 在 router guard 中使用 store(必须在 pinia 安装后调用)
const userStore = useUserStore()
if (to.meta.requiresAuth && !userStore.isLoggedIn) {
return { name: 'Login' }
}
})
}⚠️ 在组件外使用 store 时,必须确保
createPinia()已经app.use(pinia)。
测试 Store
ts
// stores/__tests__/counter.test.ts
import { describe, it, expect } from 'vitest' // 导入测试工具
import { setActivePinia, createPinia } from 'pinia' // 导入 Pinia 工具
import { useCounterStore } from '../counter' // 导入要测试的 store
describe('Counter Store', () => {
beforeEach(() => {
setActivePinia(createPinia()) // 每次测试重置 Pinia,避免状态污染
})
it('初始值为 0', () => {
const store = useCounterStore() // 创建 store 实例
expect(store.count).toBe(0) // 验证初始值
})
it('increment 正常工作', () => {
const store = useCounterStore()
store.increment() // 调用 action
expect(store.count).toBe(1) // 验证结果
})
it('doubleCount 正确计算', () => {
const store = useCounterStore()
store.count = 5 // 直接修改 state
expect(store.doubleCount).toBe(10) // 验证 getter
})
})常见坑点
1. storeToRefs 只用于 state 和 getters
ts
// ✅ 正确
const { count, doubleCount } = storeToRefs(store) // state + getters
const { increment } = store // actions 直接解构
// ❌ 错误 —— actions 不需要 storeToRefs
const { increment } = storeToRefs(store)2. 组件外使用 store 忘记初始化
ts
// ❌ 在 pinia 安装前调用
const store = useUserStore() // 报错:getActivePinia was called with no active Pinia
// ✅ 确保在 app.use(pinia) 之后调用3. 直接替换 reactive 对象
ts
// ❌ Composition API 风格中
const state = reactive({ count: 0 })
state = reactive({ count: 1 }) // 丢失引用
// ✅ 修改属性
state.count = 1
// ✅ 或用 ref
const state = ref({ count: 0 })
state.value = { count: 1 }