Vue.js: Pinia 状态管理
最后更新:2026-08-26
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 用 | 新项目推荐 |
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 配置
JS
// main.js
const { createApp } = Vue
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
const app = createApp(App)
app.use(pinia)
app.mount('#app')
(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))
}
}
})
7. 完整示例:电商后台 5 大 Pinia Store
▶ 示例:Pinia 风格全局状态(CDN 用 provide/inject 模拟)
HTML
📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<style>
button { padding: 6px 12px; margin: 4px; background: #42b883; color: white; border: none; border-radius: 4px; cursor: pointer; }
.panel { padding: 1rem; border: 1px solid #ddd; margin: 0.5rem 0; border-radius: 4px; }
</style>
<div id="app">
<p style="color: #999; font-size: 0.85rem;">
Pinia 风格全局 store(CDN 用 provide/inject 模拟,3 个组件共享同一份 state)
</p>
<div class="panel">
<h4>1. 计数器组件 A</h4>
<p>count: {{ store.count }} | double: {{ store.doubleCount }}</p>
<button @click="store.increment">+1</button>
<button @click="store.decrement">-1</button>
<button @click="store.reset">Reset</button>
</div>
<div class="panel">
<h4>2. 计数器组件 B(共享同一 store)</h4>
<p>count: {{ store.count }} | double: {{ store.doubleCount }}</p>
<button @click="store.increment">+1(点我看 A 是否同步)</button>
</div>
<div class="panel">
<h4>3. 购物车 store(模拟多 store 共享)</h4>
<p>items: {{ cart.itemCount }} 件, total: ${{ cart.totalPrice }}</p>
<button @click="cart.addItem({ name: 'iPhone', price: 999 })">加 1 件 iPhone</button>
<button @click="cart.clear">清空</button>
</div>
</div>
<script>
const { createApp, ref, computed, provide, inject } = Vue
// ✅ Pinia 风格 store 工厂函数(CDN 模拟)
function createPiniaStore() {
// ----- 计数器 store -----
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() { count.value++ }
function decrement() { count.value-- }
function reset() { count.value = 0 }
const counterStore = { count, doubleCount, increment, decrement, reset }
// ----- 购物车 store -----
const items = ref([])
const itemCount = computed(() => items.value.length)
const totalPrice = computed(() =>
items.value.reduce((sum, i) => sum + i.price, 0)
)
function addItem(item) { items.value.push(item) }
function clear() { items.value = [] }
const cartStore = { items, itemCount, totalPrice, addItem, clear }
return { counterStore, cartStore }
}
// 全局单例(Pinia 内部就是这样管理)
const { counterStore, cartStore } = createPiniaStore()
// provide 全局 stores
const App = {
setup() {
provide('counterStore', counterStore)
provide('cartStore', cartStore)
return {}
},
components: {
CounterA: {
template: `
<div class="panel">
<h4>1. 计数器组件 A</h4>
<p>count: {{ store.count }} | double: {{ store.doubleCount }}</p>
<button @click="store.increment">+1</button>
<button @click="store.decrement">-1</button>
<button @click="store.reset">Reset</button>
</div>
`,
setup() {
const store = inject('counterStore')
return { store }
}
},
CounterB: {
template: `
<div class="panel">
<h4>2. 计数器组件 B(共享同一 store)</h4>
<p>count: {{ store.count }} | double: {{ store.doubleCount }}</p>
<button @click="store.increment">+1(点我看 A 是否同步)</button>
</div>
`,
setup() {
const store = inject('counterStore')
return { store }
}
},
CartPanel: {
template: `
<div class="panel">
<h4>3. 购物车 store(多 store 共享)</h4>
<p>items: {{ cart.itemCount }} 件, total: \${{ cart.totalPrice }}</p>
<button @click="cart.addItem({ name: 'iPhone', price: 999 })">加 1 件 iPhone</button>
<button @click="cart.clear">清空</button>
</div>
`,
setup() {
const cart = inject('cartStore')
return { cart }
}
}
},
template: `
<p style="color: #999; font-size: 0.85rem;">
Pinia 风格全局 store(CDN 用 provide/inject 模拟,3 个组件共享同一份 state)
</p>
<counter-a></counter-a>
<counter-b></counter-b>
<cart-panel></cart-panel>
`
}
createApp(App).mount('#app')
</script>
⚠️ 此为教学演示 Pinia 核心思想(单例 + provide/inject)。生产项目用官方
pinia 库(必须 Vite),提供 DevTools、TypeScript、模块化拆分等。
▶ 示例: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 |
▶ 示例:5 个常见错误速查
| 错误 | 现象 | 解决 |
|---|---|---|
| 解构 state 丢失响应 | 数据不变 | 用 storeToRefs |
| 直接改 state | 警告 | 改用 action |
| 异步 action 无 await | 不等结果 | await cart.fetchData() |
| pinia 未注册 | useStore is not a function |
main.js app.use(pinia) |
| 持久化后字段未持久 | key 不对 | 检查 paths 配置 |
▶ 示例: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)