Pinia 是 Vue 3 官方推荐的状态管理库,相比 Vuex 更轻量、类型推断更好、支持组合式 API 写法。
5.1 为什么用 Pinia
| 对比维度 | Vuex 4 | Pinia |
|---|---|---|
| TypeScript 支持 | 一般(需手动类型) | 完整开箱即用 |
| API 复杂度 | mutation/action/module | 只有 state/getters/actions |
| 组合式 API | 不支持 | 支持 setup store |
| DevTools | 支持 | 支持(更好的时间旅行) |
| Bundle Size | ~10KB | ~1.5KB |
| 代码分割 | 需要配置 | 天然支持 |
5.2 定义 Store
选项式写法(Options Store)
ts
// stores/counter.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// state:相当于 data
state: () => ({
count: 0,
name: 'Counter'
}),
// getters:相当于 computed
getters: {
doubled: (state) => state.count * 2,
isPositive: (state) => state.count > 0,
// 引用其他 getter(使用 this)
tripled(): number { return this.doubled * 1.5 }
},
// actions:相当于 methods,支持异步
actions: {
increment() { this.count++ },
async fetchAndSet() {
const data = await api.getData()
this.count = data.count
}
}
})
组合式写法(Setup Store,推荐)
ts
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// ref → state
const user = ref<User | null>(null)
const token = ref(localStorage.getItem('token') || '')
// computed → getters
const isLoggedIn = computed(() => !!token.value)
const userName = computed(() => user.value?.name ?? '游客')
// function → actions
async function login(email: string, password: string) {
const data = await authApi.login({ email, password })
token.value = data.token
user.value = data.user
localStorage.setItem('token', data.token)
}
function logout() {
user.value = null
token.value = ''
localStorage.removeItem('token')
}
// 必须 return 所有东西
return { user, token, isLoggedIn, userName, login, logout }
})
5.3 使用 Store
vue
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
// ✅ 用 storeToRefs 解构,保持响应式
const { count, doubled, isPositive } = storeToRefs(counterStore)
// ✅ actions 可以直接解构(不是响应式,无需 storeToRefs)
const { increment, fetchAndSet } = counterStore
// ❌ 错误做法:直接解构 state 失去响应式
// const { count } = counterStore // count 不会更新!
</script>
<template>
<p>{{ count }} × 2 = {{ doubled }}</p>
<button @click="increment">+1</button>
<button @click="counterStore.$patch({ count: 10 })">设为 10</button>
</template>
5.4 $patch 批量修改
ts
const store = useUserStore()
// 方式1:对象 patch(简单场景)
store.$patch({
name: '新名字',
email: 'new@email.com'
})
// 方式2:函数 patch(推荐,支持复杂操作)
store.$patch((state) => {
state.list.push({ id: Date.now(), text: '新条目' })
state.count++
})
5.5 持久化存储
ts
// 方案一:手动
import { defineStore } from 'pinia'
import { ref, watch } from 'vue'
export const useSettingsStore = defineStore('settings', () => {
const theme = ref(localStorage.getItem('theme') || 'dark')
watch(theme, (val) => localStorage.setItem('theme', val))
return { theme }
})
// 方案二:pinia-plugin-persistedstate 插件
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
pinia.use(piniaPluginPersistedstate)
defineStore('user', { /* ... */ }, {
persist: {
key: 'user-store',
storage: localStorage,
paths: ['token', 'preferences'] // 只持久化指定字段
}
})
5.6 Store 间相互调用
ts
// stores/cart.ts
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', () => {
const userStore = useUserStore() // 直接调用其他 store
async function checkout() {
if (!userStore.isLoggedIn) {
throw new Error('请先登录')
}
// ...
}
return { checkout }
})
章节总结
| 知识点 | 核心要点 | 重要程度 |
|---|---|---|
| defineStore | 组合式写法更推荐,灵活性更高 | ⭐⭐⭐⭐⭐ |
| storeToRefs | 解构 state/getters 时必须用 | ⭐⭐⭐⭐⭐ |
| $patch | 批量修改,函数形式支持复杂操作 | ⭐⭐⭐⭐ |
| 持久化 | 手动 or pinia-plugin-persistedstate | ⭐⭐⭐⭐ |
| Store 间调用 | 直接在 action 中引用其他 store | ⭐⭐⭐⭐ |
Demo 说明
Demo 位于 vue_demos/src/views/Chapter05_Pinia/PiniaDemo.vue:
- Counter Store(组合式):storeToRefs、increment/reset、变更历史
- User Store(选项式):登录/登出、Getters 推导值展示
- Cart Store:购物车增删、购物车角标跨组件共享(App Header)
🔗 专栏链接 :Vue 3 全栈开发实战专栏
📦 项目源码资源 :点击下载项目源码