Pinia 状态管理:Vue 3 的官方存储库
Pinia 状态管理完全指南
什么是 Pinia?
Pinia 是 Vue 生态系统中新一代的状态管理库,被官方指定为 Vue 3 的默认状态管理方案。它重新思考了 Vuex 的设计理念,以更简洁的 API、完整的 TypeScript 支持和更灵活的使用方式,让状态管理变得前所未有的轻松。
与 Vuex 相比,Pinia 拥有以下核心优势:
- 极致简洁:移除了 mutations,直接通过 actions 修改状态。
- 完整的 TypeScript 推断:无需自定义复杂的包装类型,享受开箱即用的类型安全。
- 扁平化架构:不再有模块嵌套,每个 store 都是独立的。
- 真正的代码分割:stores 可以在需要时自动注册。
- Devtools 支持:无缝集成 Vue Devtools,支持时间旅行调试。
快速上手
安装 Pinia
在已有的 Vue 3 项目中,使用你喜欢的包管理器安装 Pinia:
npm install pinia
# 或者
yarn add pinia
在 Vue 应用中注册
打开入口文件(通常是 main.js 或 main.ts),创建 Pinia 实例并传递给应用:
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')
至此,Pinia 就已经准备就绪,你可以在任何组件中使用它了。
创建你的第一个 Store
Store 是 Pinia 的核心概念,它就像一块独立的数据孤岛,承载着状态和业务逻辑。使用 defineStore() 函数来定义,第一个参数是 store 的唯一 ID(名称)。
定义一个基础 Store
我们创建一个管理用户信息的 store,文件命名为 stores/user.js:
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
name: '张三',
age: 25,
isLoggedIn: false
}),
getters: {
// 类似于计算属性,自动追踪依赖
greeting: (state) => `你好,我是 ${state.name},今年 ${state.age} 岁`,
// 使用 this 访问其他 getter
adultLabel: function () {
return this.age >= 18 ? '成年' : '未成年'
}
},
actions: {
// 可以同步或异步修改状态
login(name) {
this.name = name
this.isLoggedIn = true
},
updateAge(age) {
this.age = age
},
async fetchUserData(id) {
try {
const response = await fetch(`/api/users/${id}`)
const data = await response.json()
this.name = data.name
this.age = data.age
// 直接通过 this 访问 state 和 actions
} catch (error) {
console.error('获取用户数据失败', error)
}
}
}
})
在组件中使用
在任意 Vue 组件中,调用 use 函数返回 store 实例,就可以愉快地使用了:
<template>
<div>
<p>{{ userStore.greeting }}</p>
<p v-if="userStore.isLoggedIn">已登录:{{ userStore.name }}</p>
<button @click="handleLogin">登录</button>
</div>
</template>
<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
function handleLogin() {
userStore.login('李四')
}
</script>
状态 (State)
State 是 store 的响应式数据中心,使用箭头函数返回对象的方式定义,以确保每个 store 实例相互独立。
读取和写入状态
- 直接通过 store 实例访问:
userStore.name - 修改状态可以直接赋值:
userStore.name = '王五' - 一次性修改多个属性,推荐使用
$patch方法:
userStore.$patch({
name: '赵六',
age: 30,
isLoggedIn: true
})
- 或者传递函数版本的
$patch,进行复杂变更:
userStore.$patch((state) => {
state.name = '孙七'
state.age += 1
// 甚至可以执行逻辑
})
重置状态
Pinia 提供了 $reset() 方法,一键将 state 恢复到初始定义时的值:
userStore.$reset()
替换整个状态
通过 $state 属性,可以用新对象完全替换当前状态(通常用于 SSR 场景同步状态):
userStore.$state = { name: '新用户', age: 0, isLoggedIn: false }
派生数据 (Getters)
Getters 完全等同于 Vue 组件中的计算属性,用来基于 state 派生出新的数据。
定义与使用
Getters 通过 state 参数访问状态,也支持通过 this 访问整个 store 实例(但注意不要使用箭头函数,否则会丢失正确的 this 上下文)。
getters: {
// 接收 state 参数
fullInfo(state) {
return `姓名: ${state.name}, 年龄: ${state.age}`
},
// 使用 this 访问其他 getter
welcomeMessage() {
return this.isLoggedIn ? `欢迎回来,${this.name}` : '请先登录'
}
}
在组件或 actions 中,就像访问普通属性一样使用 getter:
console.log(userStore.fullInfo)
向 Getters 传递参数
Getters 本身不接受额外参数,但可以通过返回一个函数来实现:这样在访问时就可以传入参数了。
getters: {
getUserById: (state) => {
return (userId) => state.users.find(user => user.id === userId)
}
}
// 使用
const user = userStore.getUserById(1)
动作 (Actions)
Actions 是 store 中定义方法的地方,它们可以直接通过 this 访问整个 store 实例,并可以包含任何同步或异步操作。不再有 mutations,actions 就是修改状态的唯一入口。
同步 Action
actions: {
increment() {
this.count++
},
changeName(newName) {
this.name = newName
}
}
异步 Action
Actions 可以是异步的,使用 async/await 或返回 Promise。这对处理 API 调用非常方便。
actions: {
async login(credentials) {
this.loading = true
try {
const response = await authApi.login(credentials)
this.user = response.data
this.isLoggedIn = true
} catch (error) {
this.errorMessage = error.message
} finally {
this.loading = false
}
}
}
调用其他 Store 的 Action
在 Action 中,我们可以通过获取其他 store 实例来协作:
import { useCartStore } from './cart'
actions: {
async purchase() {
const cartStore = useCartStore()
if (cartStore.items.length === 0) return
await api.placeOrder(this.orderInfo)
cartStore.clearCart()
}
}
组合式 Store (Setup Stores)
Pinia 提供了第二种定义 store 的方式——使用类似 Vue 组合式 API 的语法。这种方式更加灵活,可以利用 Vue 的 ref、reactive、computed 甚至其他组合式函数。
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
// state
const count = ref(0)
const name = ref('计数器')
// getters 使用 computed
const doubleCount = computed(() => count.value * 2)
// actions 就是普通函数
function increment() {
count.value++
}
function reset() {
count.value = 0
}
// 暴露出去的属性和方法
return { count, name, doubleCount, increment, reset }
})
选项式 Store vs 组合式 Store:
- 选项式 Store 更接近 Vuex 的习惯,结构清晰,适合团队协作。
- 组合式 Store 更加灵活,可以充分复用组合式函数,适合喜欢 Composition API 的开发者。
- 两者在功能和性能上完全等价,你可以根据喜好混用。
在组件外部使用 Store
Pinia store 并不局限于组件,在任何 JavaScript 代码中都可以调用。这得益于 Pinia 的 “active pinia” 机制。只要确保在调用 useStore() 时 Pinia 已经被安装即可。
// 在路由守卫中
import { createRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
const router = createRouter({ ... })
router.beforeEach((to) => {
const userStore = useUserStore()
if (!userStore.isLoggedIn && to.meta.requiresAuth) {
return '/login'
}
})
如果你需要在 Pinia 安装之前就获取 store(例如在 setup 外的初始化代码中),则必须手动传入 pinia 实例:
import { createPinia } from 'pinia'
import { useUserStore } from '@/stores/user'
const pinia = createPinia()
const userStore = useUserStore(pinia) // 明确传入 pinia 实例
常用辅助函数与插件
storeToRefs()
当你需要从 store 中解构 state 和 getters 时,如果直接使用 ES6 解构会丢失响应性。storeToRefs() 可以安全地将它们转化为响应式引用。
<script setup>
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const { name, age, isLoggedIn, greeting } = storeToRefs(userStore)
// name 现在是一个 ref,可以在模板中直接使用 {{ name }}
// 但 actions 不能解构,需保留 userStore.login
</script>
$subscribe()
监听 store 的变化,类似于 Vue 的 watch。会在每次 state 被修改后触发。
userStore.$subscribe((mutation, state) => {
// mutation.type: 'direct' | 'patch object' | 'patch function'
console.log('变化类型:', mutation.type)
console.log('新状态:', state)
// 存储到 localStorage 等
localStorage.setItem('user', JSON.stringify(state))
}, { detached: false }) // detached: true 可以在组件销毁后依然存活
$onAction()
监听 action 的调用和结果,支持在 action 执行前、后或出错时执行回调。非常适合全局的日志记录、性能监控等。
userStore.$onAction(({
name, // action 名称
store, // store 实例
args, // 调用参数数组
after, // 完成后的回调
onError, // 错误时的回调
}) => {
const startTime = Date.now()
console.log(`Action "${name}" 开始执行,参数:`, args)
after((result) => {
console.log(`Action "${name}" 执行成功,耗时: ${Date.now() - startTime}ms`)
})
onError((error) => {
console.error(`Action "${name}" 执行失败`, error)
})
})
使用插件扩展 Pinia
Pinia 的插件机制允许你为每个 store 添加全局属性或功能。比如添加一个通用的 $log 方法,或者持久化插件。
import { createPinia } from 'pinia'
const pinia = createPinia()
// 自定义插件
pinia.use(({ store }) => {
// 为所有 store 添加一个调试属性
store.$state.debug = false
// 添加一个全局方法
store.$log = (message) => {
console.log(`[${store.$id}]`, message)
}
})
社区中流行的持久化插件 pinia-plugin-persistedstate 可以轻松实现状态持久化到 localStorage 或 sessionStorage。
实际项目最佳实践
- 按功能拆分 stores:不要创建一个大而全的 store,为每个功能模块创建独立的 store,如
useUserStore、useCartStore、useOrderStore。 - 保持 action 的纯粹:action 应该包含业务逻辑,而不要直接操作 UI。更改 UI 状态应通过修改 state,由组件响应。
- 合理使用 getter 而非重复计算:避免在多个组件中编写相同的派生逻辑,集中到 getter 中。
- 类型安全:如果你使用 TypeScript,Pinia 会自动推断所有类型,几乎不用额外编写接口。对于复杂 state,辅助类型推断可以使用
interface定义状态结构。 - 使用组合式 Store 时注意一致性:在团队中统一风格,避免选项式和组合式混用导致维护困难。
小结
Pinia 以极简的 API、天生对 TypeScript 的友好、灵活的架构,彻底改变了 Vue 状态管理的体验。无论是小型项目还是大型企业应用,Pinia 都能提供优雅且高效的状态管理方案。现在就开始在项目中使用 Pinia,告别繁琐的样板代码,享受清晰直观的数据流吧。