10.1 核心概念
- Store 组织结构
- 按领域拆分 store(auth/cart/product),每个文件一个 store,放在
src/stores/下。 - 组合 store
- store 之间通过在 action 内调用其它 use 函数协作,而非嵌套模块。
- 测试 store
- 用
setActivePinia(createPinia())在每个测试前建立干净实例,独立测试逻辑。 - DevTools
- Vue DevTools 的 Pinia 面板可查看/编辑 state、追踪 action、时间旅行回放。
- Vuex 迁移
- 把 modules 拆成独立 store、mutations 合并进 actions、getters 直接搬、命名空间去掉。
- 性能清单
- storeToRefs 解构、避免带参 getter 滥用、按需持久化、markRaw 大对象等优化点。
10.2 完整购物车 Store
// stores/cart.ts
import { defineStore } from "pinia";
import { useAuthStore } from "./auth";
interface CartItem { id: number; name: string; price: number; qty: number; }
interface CartState { items: CartItem[]; }
export const useCartStore = defineStore("cart", {
state: (): CartState => ({ items: [] }),
getters: {
count: (s) => s.items.reduce((n, i) => n + i.qty, 0),
total: (s) => s.items.reduce((t, i) => t + i.price * i.qty, 0),
isEmpty(): boolean { return this.count === 0; },
},
actions: {
add(product: Omit<CartItem, "qty">) {
const found = this.items.find((i) => i.id === product.id);
if (found) found.qty++;
else this.items.push({ ...product, qty: 1 });
},
remove(id: number) {
this.items = this.items.filter((i) => i.id !== id);
},
async checkout() {
const auth = useAuthStore();
if (!auth.isLoggedIn) throw new Error("请先登录");
await fetch("/api/orders", {
method: "POST",
body: JSON.stringify({ userId: auth.userId, items: this.items }),
});
this.items = [];
},
},
persist: true, // 刷新保留购物车
});
10.3 用户/认证 Store
// stores/auth.ts
export const useAuthStore = defineStore("auth", {
state: () => ({ token: "", user: null as User | null }),
getters: {
isLoggedIn: (s) => !!s.token,
userId: (s) => s.user?.id ?? 0,
},
actions: {
async login(email: string, pwd: string) {
const res = await fetch("/api/login", {
method: "POST", body: JSON.stringify({ email, pwd }),
});
const data = await res.json();
this.$patch({ token: data.token, user: data.user });
},
logout() { this.$reset(); },
},
persist: { pick: ["token"] },
});
10.4 目录结构建议
src/
├─ stores/
│ ├─ auth.ts # 认证
│ ├─ cart.ts # 购物车
│ ├─ product.ts # 商品列表
│ └─ index.ts # 可选:统一 re-export
├─ main.ts # createPinia + 插件
└─ components/
拆分原则
一个 store 只管一个领域。别做"上帝 store"塞下所有状态。store 太大时按子领域再拆,用组合 store 协作,而不是嵌套。
10.5 测试 Store
// cart.spec.ts —— Vitest
import { setActivePinia, createPinia } from "pinia";
import { beforeEach, describe, it, expect } from "vitest";
import { useCartStore } from "@/stores/cart";
describe("cart store", () => {
beforeEach(() => {
// 每个用例前建立干净的 pinia
setActivePinia(createPinia());
});
it("添加商品累加数量", () => {
const cart = useCartStore();
cart.add({ id: 1, name: "茶", price: 20 });
cart.add({ id: 1, name: "茶", price: 20 });
expect(cart.count).toBe(2);
expect(cart.total).toBe(40);
});
});
测试组件内的 store,可用 @pinia/testing 的 createTestingPinia(),它默认把 action 做成 spy(可断言调用而不真正执行)。
10.6 DevTools 调试
- 安装 Vue DevTools 浏览器扩展,Pinia 面板会列出所有已激活 store。
- 可实时查看/直接编辑 state,观察 getter 计算结果。
- 时间旅行:回放 state 变更历史,定位是哪次 action 改坏了数据。
- 每个 action 调用、每次
$patch都会记录时间线。
10.7 从 Vuex 迁移对照
| Vuex | Pinia |
|---|---|
| modules(嵌套 + namespaced) | 多个独立 store 文件 |
| state | state(组合式用 ref) |
| getters | getters(组合式用 computed) |
| mutations + actions | 合并为 actions(直接改 state) |
| commit('setX', v) | store.x = v 或 action |
| dispatch('fetch') | store.fetch() |
| mapState / mapActions | 同名辅助函数(首参传 useStore) |
迁移策略
Pinia 与 Vuex 可共存,逐个 module 迁移:先搬无依赖的叶子模块,把 mutations 逻辑并进 actions,删掉 commit,最后移除 namespace。全部迁完再卸载 Vuex。
10.8 性能与最佳实践清单
- ✅ 解构 store 一律
storeToRefs,避免丢响应式或整 store 依赖。 - ✅ 慎用带参 getter(不缓存),高频查找改用 Map 结构。
- ✅ 持久化用
pick/omit精控,别整对象塞 localStorage。 - ✅ 大型不可变对象用
markRaw跳过响应式代理,省内存。 - ✅ SSR 严守"每请求一个 pinia 实例",浏览器 API 加环境守卫。
- ✅ 按领域拆 store,跨 store 用组合而非嵌套。
- ✅ store 逻辑写单测,组件测试用
createTestingPinia。
结语
从"认识 Pinia"到"生产实战",你已掌握 Vue 官方状态管理的全貌。Pinia 的哲学是少即是多——更少的概念、更少的样板、更好的类型。拿它替换 Vuex,或作为新项目的默认选择,都是 2026 年的正确答案。