很多刚接触 Vue3 状态管理的同学都会问:"Vuex 还没学明白,怎么又出了个 Pinia?"这篇就来聊聊 Pinia,看看它到底是什么、为什么要用它、以及怎么用好它。
- Pinia 是什么:Pinia 是 Vue 的全新状态管理库,它和 Vuex 有哪些核心区别?
- 如何定义 Store:掌握选项式和组合式两种创建 Store 的风格。
- 在组件中如何使用 :学会在
setup中正确地使用 Store 并保持响应性。
一、使用场景
Pinia 适用于需要对 Vue 应用进行全局状态管理的场景:
- 跨组件共享状态:例如用户登录信息、购物车数据、主题配置等
- 与后端 API 交互:集中管理 API 调用和数据缓存逻辑
- 调试与开发体验:配合 Vue DevTools 进行状态追踪和时间旅行
- 类型安全:为 TypeScript 项目提供完整的类型推导支持
- 模块化管理:将不同领域的全局状态拆分为独立的 Store,易于维护和扩展
二、注意事项
划重点: Pinia 在 Vue 2 和 Vue 3 中均可使用,但本文所有示例均基于 Vue 3 + Script Setup 语法。
-
defineStore 的 ID 必须唯一:每个 Store 都需要一个唯一的 ID,用于 DevTools 的连接 - 没有
mutations :与 Vuex 不同,Pinia 将状态(state)与变更逻辑(actions)直接统一,摒弃了繁琐的 mutations - 避免过度使用全局状态:组件内部能处理的数据,不要提升到 Store 中
三、基本用法
3.1 安装与配置
bash
yarn add pinia
# 或
npm install pinia
在 main.ts 中创建 Pinia 实例并注册到 Vue 应用:
typescript
// main.ts
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')
3.2 定义 Store
Pinia 提供两种风格来定义 Store:选项式(Options API) 和 组合式(Composition API) 。选择哪个全凭个人喜好,但组合式风格更适合复杂逻辑的复用。
选项式风格:
typescript
// stores/counter.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// 状态(数据)
state: () => ({
count: 0,
name: '我的计数器',
}),
// 计算属性(派生数据)
getters: {
// 使用箭头函数时,state 为第一个参数
doubleCount: (state) => state.count * 2,
// 需要访问其他 getter 时,使用普通函数
doubleCountPlusOne(): number {
return this.doubleCount + 1
},
},
// 操作方法(业务逻辑)
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
// 支持异步操作
async incrementAsync() {
await new Promise((resolve) => setTimeout(resolve, 1000))
this.increment()
},
incrementBy(amount: number) {
this.count += amount
},
},
})
组合式风格(推荐):
typescript
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { api } from '@/services/api' // 假设的 API 服务
// 类型定义
interface User {
id: number
name: string
email: string
}
interface LoginCredentials {
username: string
password: string
}
export const useUserStore = defineStore('user', () => {
// 状态(类似 options 的 data)
const user = ref<User | null>(null)
const isLoggedIn = ref(false)
// 计算属性(类似 options 的 getters)
const userName = computed(() => user.value?.name || '游客')
const userId = computed(() => user.value?.id || 0)
// 操作方法(类似 options 的 actions)
function login(credentials: LoginCredentials) {
return api.login(credentials).then((response) => {
user.value = response.user
isLoggedIn.value = true
return response
})
}
function logout() {
user.value = null
isLoggedIn.value = false
}
async function fetchUserProfile() {
if (!userId.value) return
try {
const profile = await api.getUserProfile(userId.value)
user.value = { ...user.value, ...profile }
} catch (error) {
console.error('获取用户资料失败:', error)
}
}
// 返回需要暴露给外部的所有内容
return { user, isLoggedIn, userName, userId, login, logout, fetchUserProfile }
})
代码解析:
-
defineStore:这是定义 Store 的核心函数,第一个参数是 Store 的唯一 ID,第二个参数是 Store 的定义(可以是对象或函数)。 -
ref 和 computed :在组合式风格中,我们使用 Vue 的响应式 API 来定义状态和计算属性,和写组件script setup一模一样。 -
return 语句 :组合式 Store 必须return一个对象,对象中的属性和方法才能被外部访问。
3.3 在组件中使用 Store
vue
<template>
<div>
<h1>{{ counterStore.name }}</h1>
<p>计数: {{ counterStore.count }}</p>
<p>双倍计数: {{ counterStore.doubleCount }}</p>
<p>用户名: {{ userStore.userName }}</p>
<button @click="counterStore.increment()">增加</button>
<button @click="counterStore.decrement()">减少</button>
<button @click="counterStore.incrementBy(5)">增加5</button>
<button @click="counterStore.incrementAsync()">异步增加</button>
<button @click="login" v-if="!userStore.isLoggedIn">登录</button>
<button @click="userStore.logout()" v-else>退出</button>
</div>
</template>
<script lang="ts" setup>
import { watch } from 'vue'
import { useCounterStore } from '@/stores/counter'
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
const counterStore = useCounterStore()
const userStore = useUserStore()
// 关键:使用 storeToRefs 保持解构后的响应性
const { count, doubleCount } = storeToRefs(counterStore)
const { userName, isLoggedIn } = storeToRefs(userStore)
// 直接调用 action(action 本身就是方法,无需解构)
const login = () => {
userStore
.login({ username: 'admin', password: 'password' })
.then(() => {
console.log('登录成功')
})
.catch((error) => {
console.error('登录失败:', error)
})
}
// 监听 Store 状态变化
watch(
() => counterStore.count,
(newCount, oldCount) => {
console.log(`计数从 ${oldCount} 变为 ${newCount}`)
}
)
</script>
代码解析:
-
storeToRefs :这是 Pinia 提供的一个工具函数,用于"解包" Store 的 state 和 getters。直接从 Store 解构const { count } = counterStore会失去响应性,而storeToRefs保证了解构后的每个ref依然是响应式的。 - 直接调用
actions :actions是普通函数,可以直接从 Store 对象上调用,不需要解构。counterStore.increment()和userStore.login()都是正确的用法。 -
watch 监听 :你可以像监听普通ref一样监听 Store 中的状态,使用watch(() => counterStore.count, handler)。
最后
刚接触 Pinia,建议先从最典型的状态(比如用户登录信息、全局配置)开始使用,不要一开始就把所有组件数据都放进 Store。写得顺手了,再逐步将那些频繁跨组件传递的 props 和 emit 改为 Store 管理,你会发现代码瞬间清爽许多。