Pinia 是 Vue.js 的官方状态管理库,专为 Vue 3 设计,同时支持 Vue 2。它提供了直观、类型安全、模块化的状态管理方案,是 Vuex 的现代化替代品。
Pinia 是一个轻量级、易用的状态管理库,具有以下特点:
相比 Vuex,API 更简洁直观,学习成本低
完善的 TypeScript 支持,类型推断准确
天然支持模块化,每个 Store 都是独立的
与 Vue DevTools 完美集成,调试方便
# 使用 npm
npm install pinia
# 使用 yarn
yarn add pinia
# 使用 pnpm
pnpm add pinia
// main.js / main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
// 创建 Pinia 实例
const pinia = createPinia()
// 创建 Vue 应用
const app = createApp(App)
// 使用 Pinia
app.use(pinia)
app.mount('#app')
Pinia 的核心概念非常简单,主要由三个部分组成:
| 概念 | 对应 Vuex | 说明 | 特点 |
|---|---|---|---|
| State | State | 存储应用的状态数据 | 响应式,可直接修改 |
| Getter | Getter | 计算属性,派生状态 | 类似 Vue 的计算属性 |
| Action | Mutation + Action | 执行操作,修改状态 | 同步/异步操作,直接修改 state |
Pinia 没有 Mutation 的概念,Action 既可以执行同步操作,也可以执行异步操作。这与 Vuex 不同,Vuex 中同步操作使用 Mutation,异步操作使用 Action。
// stores/counter.js
import { defineStore } from 'pinia'
// 使用 defineStore 定义 Store
// 第一个参数是 Store 的唯一 ID
export const useCounterStore = defineStore('counter', {
// State: 存储状态数据
state: () => ({
count: 0,
name: '计数器'
}),
// Getter: 计算属性
getters: {
// 自动推断返回类型
doubleCount: (state) => state.count * 2,
// 使用其他 getter
doubleCountPlusOne() {
return this.doubleCount + 1
},
// 带参数的 getter
multiplyBy: (state) => {
return (multiplier) => state.count * multiplier
}
},
// Action: 执行操作
actions: {
increment() {
// 直接修改 state
this.count++
},
decrement() {
this.count--
},
// 带参数的 action
incrementBy(amount) {
this.count += amount
},
// 异步 action
async incrementAsync() {
// 模拟异步操作
await new Promise(resolve => setTimeout(resolve, 1000))
this.count++
},
// 重置状态
reset() {
this.$reset() // Pinia 提供的重置方法
}
}
})
// stores/counter.ts
import { defineStore } from 'pinia'
// 定义 State 类型
interface CounterState {
count: number
name: string
}
export const useCounterStore = defineStore('counter', {
// State
state: (): CounterState => ({
count: 0,
name: '计数器'
}),
// Getter
getters: {
doubleCount(state): number {
return state.count * 2
},
// 使用 this 访问其他 getter
doubleCountPlusOne(): number {
return this.doubleCount + 1
}
},
// Action
actions: {
increment(): void {
this.count++
},
incrementBy(amount: number): void {
this.count += amount
},
async incrementAsync(): Promise {
await new Promise(resolve => setTimeout(resolve, 1000))
this.count++
}
}
})
<template>
<div>
<h3>计数器示例</h3>
<p>当前计数: {{ count }}</p>
<p>双倍计数: {{ doubleCount }}</p>
<p>双倍计数加一: {{ doubleCountPlusOne }}</p>
<p>乘以5: {{ multiplyBy(5) }}</p>
<div class="mt-3">
<button @click="increment">增加</button>
<button @click="decrement">减少</button>
<button @click="incrementBy(5)">增加5</button>
<button @click="incrementAsync">异步增加</button>
<button @click="reset">重置</button>
</div>
</div>
</template>
<script>
import { useCounterStore } from '@/stores/counter'
import { mapState, mapActions, mapWritableState } from 'pinia'
export default {
name: 'CounterComponent',
computed: {
// 1. 直接使用 store 实例
...mapState(useCounterStore, ['count']),
// 2. 使用 mapState 映射 getter
...mapState(useCounterStore, {
doubleCount: 'doubleCount',
doubleCountPlusOne: 'doubleCountPlusOne',
multiplyBy: 'multiplyBy'
}),
// 3. 使用 mapWritableState 映射可写状态
...mapWritableState(useCounterStore, ['name'])
},
methods: {
// 映射 actions
...mapActions(useCounterStore, [
'increment',
'decrement',
'incrementBy',
'incrementAsync',
'reset'
])
},
mounted() {
// 或者直接在方法中访问 store
const store = useCounterStore()
console.log('Store:', store)
}
}
</script>
<template>
<div>
<h3>计数器示例 (Composition API)</h3>
<p>当前计数: {{ count }}</p>
<p>Store名称: {{ counterStore.name }}</p>
<p>双倍计数: {{ doubleCount }}</p>
<p>双倍计数加一: {{ doubleCountPlusOne }}</p>
<p>乘以5: {{ multiplyBy(5) }}</p>
<div class="mt-3">
<button @click="increment">增加</button>
<button @click="decrement">减少</button>
<button @click="incrementBy(5)">增加5</button>
<button @click="incrementAsync">异步增加</button>
<button @click="reset">重置</button>
</div>
<div class="mt-3">
<input v-model="newName" placeholder="修改store名称" />
<button @click="updateName">更新名称</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
// 获取 store 实例
const counterStore = useCounterStore()
// 方法1: 直接访问 store
const count = computed(() => counterStore.count)
const doubleCount = computed(() => counterStore.doubleCount)
const doubleCountPlusOne = computed(() => counterStore.doubleCountPlusOne)
const multiplyBy = (multiplier) => counterStore.multiplyBy(multiplier)
// 方法2: 使用 storeToRefs 解构保持响应性
const { name } = storeToRefs(counterStore)
// 注意: 不能直接解构 actions,因为它们不是响应式的
const { increment, decrement, incrementBy, incrementAsync, reset } = counterStore
// 修改状态
const newName = ref('')
function updateName() {
// 直接修改
counterStore.name = newName.value
// 或者使用 $patch
counterStore.$patch({
name: newName.value
})
}
// 监听状态变化
import { watch } from 'vue'
watch(
() => counterStore.count,
(newCount, oldCount) => {
console.log(`计数从 ${oldCount} 变为 ${newCount}`)
}
)
// 访问整个 state
console.log('整个 state:', counterStore.$state)
</script>
| 特性 | Pinia | Vuex 4 |
|---|---|---|
| API 设计 | 简洁直观,易于理解 | 相对复杂,概念较多 |
| TypeScript 支持 | 原生支持,类型推断准确 | 需要额外配置,类型支持有限 |
| 模块化 | 天然模块化,每个 Store 独立 | 需要 namespaced 模块 |
| Mutation | 无,Action 处理所有操作 | 必需,用于同步修改状态 |
| 代码量 | 较少样板代码 | 较多样板代码 |
| 组合式 API | 完美集成 | 需要额外配置 |
| 包大小 | 约 1.5KB | 约 10KB |
| Vue 版本 | Vue 2 和 Vue 3 | Vue 2 (Vuex 3) / Vue 3 (Vuex 4) |
Pinia 天然支持模块化,每个 Store 都是独立的模块。
// stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
id: null,
name: '',
email: '',
isLoggedIn: false,
token: ''
}),
getters: {
isAuthenticated: (state) => state.isLoggedIn,
userInfo: (state) => ({
id: state.id,
name: state.name,
email: state.email
})
},
actions: {
login(credentials) {
// 模拟登录
return new Promise((resolve) => {
setTimeout(() => {
this.id = 1
this.name = '张三'
this.email = 'zhangsan@example.com'
this.isLoggedIn = true
this.token = 'fake-jwt-token'
// 保存到 localStorage
localStorage.setItem('auth_token', this.token)
resolve(this.userInfo)
}, 1000)
})
},
logout() {
this.$reset()
localStorage.removeItem('auth_token')
},
updateProfile(userData) {
this.name = userData.name || this.name
this.email = userData.email || this.email
}
}
})
// stores/product.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// 使用 Composition API 风格定义 Store
export const useProductStore = defineStore('product', () => {
// State
const products = ref([])
const cart = ref([])
const isLoading = ref(false)
// Getter
const totalProducts = computed(() => products.value.length)
const cartTotal = computed(() =>
cart.value.reduce((total, item) => total + item.price * item.quantity, 0)
)
const cartItemCount = computed(() =>
cart.value.reduce((count, item) => count + item.quantity, 0)
)
// Action
async function fetchProducts() {
isLoading.value = true
try {
// 模拟 API 调用
const response = await fetch('/api/products')
products.value = await response.json()
} catch (error) {
console.error('获取商品失败:', error)
} finally {
isLoading.value = false
}
}
function addToCart(product) {
const existingItem = cart.value.find(item => item.id === product.id)
if (existingItem) {
existingItem.quantity++
} else {
cart.value.push({
...product,
quantity: 1
})
}
}
function removeFromCart(productId) {
cart.value = cart.value.filter(item => item.id !== productId)
}
function clearCart() {
cart.value = []
}
return {
// State
products,
cart,
isLoading,
// Getter
totalProducts,
cartTotal,
cartItemCount,
// Action
fetchProducts,
addToCart,
removeFromCart,
clearCart
}
})
// stores/order.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'
import { useProductStore } from './product'
export const useOrderStore = defineStore('order', {
state: () => ({
orders: [],
currentOrder: null
}),
actions: {
async createOrder(cartItems) {
const userStore = useUserStore()
const productStore = useProductStore()
if (!userStore.isAuthenticated) {
throw new Error('请先登录')
}
// 创建订单
const order = {
id: Date.now(),
userId: userStore.id,
items: cartItems,
total: cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0),
createdAt: new Date().toISOString()
}
this.orders.push(order)
this.currentOrder = order
// 清空购物车
productStore.clearCart()
return order
},
async fetchUserOrders() {
const userStore = useUserStore()
if (!userStore.isAuthenticated) {
throw new Error('请先登录')
}
// 模拟获取用户订单
this.orders = [
{
id: 1,
userId: userStore.id,
items: [],
total: 100,
createdAt: '2023-01-01'
}
]
}
}
})
// plugins/pinia-plugin.js
import { createPinia } from 'pinia'
// 持久化存储插件
export function piniaPersistPlugin({ store }) {
// 从 localStorage 恢复状态
const storageKey = `pinia-${store.$id}`
const savedState = localStorage.getItem(storageKey)
if (savedState) {
store.$patch(JSON.parse(savedState))
}
// 监听状态变化并保存到 localStorage
store.$subscribe((mutation, state) => {
localStorage.setItem(storageKey, JSON.stringify(state))
})
}
// 日志插件
export function piniaLoggerPlugin({ store }) {
// 监听状态变化
store.$subscribe((mutation, state) => {
console.group(`Pinia Store: ${store.$id}`)
console.log('Mutation:', mutation)
console.log('New State:', state)
console.groupEnd()
})
// 监听 action 执行
store.$onAction(({ name, store, args, after, onError }) => {
console.group(`Action: ${name}`)
console.log('Store:', store.$id)
console.log('Args:', args)
after((result) => {
console.log('Result:', result)
console.groupEnd()
})
onError((error) => {
console.error('Error:', error)
console.groupEnd()
})
})
}
// 在应用中使用插件
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
// 注册插件
pinia.use(piniaPersistPlugin)
pinia.use(piniaLoggerPlugin)
const app = createApp(App)
app.use(pinia)
app.mount('#app')
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
// 1. 订阅状态变化
const unsubscribe = counterStore.$subscribe((mutation, state) => {
console.log('状态变化:', mutation)
console.log('新状态:', state)
// mutation 包含以下信息:
// - type: 'direct' | 'patch object' | 'patch function'
// - storeId: store 的 ID
// - events: 触发的事件
})
// 取消订阅
unsubscribe()
// 2. 订阅特定状态的变化
import { watch } from 'vue'
watch(
() => counterStore.count,
(newCount, oldCount) => {
console.log(`count 从 ${oldCount} 变为 ${newCount}`)
}
)
// 3. 订阅 action 执行
counterStore.$onAction(({ name, store, args, after, onError }) => {
console.log(`Action ${name} 开始执行`)
after((result) => {
console.log(`Action ${name} 执行完成,结果:`, result)
})
onError((error) => {
console.error(`Action ${name} 执行失败:`, error)
})
})
模拟一个简单的购物车应用:
等待操作...
Pinia 是 Vue.js 生态中一个现代化、高效的状态管理解决方案。通过简洁的 API 设计和优秀的 TypeScript 支持,它大大简化了 Vue 应用的状态管理。
关键要点回顾:
defineStore 函数定义 Store无论是新项目还是迁移现有项目,Pinia 都提供了一个优秀的状态管理方案,值得在 Vue 应用中尝试和使用。