FORMA

状态管理:Pinia 与 Vuex

Pinia 是 Vue 官方推荐的状态管理库(Vue 3 新项目默认选型)。Vuex 4 仍可与 Vue 3 配合,用于维护旧项目。见 Vue 基础

Pinia 是 Vue 官方新一代状态管理库,专为 Vue 3 设计,继承了 Vuex 的优点并简化了其复杂性。相比 Vuex,Pinia 提供了更直观的 API、更好的 TypeScript 支持和更小的打包体积。

一、Pinia 的核心设计差异

特性Vuex 4Pinia
mutations必须使用 mutations 修改状态,actions 用于异步操作删除 mutations,同步/异步统一在 actions 处理
模块化需要 modules + namespaced: true 实现隔离扁平化:每个 store 独立定义,自动隔离
TypeScript 支持需要手动定义复杂类型,类型推导较弱一流 TS 支持,自动推导大部分类型
Tree-shaking无法 tree-shake,未使用的模块也会打包Tree-shaking 友好,未使用的 store 自动剔除
插件扩展通过复杂 API 注册插件简单插件机制,支持响应式状态扩展

关键差异解读

  1. 删除 mutations:Vuex 中同步修改必须 commit mutation,异步操作必须 dispatch action,这导致了代码冗余和认知负担。Pinia 直接取消 mutations,所有状态修改都在 actions 中完成(无论是同步还是异步),简化了心智模型。
  2. 扁平化模块:Vuex 的模块嵌套容易导致命名冲突,需要手动添加 namespaced: true。Pinia 的每个 store 都是独立模块,天然隔离,无需额外配置。
  3. Tree-shaking 友好:Pinia 的 store 是函数调用生成的,构建工具可以静态分析哪些 store 被实际导入,未使用的 store 不会被打包。

二、Pinia Store 的三种定义方式

1. Options Store(类 Vuex 风格)

使用 stategettersactions 对象定义,适合熟悉 Vuex 语法或需要快速迁移的场景。

js
import { defineStore } from "pinia";

export const useCounterStore = defineStore("counter", {
  state: () => ({ count: 0, name: "Counter" }),
  getters: {
    doubleCount: (state) => state.count * 2,
    // 通过 this 访问其他 getter
    message: (state) => `${state.name} value: ${this.doubleCount}`,
  },
  actions: {
    increment() {
      this.count++;
    },
    async asyncIncrement() {
      await new Promise((resolve) => setTimeout(resolve, 1000));
      this.count++;
    },
  },
});

2. Setup Store(Composition API 风格)

使用 refreactivecomputed 等组合式 API 定义,推荐用于 Vue 3 项目,更灵活且与组合式 API 风格一致。

js
import { defineStore } from "pinia";
import { ref, computed } from "vue";

export const useCounterStore = defineStore("counter", () => {
  const count = ref(0);
  const name = ref("Counter");
  const doubleCount = computed(() => count.value * 2);
  function increment() {
    count.value++;
  }
  async function asyncIncrement() {
    await new Promise((resolve) => setTimeout(resolve, 1000));
    count.value++;
  }
  return { count, name, doubleCount, increment, asyncIncrement };
});

3. 对比分析:何时选用何种风格

特性Options StoreSetup Store
上手难度低,类似 Vuex中,需熟悉组合式 API
代码组织按类型分组(state/getters/actions)按逻辑分组,更接近业务
TypeScript 推导良好,但 getter 中 this 类型需注意极佳,完全自动推导
复用性较低,逻辑复用需借助 helper高,可直接组合其他 composables
迁移成本低,适合从 Vuex 迁移需改写代码,但更现代化

建议

  • 新项目优先选择 Setup Store,因为它与 <script setup> 风格一致,且更容易复用逻辑。
  • 若团队长期使用 Vuex 且迁移时间有限,可先用 Options Store 过渡。

三、跨 Store 组合与复用

1. Store 之间相互引用

一个 store 的 action 中可以导入并使用另一个 store。

js
// stores/user.js
export const useUserStore = defineStore("user", () => {
  const token = ref(null);
  const login = (newToken) => {
    token.value = newToken;
  };
  return { token, login };
});

// stores/cart.js
import { useUserStore } from "./user";
export const useCartStore = defineStore("cart", () => {
  const items = ref([]);
  async function checkout() {
    const userStore = useUserStore();
    if (!userStore.token) throw new Error("请先登录");
    // 执行下单逻辑...
  }
  return { items, checkout };
});

2. Pinia Store 与 Composables 结合的设计模式

  • 将 store 作为依赖注入到 composable
