讲解
组件内状态用 ref,子树共享用 provide/inject,那整个应用共享的状态——登录用户、购物车、全局通知——放哪?答案是 Pinia,Vue 官方的状态管理库。一个 store 就是一个「可全局访问的组合式函数」:defineStore 定义,任何组件 useXxxStore() 取用,拿到的是同一个响应式实例。
store 的三件套和组件一一对应:state 是数据(相当于 ref),getters 是推导(相当于 computed),actions 是方法(相当于普通函数,可异步)。推荐 setup 写法,和 <script setup> 风格完全一致:
什么时候需要 Pinia?一条判断线:状态被「不相邻的多个组件」使用,或需要在路由切换后保持(购物车跳页面不能丢),或需要 devtools 时间旅行调试。两个组件共享用 props/emits 或 provide 就够,别急着上 store。
三个高频细节。其一,解构 store 会丢响应式,const { count } = store 是快照不是引用,要么直接用 store.count,要么用 storeToRefs。其二,异步逻辑放 actions:async function fetchUser() { user.value = await api.getUser() },别在组件里散落请求。其三,多个 store 可以互相引用(cart store 里用 user store 的 id),但注意避免循环依赖。
示例
手写一个迷你 Pinia:模块级单例 + state/getters/actions 三件套(在本教程构建时被真实执行):
import assert from 'node:assert/strict';
// 迷你 store 工厂:单例缓存 + computed 风格的 getter
const registry = new Map();
function defineStore(id, setup) {
return function useStore() {
if (!registry.has(id)) {
registry.set(id, setup()); // 第一次调用才创建 → 全局单例
}
return registry.get(id);
};
}
const useCartStore = defineStore('cart', () => {
const items = [];
return {
items,
get totalPrice() {
// getter 等价于 computed
return items.reduce((sum, i) => sum + i.price * i.qty, 0);
},
add(product, qty = 1) {
// action:修改 state 的唯一入口
const found = items.find((i) => i.id === product.id);
if (found) found.qty += qty;
else items.push({ ...product, qty });
},
async checkout() {
// 异步 action
const total = this.totalPrice;
items.length = 0;
return total;
},
};
});
const cart1 = useCartStore();
const cart2 = useCartStore(); // 另一个「组件」拿到的同一个实例
assert.strictEqual(cart1, cart2);
cart1.add({ id: 1, name: '键盘', price: 199 });
cart2.add({ id: 1, name: '键盘', price: 199 }); // 同款合并数量
cart1.add({ id: 2, name: '鼠标', price: 99 }, 2);
assert.strictEqual(cart1.items.length, 2);
assert.strictEqual(cart1.items[0].qty, 2);
assert.strictEqual(cart2.totalPrice, 199 * 2 + 99 * 2);
cart1.checkout().then((paid) => {
assert.strictEqual(paid, 596);
assert.strictEqual(cart1.items.length, 0);
console.log('结算', paid, '元,购物车已清空');
});
console.log('购物车总价:', cart1.totalPrice, '元(两个组件共享同一份状态)');
真实的 Pinia store(setup 写法):
// stores/cart.js —— 仅示意:真实项目代码(依赖 pinia 包),构建时不执行
import { ref, computed } from 'vue';
import { defineStore } from 'pinia';
export const useCartStore = defineStore('cart', () => {
const items = ref([]);
const totalPrice = computed(() =>
items.value.reduce((sum, i) => sum + i.price * i.qty, 0),
);
function add(product, qty = 1) {
const found = items.value.find((i) => i.id === product.id);
if (found) found.qty += qty;
else items.value.push({ ...product, qty });
}
async function checkout() {
const total = totalPrice.value;
await fetch('/api/orders', { method: 'POST', body: JSON.stringify(items.value) });
items.value = [];
return total;
}
return { items, totalPrice, add, checkout };
});
<script setup>
import { storeToRefs } from 'pinia';
import { useCartStore } from './stores/cart';
const cart = useCartStore();
const { items, totalPrice } = storeToRefs(cart); // 解构保持响应式
</script>
<template>
<p>{{ items.length }} 种商品,共 {{ totalPrice }} 元</p>
<button @click="cart.checkout()">结算</button>
</template>
常见坑
- 直接解构 store 丢响应式:
const { items } = cart是静态快照,要么用 cart.items,要么 storeToRefs。 - 在组件里直接改 store 数据:能跑但绕过了 actions,状态变化无迹可查;复杂项目约定「修改只走 actions」。
- store 定义放在组件里:defineStore 要在模块顶层(或按需模块),放组件里会随组件重建丢失状态。
- 滥用 store:两个组件共享的小状态上 Pinia 是杀鸡用牛刀,先用 props/provide。
- 忘记持久化:刷新页面 store 归零,购物车这类状态要配合 localStorage 插件或手动序列化。
小结
Pinia = 全局单例的组合式函数;state/getters/actions 对应 ref/computed/方法;解构用 storeToRefs。下一章给界面加动感:过渡与动画。