Chapter 10

实战与最佳实践

把前九章串起来——写一套生产级购物车/用户 store,规划目录结构,测试 store,用 DevTools 调试,从 Vuex 平滑迁移。

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/testingcreateTestingPinia(),它默认把 action 做成 spy(可断言调用而不真正执行)。

10.6 DevTools 调试

10.7 从 Vuex 迁移对照

VuexPinia
modules(嵌套 + namespaced)多个独立 store 文件
statestate(组合式用 ref)
gettersgetters(组合式用 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 性能与最佳实践清单

结语

从"认识 Pinia"到"生产实战",你已掌握 Vue 官方状态管理的全貌。Pinia 的哲学是少即是多——更少的概念、更少的样板、更好的类型。拿它替换 Vuex,或作为新项目的默认选择,都是 2026 年的正确答案。