js
// useCartTotal.js
import { computed } from "vue";
import { useCartStore } from "@/stores/cart";
export function useCartTotal() {
  const cart = useCartStore();
  const total = computed(() => cart.items.reduce((sum, i) => sum + i.price, 0));
  return { total };
}
  • 在 composable 内部封装 store 的操作:例如带有本地加载状态的请求逻辑。

优势:业务逻辑与视图解耦,便于测试和复用。

四、状态持久化

pinia-plugin-persistedstate 是官方推荐的持久化插件,可自动将 store 状态保存到 localStoragesessionStorage

1. 插件配置策略

基础配置(保存整个 store):

js
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";

const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);

按路径持久化(仅保存部分字段):

js
export const useUserStore = defineStore("user", {
  state: () => ({ token: null, name: "", permissions: [] }),
  persist: {
    paths: ["token"], // 只持久化 token
    storage: sessionStorage, // 默认 localStorage
  },
});

Setup Store 中配置

js
export const useUserStore = defineStore(
  "user",
  () => {
    const token = ref(null);
    return { token };
  },
  { persist: { paths: ["token"] } },
);

2. 敏感数据处理:避免持久化 token 时的安全隐患

  • 不要持久化敏感信息:如密码、信用卡号等。
  • token 持续性考虑:若 token 设置较短有效期,持久化可能会导致 token 失效后应用仍认为已登录。解决方案:在 pinia 初始化时增加 token 有效性校验。
  • 加密存储:可自定义 storage 实现加密(如使用 crypto-js 对值进行 AES 加密后存入 localStorage)。
  • 搭配退出登录清理:在退出时主动 $reset() store 并清除存储。
js
// 手动清理持久化状态
const userStore = useUserStore();
userStore.$reset(); // 重置状态
localStorage.removeItem("pinia-user"); // 持久化插件存储的键名规则是 `pinia-${store.$id}`

五、Pinia 插件开发

Pinia 插件可以扩展 store 的功能,添加全局属性、监听状态变化等。

1. $subscribe 监听状态变化(用于日志、调试、自动保存)

在组件或插件中订阅:

js
cartStore.$subscribe((mutation, state) => {
  console.log("状态变化", mutation, state);
  localStorage.setItem("cart", JSON.stringify(state));
});

插件中统一为所有 store 添加订阅:

js
function persistPlugin(context) {
  const { store } = context;
  store.$subscribe((mutation, state) => {
    // 保存到本地
  });
}

2. $onAction 监听 action 调用

用于记录操作日志、性能追踪或添加全局 loading。

js
userStore.$onAction(({ name, store, args, after, onError }) => {
  console.log(`Action ${name} 开始`);
  after((result) => console.log(`Action ${name} 完成`, result));
  onError((error) => console.error(`Action ${name} 失败`, error));
});

3. 自定义插件注入全局功能(如请求去重、错误重试)

示例:为所有 store 注入 $fetchWithRetry 方法。

js
function retryPlugin({ store }) {
  store.$fetchWithRetry = async (url, options, retries = 3) => {
    for (let i = 0; i < retries; i++) {
      try {
        return await fetch(url, options);
      } catch (e) {
        if (i === retries - 1) throw e;
      }
    }
  };
}
pinia.use(retryPlugin);

六、响应式丢失处理:storeToRefs

reactive 对象类似,解构 Pinia store 会丢失响应性。必须使用 storeToRefs 提取响应式的 state/getters。

js
import { storeToRefs } from "pinia";
const cartStore = useCartStore();
// ❌ 错误:解构会导致响应式丢失
const { items, totalPrice } = cartStore;
// ✅ 正确:使用 storeToRefs
const { items, totalPrice } = storeToRefs(cartStore);
// actions 可以直接解构(因为它是函数,不需要响应性)
const { checkout } = cartStore;

原理storeToRefs 只提取 stategetters,并保证每个属性都是 ref 链接到 store 内部的值。

七、总结

维度Pinia 方案
核心设计删除 mutations,扁平化模块,TS 友好,Tree-shaking
Store 定义Options Store(过渡友好)、Setup Store(推荐 Vue 3)
跨 Store 组合直接 import 其他 store 并在 action 中使用
持久化pinia-plugin-persistedstate,支持按路径、自定义存储
插件开发$subscribe(状态)、$onAction(动作)、全局注入
响应式保持storeToRefs 解构 state/getters

新项目应直接使用 Pinia;Vuex 项目可按官方迁移指南逐步迁移(Pinia 亦提供 Vue 2 支持说明)。

参考文献

以下链接在编写时均可正常访问:

资料说明
Pinia 文档中文
Vuex 4 文档维护模式下的 Vuex
Pinia:从 Vuex 迁移迁移指南(英文)

Series

vue

12 / 16