Pinia 是 Vue 核心团队开发的状态管理库,作为 Vuex 的官方继任者。它比 Vuex 更轻量、更 TypeScript 友好、API 更直觉——没有 mutations,直接通过 action 修改 state,支持 Composition API 原生。Vue 官方在 Vue 3 时代明确推荐 Pinia 作为新项目的状态管理方案。
一、为什么选 Pinia?
1.1 Pinia vs Vuex:核心差异
| 维度 | Pinia | Vuex 4 |
|---|---|---|
| API 风格 | 直觉(store.count++) | 样板多(mutation → action) |
| TypeScript | ⭐ 原生支持,类型推导完美 | 需要模块声明 |
| 体积 | ⭐ ~1KB | ~1.5KB |
| Devtools | ✅ 时间旅行、热更新 | ✅ 时间旅行 |
| SSR | ✅ 原生支持 | 需要配置 |
| 插件 | ✅ 简单 | ✅ 复杂 |
| 模块化 | 自动(每个 store 独立) | 需要手动命名空间 |
| 学习曲线 | ⭐ 低 | 中 |
1.2 Pinia vs 全局状态库(Zustand 等)
虽然理论上任何 JS 状态库都能与 Vue 配合,但 Pinia 的优势在于:
- Vue 响应式集成:Pinia 的 state 就是 Vue 的 reactive/ref,天然与组件响应式系统打通
- SSR 兼容:服务端渲染时自动处理 hydration
- Devtools:与 Vue DevTools 深度集成
- 插件生态:持久化、缓存、日志等插件丰富
二、Store 定义与基础用法
2.1 定义 Store
// stores/counter.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// 命名规范:use + Store 名
export const useCounterStore = defineStore('counter', () => {
// ========== State ==========
const count = ref(0)
const items = ref<string[]>([])
// ========== Getters ==========
const doubleCount = computed(() => count.value * 2)
const itemCount = computed(() => items.value.length)
// ========== Actions ==========
function increment() {
count.value++
}
function addItem(item: string) {
items.value.push(item)
}
function $reset() {
count.value = 0
items.value = []
}
return { count, items, doubleCount, itemCount, increment, addItem, $reset }
})
注意:次参数 'counter' 是 store 的唯一 ID,全局不能重复。
2.2 在组件中使用
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
const store = useCounterStore()
// 解构保持响应性(必须用 storeToRefs)
const { count, doubleCount } = storeToRefs(store)
// 可以直接修改 state(不需要 mutation)
const increase = () => {
store.increment()
// 或:store.count++
}
// $reset
const reset = () => store.$reset()
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double: {{ doubleCount }}</p>
<button @click="increase">+1</button>
<button @click="reset">Reset</button>
</div>
</template>
2.3 Options 风格定义(兼容 Vuex 用户)
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0, items: [] as string[] }),
getters: {
doubleCount: (state) => state.count * 2,
itemCount: (state) => state.items.length,
},
actions: {
increment() {
this.count++
},
addItem(item: string) {
this.items.push(item)
},
},
})
推荐:新项目中使用 Setup 风格(Composition API),类型推导更完整。
三、Pinia Store 组织策略
3.1 按领域拆分 Store
stores/
├── index.ts # store 导出聚合
├── auth.ts # 认证状态
├── user.ts # 用户信息
├── cart.ts # 购物车
├── product.ts # 商品数据
├── ui.ts # UI 状态(主题/侧边栏/模态框)
└── settings.ts # 用户设置
3.2 Store 间互相调用
// stores/cart.ts
import { useAuthStore } from './auth'
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([])
async function checkout() {
const auth = useAuthStore() // 在其他 store 中获取
if (!auth.isLoggedIn) {
throw new Error('Please login first')
}
await api.checkout({ items: items.value, userId: auth.user?.id })
items.value = []
}
return { items, checkout }
})
3.3 聚合导出
// stores/index.ts
export { useAuthStore } from './auth'
export { useUserStore } from './user'
export { useCartStore } from './cart'
export { useUIStore } from './ui'
四、插件与持久化
4.1 Pinia Plugin 开发
// plugins/piniaStorage.ts
import { PiniaPluginContext } from 'pinia'
export function piniaStoragePlugin(context: PiniaPluginContext) {
const { store } = context
// 从 localStorage 恢复
const saved = localStorage.getItem(store.$id)
if (saved) {
store.$patch(JSON.parse(saved))
}
// 订阅变化并保存
store.$subscribe((mutation, state) => {
localStorage.setItem(store.$id, JSON.stringify(state))
})
}
// main.ts
import { createPinia } from 'pinia'
import { piniaStoragePlugin } from './plugins/piniaStorage'
const pinia = createPinia()
pinia.use(piniaStoragePlugin)
4.2 使用 pinia-plugin-persistedstate
npm install pinia-plugin-persistedstate
// main.ts
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
pinia.use(piniaPluginPersistedstate)
// stores/auth.ts
export const useAuthStore = defineStore('auth', () => {
// ...
}, {
persist: {
storage: localStorage, // 默认 localStorage
paths: ['token', 'user'], // 只持久化这些字段
},
})
五、SSR 兼容
// Nuxt.js / SSR 项目
// 使用 $fetchState 或 ensure data is fetched
export const useProductStore = defineStore('product', () => {
const products = ref<Product[]>([])
const loading = ref(false)
async function fetchProducts() {
if (products.value.length) return // SSR 时已获取则跳过
loading.value = true
products.value = await $fetch('/api/products') // Nuxt 的 $fetch
loading.value = false
}
return { products, loading, fetchProducts }
})
// 在页面中
<script setup>
const productStore = useProductStore()
await productStore.fetchProducts() // await 确保 SSR 完成后再渲染
</script>
常见问题(FAQ)
为什么不用全局 ref/composable?
全局 ref 或跨文件 composable 可以共享状态,但缺少:
- Devtools 支持(时间旅行、状态快照)
- SSR 兼容性(hydration 处理)
- 插件生态(持久化、缓存)
- 命名冲突管理
Pinia 提供了生产级状态管理的完整解决方案。
Pinia 支持异步 Action 吗?
export const useStore = defineStore('store', () => {
const loading = ref(false)
async function fetchData() {
loading.value = true
try {
const data = await api.fetch()
return data
} finally {
loading.value = false
}
}
return { loading, fetchData }
})
// 使用
const data = await store.fetchData()
相关阅读
- Vue 详解 — 响应式系统基础
- Vue Composition API 完全指南 — ref/reactive/computed
- Vue Router 完全指南 — 路由与状态管理结合
- Nuxt.js 完全指南 — SSR 状态管理
- React 状态管理指南 — Pinia vs Zustand vs Redux
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。
「frontend」更多文章
Vue 性能优化指南:虚拟列表、懒加载、渲染优化与 Core Web Vitals
Vue 3 应用性能优化完整策略:虚拟滚动(vue-virtual-scroller)、组件懒加载与异步组件、KeepAlive 缓存、Suspense 异步优化、v-memo 渲染记忆化、响应式性能(shallowRef/toRaw)、Bundle 分析与代码分割、渲染函数优化、以及 Core Web Vitals(LCP/INP/CLS)调优。
Nuxt.js 完全指南:Vue 全栈框架的 SSR、SSG、API 路由与部署实践
Nuxt.js 3 深度实践:文件系统路由、SSR/SSG/ISR 渲染模式、API 路由(Server Routes)、useFetch/useAsyncData 数据获取、中间件与插件、SEO 与 Meta 管理、Nitro 服务端引擎、自动导入、部署到 Vercel/Netlify/Node.js。
Vue 测试深度指南:Vitest + Vue Test Utils + Playwright E2E 与 CI 集成
Vue 3 应用从单元测试到 E2E 的完整测试策略:Vitest + Vue Test Utils 组件测试(mount/emits/slots/async)、Pinia Store Mocking、MSW API 拦截、Playwright 端到端测试、Cypress 组件测试、覆盖率标准与 GitHub Actions CI 集成。