Pinia 状态管理
Pinia 是 Vue 3 推荐的状态管理库,替代了 Vuex,提供更简洁的 API 和更好的 TypeScript 支持。
基础概念
什么是状态管理?
当应用变得复杂时,组件之间需要共享状态,这时就需要状态管理:
┌─────────────────────────────────────────────────────────┐
│ Pinia 工作原理 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │Component│ │Component│ │Component│ │
│ │ A │ │ B │ │ C │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ └──────────────┼──────────────┘ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Pinia │ 状态存储 │
│ │ Store │ │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
为什么选择 Pinia?
- 更简单的 API:比 Vuex 更简洁
- 模块化设计:无需嵌套模块
- 完整的 TypeScript 支持
- 支持 Vue DevTools
- 可组合式:可以使用 storeToRefs
- SSR 支持
安装 Pinia
npm install pinia
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')
创建 Store
选项式 Store
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// 状态
state: () => ({
count: 0,
name: 'Counter'
}),
// 计算属性
getters: {
doubleCount: (state) => state.count * 2,
countPlusOne: (state) => state.count + 1
},
// 方法
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
reset() {
this.count = 0
}
}
})
组合式 Store
// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// 状态
const name = ref('')
const age = ref(0)
const token = ref('')
// 计算属性
const isLoggedIn = computed(() => !!token.value)
const userInfo = computed(() => ({ name: name.value, age: age.value }))
// 方法
function login(userData) {
name.value = userData.name
age.value = userData.age
token.value = 'mock-token'
}
function logout() {
name.value = ''
age.value = 0
token.value = ''
}
return {
name,
age,
token,
isLoggedIn,
userInfo,
login,
logout
}
})
使用 Store
基本使用
<script setup>
import { useCounterStore } from './stores/counter'
const counterStore = useCounterStore()
// 访问 state
console.log(counterStore.count)
// 访问 getters
console.log(counterStore.doubleCount)
// 调用 actions
counterStore.increment()
</script>
<template>
<div>
<p>计数: {{ counterStore.count }}</p>
<p>双倍: {{ counterStore.doubleCount }}</p>
<button @click="counterStore.increment">+</button>
</div>
</template>
storeToRefs
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from './stores/counter'
const counterStore = useCounterStore()
// 使用 storeToRefs 保持响应式
const { count, name } = storeToRefs(counterStore)
// actions 不需要 storeToRefs
const { increment, reset } = counterStore
</script>
<template>
<div>
<p>计数: {{ count }}</p>
<p>名称: {{ name }}</p>
<button @click="increment">+</button>
<button @click="reset">重置</button>
</div>
</template>
Getters
基本用法
state: () => ({
firstName: '张',
lastName: '三',
users: [
{ name: '张三', age: 25 },
{ name: '李四', age: 30 }
]
}),
getters: {
// 基本用法
fullName: (state) => state.firstName + state.lastName,
// 访问其他 getter
greeting: (state) => `你好,${state.firstName}${state.lastName}!`,
// 使用 this
upperName() {
return this.firstName.toUpperCase() + this.lastName.toUpperCase()
},
// 接收参数
getUserByName: (state) => (name) => {
return state.users.find(user => user.name === name)
}
}
Actions
基本用法
actions: {
async fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`)
const user = await response.json()
this.currentUser = user
} catch (error) {
console.error('获取用户失败:', error)
}
},
// 多个 actions
async login(credentials) {
const user = await api.login(credentials)
this.user = user
this.token = user.token
},
logout() {
this.user = null
this.token = null
}
}
访问其他 Store
import { useOtherStore } from './other'
actions: {
someAction() {
const otherStore = useOtherStore()
otherStore.someData
}
}
持久化
手动持久化
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
token: localStorage.getItem('token') || '',
userInfo: JSON.parse(localStorage.getItem('userInfo') || '{}')
}),
actions: {
setUser(user) {
this.userInfo = user
this.token = user.token
localStorage.setItem('token', user.token)
localStorage.setItem('userInfo', JSON.stringify(user))
},
clearUser() {
this.userInfo = {}
this.token = ''
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
}
}
})
使用插件
npm install pinia-plugin-persistedstate
// main.js
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
// store
export const useUserStore = defineStore('user', {
state: () => ({ name: '' }),
persist: true // 开启持久化
})
模块化组织
按功能划分
stores/
├── index.js # 导出所有 store
├── user.js # 用户相关
├── cart.js # 购物车相关
├── product.js # 产品相关
└── order.js # 订单相关
// stores/user.js
export const useUserStore = defineStore('user', { ... })
// stores/cart.js
export const useCartStore = defineStore('cart', { ... })
// stores/index.js
export { useUserStore } from './user'
export { useCartStore } from './cart'
实际示例:购物车
// stores/cart.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCartStore = defineStore('cart', () => {
const items = ref([])
// 计算属性
const totalItems = computed(() =>
items.value.reduce((sum, item) => sum + item.quantity, 0)
)
const totalPrice = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
// 方法
function addItem(product) {
const existingItem = items.value.find(item => item.id === product.id)
if (existingItem) {
existingItem.quantity++
} else {
items.value.push({
...product,
quantity: 1
})
}
}
function removeItem(productId) {
const index = items.value.findIndex(item => item.id === productId)
if (index > -1) {
items.value.splice(index, 1)
}
}
function updateQuantity(productId, quantity) {
const item = items.value.find(item => item.id === productId)
if (item) {
item.quantity = Math.max(0, quantity)
if (item.quantity === 0) {
removeItem(productId)
}
}
}
function clearCart() {
items.value = []
}
return {
items,
totalItems,
totalPrice,
addItem,
removeItem,
updateQuantity,
clearCart
}
})
<!-- Cart.vue -->
<script setup>
import { useCartStore } from '@/stores/cart'
import { storeToRefs } from 'pinia'
const cartStore = useCartStore()
const { items, totalItems, totalPrice } = storeToRefs(cartStore)
const { addItem, removeItem, updateQuantity, clearCart } = cartStore
</script>
<template>
<div class="cart">
<h2>购物车</h2>
<div v-if="items.length === 0">购物车为空</div>
<div v-else>
<div v-for="item in items" :key="item.id" class="cart-item">
<span>{{ item.name }}</span>
<span>¥{{ item.price }}</span>
<input
type="number"
:value="item.quantity"
@input="updateQuantity(item.id, Number($event.target.value))"
/>
<button @click="removeItem(item.id)">删除</button>
</div>
<div class="summary">
<p>总数量: {{ totalItems }}</p>
<p>总价: ¥{{ totalPrice }}</p>
<button @click="clearCart">清空购物车</button>
</div>
</div>
</div>
</template>
调试
Vue DevTools
Pinia 支持 Vue DevTools,可以查看 store 状态、修改历史等。
// 启用调试(开发环境)
import { createPinia } from 'pinia'
const pinia = createPinia()
// 开发环境添加调试
if (import.meta.env.DEV) {
pinia.use(({ store }) => {
store.$subscribe((mutation, state) => {
console.log('Store 变化:', mutation, state)
})
})
}
小结
本章我们详细学习了 Pinia 状态管理的完整内容:
- Pinia 基础:安装和基本概念
- 创建 Store:选项式和组合式两种方式
- 使用 Store:组件中使用 store
- Getters:计算属性
- Actions:修改状态的方法
- 持久化:数据持久化存储
- 购物车示例:综合应用
练习
- 创建一个用户认证 Store
- 创建一个待办事项 Store,支持添加、删除、完成
- 实现 Store 的持久化
准备好进入下一章了吗?