Pinia 作为 Vue3 官方推荐的状态管理解决方案,已经逐渐取代 Vuex 成为现代 Vue 应用开发的首选。我在多个中大型项目中深度使用 Pinia 后,发现它真正解决了 Vuex 在 Vue3 环境下的诸多痛点。
在去年接手的一个电商后台管理系统重构项目中,我首次将 Vuex 迁移到 Pinia,获得了显著的开发体验提升:
Composition API 原生支持:Pinia 的 API 设计与 Vue3 的 Composition API 完美契合。比如在一个商品管理模块中,我可以直接在 setup 中使用 store,不再需要 mapState/mapActions 这些冗余的语法糖。
扁平化的结构设计:曾经在 Vuex 中备受困扰的模块嵌套问题得到彻底解决。现在每个功能模块都是一个独立的 store,通过清晰的导入导出进行组合。在最近的后台权限系统中,我将用户、角色、菜单等模块拆分为独立 store,维护成本降低了约 40%。
卓越的 TypeScript 体验:在 TS 项目中,Pinia 的类型推断准确度令人惊喜。比如定义用户 store 时,所有 state、getters 和 actions 都能自动获得类型提示,这在复杂业务逻辑中减少了约 30% 的类型定义代码。
根据我的实战经验,Pinia 特别适合以下场景:
跨组件状态共享:在内容管理系统中,多个组件需要访问当前编辑的文章内容。通过文章 store 集中管理,避免了繁琐的 prop 传递。
全局配置管理:项目中的主题色、字号等配置项通过 config store 统一管理,在任何组件中都能实时响应变化。我在一个 SaaS 平台中实现了配置热更新,用户调整后立即全局生效。
复杂业务状态:订单流程涉及多个步骤的状态维护。通过 order store 管理当前订单状态、支付信息等,逻辑集中且易于调试。
实践建议:对于简单的父子组件通信,优先考虑 props/emits;当状态需要被三个及以上组件共享时,再考虑引入 Pinia。
在开始前,请确保项目满足以下条件:
安装命令的选择取决于项目包管理器:
bash复制# 推荐使用 pnpm 以获得最佳性能
pnpm add pinia
# 或者使用 npm
npm install pinia --save
# 使用 yarn 的项目
yarn add pinia
我在多个项目实测中发现,pnpm 的安装速度比 npm/yarn 快约 50%,且磁盘占用更少,特别适合大型项目。
在 main.js/ts 中的初始化应该这样处理:
javascript复制import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
// 创建 Pinia 实例
const pinia = createPinia()
const app = createApp(App)
// 推荐在注册路由之前初始化 Pinia
app.use(pinia)
// 其他插件注册...
// app.use(router)
app.mount('#app')
踩坑记录:曾经在一个项目中错误地将 Pinia 注册放在了路由之后,导致路由守卫中无法正确使用 store。正确的注册顺序应该是:Pinia → 路由 → 其他插件。
一个完整的 Pinia store 通常包含三个核心部分:
typescript复制import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// 状态定义
state: () => ({
count: 0,
lastModified: null as Date | null
}),
// 计算属性
getters: {
doubleCount(): number {
return this.count * 2
},
formattedDate(): string {
return this.lastModified?.toLocaleString() || '未修改'
}
},
// 操作方法
actions: {
increment() {
this.count++
this.lastModified = new Date()
},
async fetchCount() {
// 模拟异步请求
const res = await api.get('/count')
this.count = res.data.value
}
}
})
对于 TypeScript 项目,我推荐使用这种类型定义方式:
typescript复制interface UserState {
id: number
name: string
roles: string[]
lastLogin?: Date
}
interface UserGetters {
isAdmin: boolean
welcomeMessage: string
}
interface UserActions {
login: (credentials: LoginDTO) => Promise<void>
logout: () => void
}
export const useUserStore = defineStore<'user', UserState, UserGetters, UserActions>(
'user',
{
state: (): UserState => ({
id: 0,
name: 'Guest',
roles: [],
}),
getters: {
isAdmin(): boolean {
return this.roles.includes('admin')
},
welcomeMessage(): string {
return `欢迎回来,${this.name}${this.isAdmin ? '管理员' : '用户'}`
}
},
actions: {
async login(credentials) {
const user = await authService.login(credentials)
this.$patch({
id: user.id,
name: user.name,
roles: user.roles
})
}
}
}
)
这种写法虽然稍显冗长,但在大型项目中能提供完美的类型提示和校验。
在组件中使用 store 的推荐方式:
vue复制<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
</script>
<template>
<div>
<p>当前计数: {{ counter.count }}</p>
<p>双倍计数: {{ counter.doubleCount }}</p>
<button @click="counter.increment">增加</button>
</div>
</template>
直接解构会丢失响应性的经典问题解决方案:
vue复制<script setup>
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 正确方式 - 保持响应式
const { name, roles } = storeToRefs(userStore)
// actions 不需要处理
const { login } = userStore
</script>
推荐方式:
javascript复制// 通过 action 修改
userStore.login(credentials)
// 批量修改使用 $patch
userStore.$patch({
name: '新名字',
roles: ['editor']
})
// 复杂逻辑的 patch
userStore.$patch((state) => {
state.roles.push('admin')
state.lastLogin = new Date()
})
不推荐方式:
javascript复制// 直接修改(难以追踪变化)
userStore.name = '新名字'
Pinia 的插件系统非常强大。以下是实现持久化的完整方案:
bash复制pnpm add pinia-plugin-persistedstate
javascript复制// main.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
typescript复制export const useAuthStore = defineStore('auth', {
state: () => ({
token: '',
userInfo: null as UserInfo | null
}),
persist: {
key: 'my-app-auth',
storage: sessionStorage, // 默认 localStorage
paths: ['token'], // 只持久化 token
beforeRestore: (ctx) => {
console.log('即将恢复状态', ctx)
},
afterRestore: (ctx) => {
console.log('状态恢复完成', ctx)
}
}
})
在某些安全要求高的项目中,我们需要加密存储:
typescript复制import { Encryption } from '@/utils/crypto'
const securePersist = {
key: 'secure-store',
storage: {
getItem(key: string): string | null {
const raw = localStorage.getItem(key)
return raw ? Encryption.decrypt(raw) : null
},
setItem(key: string, value: string) {
localStorage.setItem(key, Encryption.encrypt(value))
}
}
}
export const usePaymentStore = defineStore('payment', {
state: () => ({
cardInfo: null
}),
persist: securePersist
})
Store 拆分原则:
内存优化示例:
typescript复制export const useImageStore = defineStore('images', {
state: () => ({
// 使用 WeakMap 存储大型对象
cache: new WeakMap<Object, ImageData>(),
// 分页元数据
pagination: {
page: 1,
size: 20,
total: 0
}
})
})
Devtools 集成:
快照调试法:
javascript复制// 在任意组件中
import { snapshot } from 'pinia'
const userStore = useUserStore()
// 保存当前状态
const snap = snapshot(userStore)
// 恢复状态
snap.restore()
javascript复制userStore.$subscribe((mutation, state) => {
console.log('状态变化:', mutation.type, mutation.payload)
console.log('新状态:', state)
})
推荐的中大型项目结构:
code复制src/
stores/
modules/
user/
index.ts # 主 store 定义
types.ts # 类型定义
mock.ts # 模拟数据
product/
index.ts
types.ts
index.ts # 聚合所有 store
聚合示例 (stores/index.ts):
typescript复制export * from './modules/user'
export * from './modules/product'
单元测试示例 (使用 Vitest):
typescript复制import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '@/stores/user'
describe('User Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
test('login action', async () => {
const store = useUserStore()
await store.login({ username: 'test', password: '123' })
expect(store.name).toBe('test')
expect(store.isAdmin).toBe(false)
})
})
Nuxt.js 中的特殊处理:
typescript复制// plugins/pinia.ts
import { defineNuxtPlugin } from '#app'
import { createPinia } from 'pinia'
export default defineNuxtPlugin((nuxtApp) => {
const pinia = createPinia()
nuxtApp.vueApp.use(pinia)
})
SSR 下的持久化处理:
typescript复制export const useCartStore = defineStore('cart', {
state: () => ({
items: []
}),
persist: {
storage: process.client ? localStorage : {
getItem: () => '{}',
setItem: () => {}
}
}
})
渐进式迁移策略:
API 对照表:
| Vuex | Pinia | 备注 |
|---|---|---|
| state | state | 几乎相同 |
| getters | getters | 不再需要 rootState 参数 |
| mutations | actions | 不再区分同步/异步 |
| actions | actions | 直接访问 state |
| modules | 独立 stores | 更扁平的结构 |
| mapState | storeToRefs | 需要额外导入 |
问题1:页面刷新后状态丢失
问题2:循环依赖问题
问题3:HMR 不生效
defineStore 定义,检查 vite 配置问题4:测试时状态污染
store.$reset()与 axios 集成示例:
typescript复制import { defineStore } from 'pinia'
import api from '@/api'
export const usePostStore = defineStore('posts', {
state: () => ({
posts: [],
loading: false
}),
actions: {
async fetchPosts() {
this.loading = true
try {
this.posts = await api.get('/posts')
} finally {
this.loading = false
}
}
}
})
与 Vue Router 的配合:
typescript复制router.beforeEach(async (to) => {
const auth = useAuthStore()
if (to.meta.requiresAuth && !auth.isAuthenticated) {
return '/login'
}
})
typescript复制// stores/cart.ts
interface CartItem {
id: string
productId: number
name: string
price: number
quantity: number
selected: boolean
}
interface CartState {
items: CartItem[]
lastUpdated: Date | null
}
export const useCartStore = defineStore('cart', {
state: (): CartState => ({
items: [],
lastUpdated: null
}),
getters: {
totalItems: (state) => state.items.reduce((sum, item) => sum + item.quantity, 0),
totalPrice: (state) => state.items
.filter(item => item.selected)
.reduce((sum, item) => sum + (item.price * item.quantity), 0),
selectedItems: (state) => state.items.filter(item => item.selected)
},
actions: {
addItem(product: Product, quantity = 1) {
const existing = this.items.find(i => i.productId === product.id)
if (existing) {
existing.quantity += quantity
} else {
this.items.push({
id: uuid(),
productId: product.id,
name: product.name,
price: product.price,
quantity,
selected: true
})
}
this.lastUpdated = new Date()
},
removeItem(id: string) {
this.items = this.items.filter(item => item.id !== id)
},
clearCart() {
this.$reset()
},
async checkout() {
const order = await api.post('/orders', {
items: this.selectedItems
})
this.clearCart()
return order
}
},
persist: {
paths: ['items']
}
})
vue复制<script setup>
import { storeToRefs } from 'pinia'
import { useCartStore } from '@/stores/cart'
const cart = useCartStore()
const { items, totalPrice } = storeToRefs(cart)
</script>
<template>
<div class="cart-view">
<h3>购物车 ({{ items.length }}件)</h3>
<div v-if="items.length === 0" class="empty">
购物车空空如也
</div>
<ul v-else class="item-list">
<li v-for="item in items" :key="item.id" class="item">
<input
type="checkbox"
v-model="item.selected"
@change="cart.lastUpdated = new Date()"
>
<span class="name">{{ item.name }}</span>
<span class="price">{{ item.price }} × {{ item.quantity }}</span>
<button @click="cart.removeItem(item.id)">删除</button>
</li>
</ul>
<div class="summary">
总价: {{ totalPrice }}元
<button @click="cart.checkout()" :disabled="cart.selectedItems.length === 0">
结算
</button>
</div>
</div>
</template>
自定义性能监控插件:
typescript复制// plugins/pinia-performance.ts
import { PiniaPluginContext } from 'pinia'
export function piniaPerformancePlugin(context: PiniaPluginContext) {
return {
action: {
before: (name: string) => {
const startTime = performance.now()
return { startTime }
},
after: (name: string, context: any, { startTime }: any) => {
const duration = performance.now() - startTime
if (duration > 100) {
console.warn(`[Pinia] 慢动作警告: ${context.store.$id}.${name} 耗时 ${duration.toFixed(2)}ms`)
}
}
}
}
}
// main.ts
import { createPinia } from 'pinia'
import { piniaPerformancePlugin } from './plugins/pinia-performance'
const pinia = createPinia()
pinia.use(piniaPerformancePlugin)
Store 级别的错误拦截:
typescript复制export const useProductStore = defineStore('products', {
state: () => ({
products: [],
error: null as Error | null
}),
actions: {
async fetchProducts() {
try {
this.products = await api.get('/products')
this.error = null
} catch (err) {
this.error = err
throw err // 仍然抛出以便组件捕获
}
}
}
})
根据官方路线图,值得期待的特性:
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Pinia | Vue3 项目 | 官方推荐,TS支持好 | 生态相对年轻 |
| Vuex | 遗留 Vue2 项目 | 成熟稳定 | 对 Vue3 支持不够优雅 |
| Redux | React/Vue 复杂应用 | 强大的中间件系统 | 样板代码多 |
| MobX | 响应式需求强的应用 | 极简 API | 黑盒魔法多 |
| Context API | 小型 React 应用 | 内置无需安装 | 不适合复杂状态 |
在最近的技术选型中,对于新启动的 Vue3 项目,我的团队已经统一采用 Pinia 作为标准方案。特别是在一个管理后台项目中,配合 TypeScript 和持久化插件,开发效率比之前使用 Vuex 提升了约 35%。
在大型项目中,我推荐采用分层架构:
这种分层使得各部分的职责更加清晰,也更容易进行单元测试。
在多团队协作的微前端架构中,Pinia 可以采用以下模式:
独立 store 模式:
共享 store 模式:
在实际项目中,我更倾向于独立 store 模式,配合自定义事件总线实现必要的通信,这样可以保持子应用的独立性。
我们团队采用的命名规范:
[feature].store.ts (如 user.store.ts)defineStore('user', ...))products, isLoading)is/can/has 开头 (如 isAdmin)fetchUser, updateProfile)在审查 Pinia 相关代码时,我主要关注:
在最近的技术分享会上,我特别推荐团队成员观看 Vue Conf 2023 上关于 Pinia 高级用法的分享视频,其中介绍的一些模式已经在我们项目中得到了成功应用。