Pinia 状态管理
Pinia 状态管理
Pinia 是 Vue 3 官方推荐的状态管理库,替代了 Vuex,提供了更简洁的 API 和更好的 TypeScript 支持。它的作者 Eduardo San Martin Morote 同时也是 Vue Router 的核心维护者,因此 Pinia 从设计之初就与 Vue 生态深度契合。
一、什么是状态管理
1. 为什么需要状态管理
在讲 Pinia 之前,先要理解"状态管理"到底解决什么问题。可以把组件树想象成一家公司:每个组件是一个员工,props 是"上级给下级下发文件",emit 是"下级向上级汇报"。当公司只有三五个人时,靠打招呼就能传递信息;一旦公司有几百人,跨部门的信息传递就必须有一个"中央档案室"——这就是状态管理库的角色。
没有状态管理时,跨层级组件共享数据会遇到两个典型问题:
状态管理库把这些共享状态抽离到一个独立的、全局可访问的仓库(Store)中,任何组件都能直接读写,从而彻底解耦。
2. Pinia 与 Vuex 的区别
Vuex 是 Vue 2 时代的官方方案,但它的 API 相对繁琐(尤其是 mutations 的存在),对 TypeScript 的支持也不够友好。Pinia 正是为了解决这些痛点而生。
| 对比维度 | Vuex 4 | Pinia |
|----------|--------|-------|
| 包体积 | 约 9.6 KB | 约 1.5 KB(gzip 后约 1KB) |
| mutations | 必须通过 mutations 改状态 | 已移除,直接改或用 actions |
| 修改方式 | commit/dispatch 字符串 | 直接调用方法,有类型提示 |
| 模块化 | 嵌套 modules,命名空间复杂 | 扁平化多 store,天然隔离 |
| TypeScript | 需要大量手写类型 | 类型自动推导,几乎零配置 |
| 组合式 API | 支持有限 | 原生支持 Setup Store |
| Devtools | 支持 | 支持更好,含时间旅行 |
| 服务端渲染 | 支持 | 支持,且更简洁 |
一句话总结:Pinia = Vuex 5 的设计理念提前落地。事实上 Vuex 官方文档已明确建议新项目直接使用 Pinia。
二、安装与配置
安装:
# 使用 npm
npm install pinia
# 使用 pnpm
pnpm add pinia
# 使用 yarn
yarn add piniaVue 3 中注册:
// main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.mount('#app');Vue 2 中使用(需要 @vue/composition-api):
Vue 2 项目要使用 Pinia,必须先安装 @vue/composition-api 插件来提供组合式 API 支持。@vue/composition-api 是 Vue 官方为 Vue 2 打造的适配器,让老项目也能享受组合式 API 的代码组织优势。
// main.js(Vue 2)
import Vue from 'vue';
import { createPinia, PiniaVuePlugin } from 'pinia';
Vue.use(PiniaVuePlugin);
const pinia = createPinia();
new Vue({
el: '#app',
pinia
});Nuxt 3 中使用:
npm install @pinia/nuxt// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt']
});三、核心概念
Pinia 的一个 Store 由三部分组成,可以类比 Vue 组件:
| Pinia 概念 | 对应组件概念 | 作用 |
|------------|--------------|------|
| state | data | 存放响应式状态数据 |
| getters | computed | 基于 state 派生出的计算值,带缓存 |
| actions | methods | 修改状态、处理同步与异步逻辑 |
1. 定义 Store(Options 风格)
// stores/counter.js
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
// state:初始状态,必须是返回对象的函数
state: () => ({
count: 0,
name: 'Pinia'
}),
// getters:派生状态,自动缓存
getters: {
doubleCount: (state) => state.count * 2,
// 通过 this 访问其他 getter
doubleCountPlusOne() {
return this.doubleCount + 1;
}
},
// actions:同步/异步方法
actions: {
increment() {
this.count++;
},
async incrementAsync() {
await new Promise((resolve) => setTimeout(resolve, 1000));
this.count++;
}
}
});其中 defineStore 的第一个参数 'counter' 是这个 store 的唯一 ID,Pinia 用它来连接 Devtools 并做数据隔离,整个应用中不能重复。
2. 定义 Store(Setup 风格,推荐)
Setup 风格用组合式 API 的写法定义 Store,灵活性更高,也更容易复用组合函数:
// stores/counter.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useCounterStore = defineStore('counter', () => {
// ref/reactive → state
const count = ref(0);
const name = ref('Pinia');
// computed → getters
const doubleCount = computed(() => count.value * 2);
// function → actions
function increment() {
count.value++;
}
async function incrementAsync() {
await new Promise((resolve) => setTimeout(resolve, 1000));
count.value++;
}
// 必须 return 需要暴露的内容
return { count, name, doubleCount, increment, incrementAsync };
});3. 在组件中使用
<script setup>
import { storeToRefs } from 'pinia';
import { useCounterStore } from '@/stores/counter';
const store = useCounterStore();
// 直接解构会丢失响应式,必须用 storeToRefs 包裹 state/getters
const { count, doubleCount } = storeToRefs(store);
// actions 是方法,可以直接解构
const { increment } = store;
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double: {{ doubleCount }}</p>
<button @click="increment">+1</button>
<button @click="store.incrementAsync">异步 +1</button>
</div>
</template>四、State 的读取与修改
读取: 直接 store.count 或用 storeToRefs 保持响应式。
修改状态的三种方式:
const store = useCounterStore();
// 方式 1:直接赋值(最简单)
store.count = 10;
// 方式 2:$patch 批量修改(一次性变更多个字段,性能更好)
store.$patch({
count: store.count + 1,
name: 'Updated'
});
// 方式 3:$patch 传函数(适合数组、复杂逻辑)
store.$patch((state) => {
state.count++;
state.name = state.name.toUpperCase();
});
// 方式 4:通过 action(推荐,业务逻辑集中)
store.increment();重置状态:
// Options 风格自带 $reset
store.$reset();
// 注意:Setup 风格不支持 $reset,需要自己实现
// 在 Setup Store 里手写一个 reset 方法
function reset() {
count.value = 0;
name.value = 'Pinia';
}五、Getters 详解
Getters 类似组件的计算属性,会根据依赖自动缓存,只有依赖变化时才重新计算。
export const useProductStore = defineStore('product', {
state: () => ({
products: [
{ id: 1, name: '键盘', price: 200, stock: 5 },
{ id: 2, name: '鼠标', price: 100, stock: 0 }
]
}),
getters: {
// 普通 getter
inStockProducts: (state) => state.products.filter((p) => p.stock > 0),
// 带参数的 getter:返回一个函数(这种写法不会被缓存)
getProductById: (state) => {
return (id) => state.products.find((p) => p.id === id);
}
}
});
// 使用
const store = useProductStore();
console.log(store.inStockProducts); // [{ id: 1, ... }]
console.log(store.getProductById(1)); // { id: 1, name: '键盘', ... }六、Actions 详解
Actions 用于封装业务逻辑,可以是同步的,也可以是异步的,并且支持完善的错误处理。
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
token: '',
loading: false
}),
actions: {
async login(credentials) {
this.loading = true;
try {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
});
if (!res.ok) throw new Error('登录失败');
const data = await res.json();
this.user = data.user;
this.token = data.token;
localStorage.setItem('token', data.token);
} catch (error) {
console.error('登录出错:', error);
throw error; // 抛出给组件层处理
} finally {
this.loading = false;
}
},
logout() {
this.user = null;
this.token = '';
localStorage.removeItem('token');
}
}
});七、真实案例:电商购物车
下面用一个跨 store 协作的购物车场景,演示 Pinia 在真实项目中的用法。购物车 store 需要读取用户 store(判断是否 VIP 打折)和商品 store(查价格)。
// stores/cart.js
import { defineStore } from 'pinia';
import { useUserStore } from './user';
import { useProductStore } from './product';
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] // [{ productId, quantity }]
}),
getters: {
// 商品总数
totalCount: (state) =>
state.items.reduce((sum, i) => sum + i.quantity, 0),
// 总价(跨 store 协作 + VIP 折扣)
totalPrice() {
const userStore = useUserStore();
const productStore = useProductStore();
const subtotal = this.items.reduce((sum, item) => {
const product = productStore.getProductById(item.productId);
return sum + (product?.price || 0) * item.quantity;
}, 0);
return userStore.isVip ? subtotal * 0.9 : subtotal;
}
},
actions: {
addItem(productId, quantity = 1) {
const existing = this.items.find((i) => i.productId === productId);
if (existing) {
existing.quantity += quantity;
} else {
this.items.push({ productId, quantity });
}
},
removeItem(productId) {
this.items = this.items.filter((i) => i.productId !== productId);
},
clear() {
this.items = [];
}
}
});在组件中组合使用:
<script setup>
import { storeToRefs } from 'pinia';
import { useCartStore } from '@/stores/cart';
const cart = useCartStore();
const { totalCount, totalPrice } = storeToRefs(cart);
</script>
<template>
<div class="cart">
<p>共 {{ totalCount }} 件商品</p>
<p>合计:¥{{ totalPrice }}</p>
<button @click="cart.addItem(1)">加入键盘</button>
<button @click="cart.clear">清空</button>
</div>
</template>八、持久化存储
刷新页面后,Pinia 中的状态会丢失(因为它保存在内存里)。要让登录 token、购物车等数据在刷新后依然存在,需要做持久化。
方式 1:使用官方推荐插件
npm install pinia-plugin-persistedstate// main.js
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);// stores/user.js
export const useUserStore = defineStore('user', {
state: () => ({ user: null, token: '' }),
persist: {
key: 'user-store', // localStorage 键名
storage: localStorage, // 也可用 sessionStorage
paths: ['token'] // 只持久化 token,不存整个 user
}
});方式 2:自定义插件(了解原理)
// plugins/persist.js
export function myPersistPlugin({ store }) {
// 恢复
const saved = localStorage.getItem(store.$id);
if (saved) store.$patch(JSON.parse(saved));
// 订阅变化并保存
store.$subscribe((mutation, state) => {
localStorage.setItem(store.$id, JSON.stringify(state));
});
}九、订阅与插件系统
Pinia 提供了强大的订阅能力和插件机制,可用于日志、埋点、持久化等横切关注点。
const store = useCounterStore();
// $subscribe:监听 state 变化
store.$subscribe((mutation, state) => {
console.log('类型:', mutation.type); // 'direct' | 'patch object' | 'patch function'
console.log('storeId:', mutation.storeId);
console.log('新状态:', state);
});
// $onAction:监听 action 调用(可做统一日志/耗时统计)
store.$onAction(({ name, args, after, onError }) => {
const start = Date.now();
console.log(`Action ${name} 开始,参数:`, args);
after((result) => {
console.log(`Action ${name} 完成,耗时 ${Date.now() - start}ms`);
});
onError((error) => {
console.error(`Action ${name} 出错:`, error);
});
});十、TypeScript 支持
Pinia 对 TypeScript 的支持是它最大的卖点之一。Options 风格几乎零配置就能获得完整类型推导:
// stores/user.ts
import { defineStore } from 'pinia';
interface User {
id: number;
name: string;
email: string;
}
interface UserState {
user: User | null;
token: string;
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({
user: null,
token: ''
}),
getters: {
isLoggedIn: (state): boolean => !!state.token,
userName: (state): string => state.user?.name ?? 'Guest'
},
actions: {
async login(credentials: { username: string; password: string }) {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
});
const data: { user: User; token: string } = await res.json();
this.user = data.user;
this.token = data.token;
}
}
});十一、调试
Vue DevTools 是 Vue 官方浏览器扩展,对 Pinia 有一等支持。它提供组件树检查、状态查看、时间旅行调试等功能,可以直观查看每个 store 的 state/getters、追踪每次 action 调用、回退到任意历史状态。Chrome 和 Firefox 商店均可安装,Vue 2/3 都支持。
Pinia 在 DevTools 中的三大调试能力:
十二、常见坑与最佳实践
常见坑:
| 坑 | 现象 | 解决 |
|----|------|------|
| 直接解构 store | 数据不再响应式 | 用 storeToRefs 包裹 state/getters |
| 在 setup 外调用 useStore | 报错找不到 pinia 实例 | 确保在组件 setup 或已注册 pinia 后调用 |
| Setup Store 用 $reset | 报错不存在该方法 | 自己写 reset 方法 |
| 持久化了敏感的整个 user | 泄露风险 | 用 paths 精确控制持久化字段 |
最佳实践:
推荐的目录结构:
stores/
├── user.ts # 用户相关状态
├── cart.ts # 购物车相关状态
├── product.ts # 产品相关状态
├── settings.ts # 设置相关状态
└── index.ts # 统一导出十三、总结
| 要点 | 说明 |
|------|------|
| 定位 | Vue 3 官方状态管理库,取代 Vuex |
| 核心 | state / getters / actions 三件套 |
| 两种写法 | Options 风格(直观)、Setup 风格(灵活,推荐) |
| 响应式解构 | 必须用 storeToRefs |
| 修改状态 | 直接改 / $patch / action |
| 持久化 | pinia-plugin-persistedstate 插件 |
| 扩展 | $subscribe / $onAction / 自定义插件 |
| 优势 | 体积小、类型好、API 简洁、Devtools 强 |
Pinia 用极简的 API 覆盖了从简单计数器到复杂电商系统的全部状态管理需求。掌握 state/getters/actions 三件套,配合 storeToRefs 和持久化插件,就能应对绝大多数实际项目场景。