Vue.js: Pinia 状态管理
最后更新:2026-10-20
Pinia 是 Vue 3 官方推荐的状态管理库——替代 Vuex。Pinia 提供了更简洁的 API、完整的 TypeScript 支持、模块化热更新、原生 DevTools 支持。Vue 官方已经将 Pinia 作为默认推荐(Vuex 不再维护)。
Pinia 让你管理跨组件共享状态(用户信息、购物车、全局设置)比 provide/inject 更结构化,比 Vuex 简洁 50%。
1. 你将学到
- 为什么 Pinia 替代 Vuex(5 大优势)
- defineStore 两种风格(Options / Setup)
- state / getter / action 三大核心
- Pinia 模块化(多 store)
- 持久化(pinia-plugin-persistedstate)
- DevTools 集成
- 5 大实战场景
2. 一个购物车"组件 5 处不一致"的噩梦
(1) 痛点:5 个组件各管各的购物车数据
Alice 的电商后台有 5 个组件需要购物车数据:
JS
// ❌ 翻车版:5 个组件 5 份数据
// CartIcon.vue
const cartCount = ref(0)
// ProductCard.vue
const localCart = ref([])
// CartPage.vue
const cart = ref({ items: [], total: 0 })
// CheckoutPage.vue
const myCart = ref([])
// Header.vue
const cartItems = ref([])
产品经理 Charlie:
"Alice,我在 ProductCard 里添加商品,购物车图标没更新!图标显示'0',但页面显示'1 item'。5 个组件 5 份购物车数据——它们不同步!"
(2) Vue Pinia 解法:1 个 store,5 个组件共享
JS
// stores/cart.js
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
items: [],
total: 0
}),
getters: {
itemCount: (state) => state.items.length,
totalPrice: (state) => state.items.reduce((sum, i) => sum + i.price, 0)
},
actions: {
addItem(product) {
this.items.push(product)
this.total += product.price
},
removeItem(id) {
this.items = this.items.filter(i => i.id !== id)
}
}
})
VUE
<!-- CartIcon.vue -->
<script setup>
import { useCartStore } from '@/stores/cart'
const cart = useCartStore()
// 自动响应:cart.itemCount 变了,icon 立即更新
</script>
<template>
<span>🛒 {{ cart.itemCount }}</span>
</template>
VUE
<!-- ProductCard.vue -->
<script setup>
import { useCartStore } from '@/stores/cart'
const cart = useCartStore()
</script>
<template>
<button @click="cart.addItem(product)">Add to Cart</button>
</template>
1 个 store,5 个组件实时同步。
(3) 收益
使用 Pinia 后:
- 数据一致性:1 个 store,5 个组件实时同步
- 代码量:5 份 ref → 1 个 store(-80%)
- DevTools:时间旅行调试(看每一步状态)
- TypeScript:完整类型推断
- 持久化: localStorage 自动保存
3. Pinia vs Vuex 5 大优势
| 维度 | Vuex 4 (Vue 3) | Pinia (Vue 3) |
|---|---|---|
| API 简洁 | ⭐⭐⭐ 复杂(mutations / actions) | ⭐⭐⭐⭐⭐ 简洁(state / getters / actions) |
| TypeScript | ⭐⭐⭐ 需额外配置 | ⭐⭐⭐⭐⭐ 原生支持 |
| Composition API | ⭐⭐ 不友好 | ⭐⭐⭐⭐⭐ 一等公民 |
| DevTools | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ 改进 |
| 包大小 | ~10KB | ~1KB |
Vue 官方推荐 Pinia,Vuex 4 进入维护模式(不再添加新特性)。
4. defineStore 两种风格
(1) Options 风格(类似 Vuex)
JS
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// state:数据
state: () => ({
count: 0,
name: 'Counter'
}),
// getters:派生值(类似 computed)
getters: {
doubleCount: (state) => state.count * 2,
isZero: (state) => state.count === 0
},
// actions:方法(类似 methods)
actions: {
increment() {
this.count++
},
async fetchData() {
const res = await fetch('/api/count')
this.count = await res.json()
}
}
})
(2) Setup 风格(推荐,组合式 API)
JS
// stores/auth.js
import { defineStore } from 'pinia'
const { ref, computed } = Vue
export const useAuthStore = defineStore('auth', () => {
// 1. state(用 ref)
const user = ref(null)
const token = ref(localStorage.getItem('token') || '')
// 2. getters(用 computed)
const isLoggedIn = computed(() => !!token.value)
const userName = computed(() => user.value?.name || 'Guest')
// 3. actions(普通函数)
function login(credentials) {
// API call...
user.value = { name: 'Alice' }
token.value = 'xxx'
}
function logout() {
user.value = null
token.value = ''
}
return { user, token, isLoggedIn, userName, login, logout }
})
(3) Options vs Setup 对比
| 维度 | Options 风格 | Setup 风格 |
|---|---|---|
| 写法 | state: () => ({}) |
const x = ref() |
| getters | (state) => ... |
computed() |
| actions | function() { this.x } |
普通函数 |
| TypeScript | 需手动 | 自动推断 |
| 组合能力 | 较弱 | 强(可用其他 composable) |
| 推荐度 | 熟悉 Vuex 用 | 新项目推荐 |
▶ 示例:用 reactive 模拟 defineStore 的 Options 风格
▶ 试一试浏览器里没有 Pinia 的 CDN 依赖,这里用
reactive+provide/inject手写一个 store:state 就是对象字段,getters 就是get访问器,actions 就是方法。
输出:
TEXT
根组件面板显示「count: 0 | doubleCount: 0 | isZero: true」,子组件面板显示「count: 0 | doubleCount: 0」。
点根组件「+1」两次,两处同时变成 count: 2、doubleCount: 4,isZero 变为 false。
点子组件「子组件 +1」后根组件同步到 count: 3、doubleCount: 6;点「reset」两处一起归零,isZero 回到 true。
5. 5 大核心 API
(1) state:数据
JS
// Options 风格
state: () => ({
count: 0,
user: null,
items: []
})
// Setup 风格
const count = ref(0)
const user = ref(null)
const items = ref([])
(2) getters:派生值
JS
// Options 风格
getters: {
// 简单 getter
doubleCount: (state) => state.count * 2,
// 访问其他 getter(用 this)
ratio(state) {
return this.doubleCount / 100
},
// 返回函数(参数化 getter)
getItemById: (state) => (id) => {
return state.items.find(i => i.id === id)
}
}
// Setup 风格
const doubleCount = computed(() => count.value * 2)
const getItemById = (id) => items.value.find(i => i.id === id)
(3) actions:方法
JS
// Options 风格
actions: {
// 同步
increment() {
this.count++
},
// 异步
async fetchData() {
const res = await fetch('/api/data')
this.data = await res.json()
},
// 访问其他 actions
async loginAndFetch(credentials) {
await this.login(credentials)
await this.fetchUser()
}
}
// Setup 风格
function increment() {
count.value++
}
async function fetchData() {
const res = await fetch('/api/data')
data.value = await res.json()
}
(4) 组件中使用
VUE
<script setup>
import { useCartStore } from '@/stores/cart'
import { storeToRefs } from 'pinia'
const cart = useCartStore()
// 1. 直接访问 state(响应式)
console.log(cart.items)
// 2. 用 storeToRefs 解构(保持响应式)
const { items, total } = storeToRefs(cart)
// 3. 调用 action
cart.addItem(product)
// 4. 监听 state 变化
watch(() => cart.items, (newItems) => {
console.log('Cart changed:', newItems)
})
</script>
(5) 5 大注意事项
JS
// ⚠️ 注意 1:解构 state 丢失响应式
const { items } = cart // ❌ items 是普通值
const { items } = storeToRefs(cart) // ✅ items 是 ref
// ⚠️ 注意 2:修改 state 必须用 action
cart.items.push(...) // ❌ 不推荐(直接修改)
cart.addItem(...) // ✅ 用 action
// ⚠️ 注意 3:action 内的 this 指向 store
actions: {
increment() {
this.count++ // ✅ this = store
}
}
// ⚠️ 注意 4:getter 缓存
getters: {
doubleCount() {
console.log('recomputed') // 只在依赖变化时打印
return this.count * 2
}
}
// ⚠️ 注意 5:useStore 在 setup 外使用要传 pinia 实例
import { getActivePinia } from 'pinia'
const cart = useCartStore(getActivePinia()) // 在 .js 文件中
6. Pinia 持久化
(1) 安装插件
BASH
npm install pinia-plugin-persistedstate
(2) main.js 配置
(3) 5 种持久化配置
JS
// stores/cart.js
export const useCartStore = defineStore('cart', () => {
const items = ref([])
return { items }
}, {
// 1. 默认:localStorage key 为 'cart'
persist: true,
// 2. 自定义 key
persist: {
key: 'my-cart',
storage: localStorage
},
// 3. 只持久化部分 state
persist: {
paths: ['items'] // 只保存 items,不保存 total
},
// 4. sessionStorage(关闭浏览器就清除)
persist: {
storage: sessionStorage
},
// 5. 自定义序列化(加密等)
persist: {
serializer: {
serialize: (value) => btoa(JSON.stringify(value)),
deserialize: (value) => JSON.parse(atob(value))
}
}
})
▶ 示例:用 watch + localStorage 手写持久化 store
▶ 试一试这就是
pinia-plugin-persistedstate干的事:启动时读缓存恢复 state,state 一变就用watch深监听写回localStorage。
输出:
TEXT
首次打开显示「主题:light | 购物车:0 件」,下方 localStorage 内容为 (空),子组件徽章是 0。
点「切换主题」→ 主题变 dark;点「加入一件商品」两次 → 购物车 2 件、子组件徽章同步为 2。
localStorage 内容实时变成 {"theme":"dark","items":[{"id":1766...},{"id":1766...}]}。
刷新页面:主题仍是 dark、购物车仍是 2 件(启动时从缓存恢复);点「清空购物车」后 items 变空数组。
7. 综合演练:多组件共享同一个 Store
▶ 示例:Pinia 风格全局状态(CDN 用 provide/inject 模拟)
▶ 展开运行示例(109 行)
输出:
TEXT
页面顶部说明文字下依次出现 3 个面板:计数器组件 A、计数器组件 B 和购物车。
两个计数器初始都显示「count: 0 | double: 0」;点 A 的 +1 后两处同时变为 1 / 2,点 B 的 +1 也会同步到 A。
购物车初始显示「items: 0 件, total: $0」;点「加 1 件 iPhone」变为 1 件、$999,再点变 2 件、$1998;点「清空」归零。
⚠️ 此为教学演示 Pinia 核心思想(单例 + provide/inject)。生产项目用官方
pinia 库(必须 Vite),提供 DevTools、TypeScript、模块化拆分等。
(1) 📊 5 种核心 API 速查(Pinia)
| API | 作用 | 例子 |
|---|---|---|
| state | 响应式状态 | const count = ref(0) |
| getters | 计算派生(缓存) | computed(() => count * 2) |
| actions | 方法(可异步) | function increment() { count++ } |
| storeToRefs | 解构保持响应 | const { count } = storeToRefs(cart) |
| 持久化 | localStorage 同步 | pinia-plugin-persistedstate |
(2) 📊 5 个常见错误速查
| 错误 | 现象 | 解决 |
|---|---|---|
| 解构 state 丢失响应 | 数据不变 | 用 storeToRefs |
| 直接改 state | 警告 | 改用 action |
| 异步 action 无 await | 不等结果 | await cart.fetchData() |
| pinia 未注册 | useStore is not a function |
main.js app.use(pinia) |
| 持久化后字段未持久 | key 不对 | 检查 paths 配置 |
(3) 📊 5 大实战场景
| 场景 | Store | 关键字段 |
|---|---|---|
| 认证 | useAuthStore | user, token, isLoggedIn |
| 购物车 | useCartStore | items, total, itemCount |
| 主题 | useThemeStore | theme, locale, density |
| 通知 | useNotificationStore | notifications, unread |
| 数据 | useDataStore | list, loading, error |
❓ 常见问题
Q Pinia 和 Vuex 选哪个?
A Pinia。Vue 官方已推荐 Pinia,Vuex 4 不再维护。新项目必须用 Pinia,老 Vuex 项目可逐步迁移。
Q Pinia 替代 provide/inject 吗?
A 不替代。Pinia 是全局状态管理,provide/inject 是跨层级通信。Pinia 用于"应用全局状态"(用户/购物车),provide/inject 用于"主题/i18n/配置"。
Q 什么时候用 Pinia vs Composable?
A Pinia 用于跨组件共享的全局状态(用户、购物车)。Composable 用于组件级复用逻辑(useMouse、useFetch)。Pinia 更结构化,有 DevTools。
Q Pinia 怎么模块化?
A 每个 store 一个文件,放在
src/stores/ 目录。Pinia 自动支持 tree-shaking。Q storeToRefs 什么时候用?
A 解构 state 时必须用,否则丢失响应式。getters 和 actions 不需要。
Q Pinia 持久化支持什么存储?
A 默认 localStorage,也支持 sessionStorage、自定义 storage(IndexedDB、Cookie 等)、自定义序列化(加密)。
Q Pinia 性能如何?
A state 修改触发响应式更新,getter 自动缓存,DevTools 优化。比 Vuex 快 30-50%(没有 mutations 概念)。
Q Pinia 在 SSR 中能用吗?
A 能。
import { createPinia } from 'pinia',每个请求创建独立实例。Nuxt 3 内置 Pinia 集成。📖 小节
- Pinia 是 Vue 3 官方推荐状态管理库,替代 Vuex
- 5 大优势:简洁 / TypeScript / Composition / DevTools / 小包
- defineStore 两种风格:Options(Vuex 习惯)/ Setup(推荐)
- 5 大 API:state / getters / actions / storeToRefs / 持久化
- 模块化:每个 store 一个文件
- 持久化:pinia-plugin-persistedstate
- Pinia vs Composable:全局状态 vs 组件逻辑
📝 作业
-
基础题(难度⭐) 实现一个简单的计数 store:
- count state
- doubleCount getter
- increment / decrement / reset actions
- 在 3 个组件中共享
-
进阶题(难度⭐⭐) 实现购物车 store:
- items array
- itemCount / totalPrice getters
- addItem / removeItem / clearCart actions
- 持久化到 localStorage
- 在 5 个组件中测试(ProductCard / CartIcon / CartPage / Checkout / Header)
-
挑战题(难度⭐⭐⭐) 实现完整的电商后台 store 系统:
- 5 个 store:auth / cart / theme / notification / product
- 每个 store 完整 state + getters + actions
- 持久化(auth/cart/theme)
- Setup 风格 + TypeScript
- storeToRefs 解构
- 跨 store 调用(cart 调用 auth 检查登录)
- 单元测试(用 Vitest)