状态管理:Pinia 与 Vuex
Pinia 是 Vue 官方推荐的状态管理库(Vue 3 新项目默认选型)。Vuex 4 仍可与 Vue 3 配合,用于维护旧项目。见 Vue 基础。
Pinia 是 Vue 官方新一代状态管理库,专为 Vue 3 设计,继承了 Vuex 的优点并简化了其复杂性。相比 Vuex,Pinia 提供了更直观的 API、更好的 TypeScript 支持和更小的打包体积。
一、Pinia 的核心设计差异
| 特性 | Vuex 4 | Pinia |
|---|---|---|
| mutations | 必须使用 mutations 修改状态,actions 用于异步操作 | 删除 mutations,同步/异步统一在 actions 处理 |
| 模块化 | 需要 modules + namespaced: true 实现隔离 | 扁平化:每个 store 独立定义,自动隔离 |
| TypeScript 支持 | 需要手动定义复杂类型,类型推导较弱 | 一流 TS 支持,自动推导大部分类型 |
| Tree-shaking | 无法 tree-shake,未使用的模块也会打包 | Tree-shaking 友好,未使用的 store 自动剔除 |
| 插件扩展 | 通过复杂 API 注册插件 | 简单插件机制,支持响应式状态扩展 |
关键差异解读:
- 删除 mutations:Vuex 中同步修改必须 commit mutation,异步操作必须 dispatch action,这导致了代码冗余和认知负担。Pinia 直接取消 mutations,所有状态修改都在 actions 中完成(无论是同步还是异步),简化了心智模型。
- 扁平化模块:Vuex 的模块嵌套容易导致命名冲突,需要手动添加
namespaced: true。Pinia 的每个 store 都是独立模块,天然隔离,无需额外配置。 - Tree-shaking 友好:Pinia 的 store 是函数调用生成的,构建工具可以静态分析哪些 store 被实际导入,未使用的 store 不会被打包。
二、Pinia Store 的三种定义方式
1. Options Store(类 Vuex 风格)
使用 state、getters、actions 对象定义,适合熟悉 Vuex 语法或需要快速迁移的场景。
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 风格)
使用 ref、reactive、computed 等组合式 API 定义,推荐用于 Vue 3 项目,更灵活且与组合式 API 风格一致。
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 Store | Setup 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。
// 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:
// 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 状态保存到 localStorage 或 sessionStorage。
1. 插件配置策略
基础配置(保存整个 store):
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
按路径持久化(仅保存部分字段):
export const useUserStore = defineStore("user", {
state: () => ({ token: null, name: "", permissions: [] }),
persist: {
paths: ["token"], // 只持久化 token
storage: sessionStorage, // 默认 localStorage
},
});
Setup Store 中配置:
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 并清除存储。
// 手动清理持久化状态
const userStore = useUserStore();
userStore.$reset(); // 重置状态
localStorage.removeItem("pinia-user"); // 持久化插件存储的键名规则是 `pinia-${store.$id}`
五、Pinia 插件开发
Pinia 插件可以扩展 store 的功能,添加全局属性、监听状态变化等。
1. $subscribe 监听状态变化(用于日志、调试、自动保存)
在组件或插件中订阅:
cartStore.$subscribe((mutation, state) => {
console.log("状态变化", mutation, state);
localStorage.setItem("cart", JSON.stringify(state));
});
插件中统一为所有 store 添加订阅:
function persistPlugin(context) {
const { store } = context;
store.$subscribe((mutation, state) => {
// 保存到本地
});
}
2. $onAction 监听 action 调用
用于记录操作日志、性能追踪或添加全局 loading。
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 方法。
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。
import { storeToRefs } from "pinia";
const cartStore = useCartStore();
// ❌ 错误:解构会导致响应式丢失
const { items, totalPrice } = cartStore;
// ✅ 正确:使用 storeToRefs
const { items, totalPrice } = storeToRefs(cartStore);
// actions 可以直接解构(因为它是函数,不需要响应性)
const { checkout } = cartStore;
原理:storeToRefs 只提取 state 和 getters,并保证每个属性都是 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 迁移 | 迁移指南(英文) |
相关文章
资源优化
图片懒加载、按需引入与 Web Vitals 监控。见 HTML 图片、CSS 字体优化。
Vue 生态关键库深度集成
VueUse、Vite 与 Nuxt 是 Vue 3 生态中常用的效率与工程化方案。概览见 Vue 基础。
依赖收集与派发更新机制
说明 Vue 3 中 track / trigger 与 targetMap 的工作方式。前置:v2-v3 差异。
测试策略
Vue 应用常用 Vitest + @vue/test-utils 做单元/组件测试,Playwright 或 Cypress 做 E2E。见 Vue 基础。
组合式 API 与逻辑复用设计模式
组合式 API 与 Composables 是 Vue 3 推荐的逻辑组织方式。概览见 Vue 基础。
TypeScript 深度集成
Vue 3 源码使用 TypeScript 编写, 可获得良好的类型推导。见 Vue 基础。