《新项目用 Pinia 还是守 Vuex?一份不废话的选型清单》

发布时间:2026/10/12 2:16:22

《新项目用 Pinia 还是守 Vuex?一份不废话的选型清单》 关键词技术选型、迁移成本、遗留系统、审计合规、Composition API 团队适配适用场景知乎回答、团队 Wiki、架构评审材料、技术负责人复盘被问Vuex 和 Pinia 选哪个时别急着背Pinia 官方推荐。选型从来不是选最好的是选最贴合当前工程约束的。下面直接给判断矩阵每个观点配真实代码对比。一、闭眼选 Pinia 的 4 种情况1. Vue 3 从零起步的项目官方立场已经锁死新 Vue 3 应用直接用 PiniaVuex 4 不再加新特性。这时候坚持 Vuex 等于主动放弃 TS 推断和 HMR 体验。举例一个简单的计数器模块Vuex 4 写法新项目还要这么写就太累了// store/index.js - Vuex 4 import { createStore } from vuex export default createStore({ state: { count: 0, history: [] }, mutations: { SET_COUNT(state, payload) { state.count payload }, PUSH_HISTORY(state, payload) { state.history.push(payload) } }, actions: { increment({ commit, state }) { const newVal state.count 1 commit(SET_COUNT, newVal) commit(PUSH_HISTORY, 1 → ${newVal}) }, async fetchCount({ commit }) { const res await axios.get(/api/count) commit(SET_COUNT, res.data) } }, getters: { doubleCount: (state) state.count * 2 } })Pinia 写法清爽到不需要注释// stores/counter.js - Pinia import { defineStore } from pinia import axios from axios export const useCounterStore defineStore(counter, () { const count ref(0) const history ref([]) function increment() { count.value history.value.push(1 → ${count.value}) } async function fetchCount() { const res await axios.get(/api/count) count.value res.data } const doubleCount computed(() count.value * 2) return { count, history, increment, fetchCount, doubleCount } })关键差异Vuex改一个 count 要经过 mutation → action 两层异步还得单独处理Pinia直接写函数同步异步一视同仁代码量少 40%2. TypeScript 覆盖率高的工程Pinia 的类型是定义即生效Vuex 的 TS 是配置即劝退。举例用户信息 storeVuex 4 TS 的痛苦日常// store/user.ts - Vuex 4 interface UserState { name: string age: number role: admin | editor | viewer } // 第一步定义类型 type UserStore { state: UserState getters: { userDisplay: (state: UserState) string } mutations: { SET_USER: (state: UserState, payload: PartialUserState) void } actions: { updateUser: (payload: PartialUserState) Promisevoid } } // 第二步手动声明 RootState declare module vue/runtime-core { interface ComponentCustomProperties { $store: Store{ user: UserState } } } // 第三步组件里还得断言 const store useStore() const userName (store.state as any).user?.name // 类型丢失Pinia 的天然推断// stores/user.ts - Pinia import { defineStore } from pinia interface UserState { name: string age: number role: admin | editor | viewer } export const useUserStore defineStore(user, { state: (): UserState ({ name: , age: 0, role: viewer }), getters: { userDisplay(): string { return ${this.name} (${this.role}) // this 自动推断 } }, actions: { async updateUser(payload: PartialUserState) { await api.updateUser(payload) Object.assign(this, payload) // this.$state 也有完整类型 } } }) // 组件里直接用无需任何额外声明 const store useUserStore() console.log(store.name) // string ✓ console.log(store.role) // admin | editor | viewer ✓ store.updateUser({ name: Tom }) // 参数类型校验 ✓实际踩坑案例某团队在 Vuex 项目中有一个user.permissions字段由于 TS 类型定义不到位上线后才发现permissions在某些分支下是undefined导致页面白屏。同样的逻辑用 Piniastate定义时就会强制你处理好初始值和可选类型。3. 重度用 Composition API /script setupPinia 的useXStore()在 setup 里和ref/computed同构。举例一个购物车组件Vuex 在script setup里的别扭写法script setup import { useStore } from vuex import { computed } from vue const store useStore() // 读取状态 - 每次都要通过 store.state const cartItems computed(() store.state.cart.items) const totalPrice computed(() store.state.cart.total) // 触发动作 - 字符串魔法 function addItem(item) { store.dispatch(cart/addItem, item) } function removeItem(id) { store.commit(cart/REMOVE_ITEM, id) } /scriptPinia 的自然写法script setup import { useCartStore } from /stores/cart import { storeToRefs } from pinia const cartStore useCartStore() // 解构出响应式数据 const { items, totalPrice } storeToRefs(cartStore) // 方法直接调用 function addItem(item) { cartStore.addItem(item) } function removeItem(id) { cartStore.removeItem(id) } /script template !-- 直接用不用加 store.state 前缀 -- div v-foritem in items :keyitem.id {{ item.name }} - ¥{{ item.price }} /div p总计¥{{ totalPrice }}/p button clickaddItem(newItem)加入购物车/button /template关键差异Vuexstore.state.cart.items这种三级路径在模板里反复出现且dispatch用字符串容易拼错Pinia就像在用本地refIDE 补全、重构、跳转定义全都支持4. 中大型项目但要扁平分包Pinia 的一个文件一个 store比 Vuex 的 namespaced modules 清爽太多。举例电商后台管理系统涉及商品、订单、用户三个业务域Vuex 的模块嵌套噩梦// store/index.js - Vuex 4 多模块 import { createStore } from vuex import products from ./modules/products import orders from ./modules/orders import users from ./modules/users export default createStore({ modules: { products, // 内部可能还有子模块products.list, products.detail orders, // 子模块orders.list, orders.refund users // 子模块users.profile, users.permissions } }) // 组件里调用时的路径地狱 store.dispatch(products/list/fetchProducts) store.dispatch(orders/refund/submitRefund) store.commit(users/profile/SET_NAME, Tom) // 跨模块引用需要 rootState // orders/store.js 里要拿用户信息 actions: { submitOrder({ rootState }) { const userId rootState.users.profile.id // ... } }Pinia 的扁平结构stores/ ├── productStore.ts // 商品相关 ├── orderStore.ts // 订单相关 └── userStore.ts // 用户相关// stores/productStore.ts - Pinia export const useProductStore defineStore(product, () { const list refProduct[]([]) const detail refProductDetail | null(null) async function fetchList() { /* ... */ } async function fetchDetail(id: string) { /* ... */ } return { list, detail, fetchList, fetchDetail } }) // stores/orderStore.ts - 跨 store 引用 import { useUserStore } from ./userStore export const useOrderStore defineStore(order, () { const userStore useUserStore() async function submitOrder(orderData: Order) { // 直接拿用户信息不用 rootState const userId userStore.profile.id await api.submit({ ...orderData, userId }) } return { submitOrder } })实际效果某中型电商项目从 Vuex 迁到 Pinia 后store 文件数从 12 个含嵌套变成 8 个扁平文件新人理解时间从 2 天缩短到半天。二、继续用 Vuex 不丢人的 3 种情况1. Vue 2 存量系统举例一个运行两年的 OA 系统现有 Vuex store// store/modules/attendance.js - Vuex 3Vue 2 export default { namespaced: true, state: { records: [], todayStatus: {} }, mutations: { SET_RECORDS(state, records) { state.records records }, SET_TODAY_STATUS(state, status) { state.todayStatus status } }, actions: { async fetchRecords({ commit }, date) { const data await api.getAttendance(date) commit(SET_RECORDS, data) } } }如果要迁到 Pinia需要安装vue/composition-apiVue 2 的桥接包安装 pinia注意版本兼容性逐个 store 重写测试回归组件里所有mapState、mapActions替换实际案例某团队花了 3 天评估迁移发现 200 个组件引用了 store最终决定不迁。结论是功能稳定、无性能瓶颈、无新增大需求不动就是最优解。2. 金融/审计类业务强制 mutation 留痕举例资金交易系统每次余额变动必须记录操作人、时间、IPVuex 的 mutation 天然适合做审计日志// store/modules/account.js - Vuex 4 export default { state: { balance: 10000, auditLog: [] }, mutations: { // 所有余额变动必须走这里 UPDATE_BALANCE(state, { amount, operator, ip, reason }) { state.auditLog.push({ before: state.balance, after: state.balance amount, amount, operator, ip, reason, timestamp: Date.now() }) state.balance amount } }, actions: { withdraw({ commit }, { amount, operator, ip }) { // 审核逻辑... commit(UPDATE_BALANCE, { amount: -amount, operator, ip, reason: 提现 }) } } }Pinia 要实现同样效果只能靠约定// stores/account.ts - Pinia export const useAccountStore defineStore(account, () { const balance ref(10000) const auditLog refAuditEntry[]([]) // 全靠自觉每次改 balance 前手动记录日志 function updateBalance(amount: number, meta: AuditMeta) { auditLog.value.push({ before: balance.value, after: balance.value amount, ...meta, timestamp: Date.now() }) balance.value amount } function withdraw(amount: number, meta: AuditMeta) { updateBalance(-amount, { ...meta, reason: 提现 }) } return { balance, auditLog, withdraw } })关键差异Vuexmutation 是唯一的修改入口审计是强制性的Pinia可以绕过updateBalance直接balance.value x审计失效实际案例某支付公司规定所有资金变动必须经过 mutation 审计他们选择继续用 Vuex并且在 CI 阶段加了 lint 规则禁止直接修改 state。3. 深度依赖 Vuex 插件生态举例一套基于 mutation 的埋点系统和权限中间件Vuex 插件示例// plugins/tracker.js - Vuex 插件 export function createTrackerPlugin(options) { return (store) { // 监听所有 mutation store.subscribe((mutation, state) { analytics.track(state_change, { type: mutation.type, payload: mutation.payload, timestamp: Date.now() }) }) } } // plugins/permission.js - 权限拦截 export function createPermissionPlugin(allowedModules) { return (store) { store.subscribeAction((action, state) { if (!allowedModules.includes(action.type.split(/)[0])) { console.warn(Blocked action: ${action.type}) throw new Error(Permission denied) } }) } } // 使用 const store createStore({ plugins: [ createTrackerPlugin(), createPermissionPlugin([products, orders]) ] })Pinia 没有 mutation上述插件机制全部失效。如果要迁移埋点需要在每个 action 里手动调用 analytics权限需要在每个 store 的 action 开头加检查逻辑实际案例某 SaaS 平台有 6 个自定义 Vuex 插件缓存、埋点、权限、错误上报、数据同步、撤销恢复迁移成本估算超过 2 周最终决定等下一个大版本重构时一并处理。三、迁移值不值看边界清晰度中型 Vue 3 项目从 Vuex 迁 Pinia典型节奏是 1–3 天以一个真实的任务管理系统为例迁移前后对比迁移前Vuex// store/modules/tasks.js export default { namespaced: true, state: { list: [], loading: false, filter: all }, mutations: { SET_LIST(state, list) { state.list list }, SET_LOADING(state, v) { state.loading v }, SET_FILTER(state, f) { state.filter f }, ADD_TASK(state, task) { state.list.push(task) }, REMOVE_TASK(state, id) { state.list state.list.filter(t t.id ! id) }, TOGGLE_COMPLETE(state, id) { const task state.list.find(t t.id id) if (task) task.completed !task.completed } }, actions: { async fetchTasks({ commit }) { commit(SET_LOADING, true) const data await api.getTasks() commit(SET_LIST, data) commit(SET_LOADING, false) }, async addTask({ commit }, title) { const task await api.createTask(title) commit(ADD_TASK, task) } }, getters: { filteredTasks(state) { if (state.filter completed) return state.list.filter(t t.completed) if (state.filter active) return state.list.filter(t !t.completed) return state.list }, pendingCount(state) { return state.list.filter(t !t.completed).length } } }迁移后Pinia// stores/tasks.ts export const useTaskStore defineStore(tasks, () { const list refTask[]([]) const loading ref(false) const filter refall | active | completed(all) async function fetchTasks() { loading.value true list.value await api.getTasks() loading.value false } async function addTask(title: string) { const task await api.createTask(title) list.value.push(task) } function removeTask(id: string) { list.value list.value.filter(t t.id ! id) } function toggleComplete(id: string) { const task list.value.find(t t.id id) if (task) task.completed !task.completed } const filteredTasks computed(() { if (filter.value completed) return list.value.filter(t t.completed) if (filter.value active) return list.value.filter(t !t.completed) return list.value }) const pendingCount computed(() list.value.filter(t !t.completed).length) return { list, loading, filter, fetchTasks, addTask, removeTask, toggleComplete, filteredTasks, pendingCount } })迁移信号绿不绿看三点1. store 里 mutation 是不是大多只有一行赋值如果是像上面这个例子6 个 mutation 全是简单赋值Pinia 直接省掉 60% 的样板代码。2. 新人入职要不要讲一小时 Flux 概念某团队统计新人理解 Vuex 的 mutations/actions 区分平均需要 2 天而 Pinia 只需要 30 分钟。如果你的团队有持续的新人 onboarding 压力Pinia 节省的时间非常可观。3. TS 报错是不是长期靠any糊弄比如这个常见场景// Vuex 里常见的摆烂写法 const tasks (store.state as any).tasks.list const user (store.getters as any).userInfo换成 Pinia 后const taskStore useTaskStore() const tasks taskStore.list // Task[] ✓ const userStore useUserStore() const userInfo userStore.userInfo // UserInfo ✓实际案例某团队迁移后TS 相关 bug 减少了 70%因为之前很多运行时错误其实是类型不对导致的。四、一句话收尾Vuex 是把状态关进 Flux 的笼子里Pinia 是把笼子拆了因为 Vue 3 的响应式本身就是笼子。新项目别犹豫Pinia老 Vue 2 别折腾VuexVue 3 老项目看上面那张绿灯表再动手。最后的决策清单你的情况建议新项目Vue 3 TS✅ 闭眼 Pinia新项目Vue 3 JS✅ 依然 Pinia少写一半代码老项目Vue 2❌ 不动除非重构老项目Vue 3mutation 全是赋值✅ 迁1-2天搞定老项目Vue 3有复杂插件/审计需求⚠️ 评估后再动可能不值得团队全是新手✅ Pinia降低学习成本金融/合规要求严格⚠️ Vuex 可能更适合看审计粒度
延伸阅读

更多相关文章

2026/10/6 22:36:48

输入法词库定制与开发环境术语集成实战指南

你的名字总会被别人说错,而我的输入法里固定了你的名字。这句话听起来像一句浪漫的告白,但在技术人的世界里,它指向一个更实际、更普遍的痛点:如何让计算机“记住”并“理解”那些它不认识的、容易出错的专有名词?无论…

2026/10/11 13:26:43

Java Spring -- AOP详解

一. AOP概念 1.1 什么是 AOP? AOP 的全称是 Aspect-Oriented Programming,即面向切面编程。如果说面向对象编程(OOP)是将系统纵向划分为一个个独立的模块(如用户模块、订单模块),那么AOP 就是将…

2026/10/10 5:55:14

数据集转换技术:核心原理与工业级实践指南

1. 数据集转换的核心价值与应用场景在数据科学和机器学习领域,Dataset_transform(数据集转换)是数据预处理流程中最为关键的环节之一。我处理过上百个真实项目的数据集,90%的模型效果问题都源于数据转换不当。简单来说&#xff0c…

2026/10/12 2:14:31

具身智能创新原理(69):跨模态语义对齐的架构动态参照系建模研究

前沿技术探索:TVA智能体(简称TVA) TVA智能体(亦称“AI智能体视觉”)是依托Transformer架构与“因式智能体”理论构建的通用视觉技术体系。它有机融合深度强化学习(DRL)、卷积神经网络(CNN)与因式分解算法(FRA),构成了具身智能的核心视觉中枢(详见官方技术平台www…

2026/10/12 2:14:31

SQL Server建库到视图全流程:约束、索引与备份避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 2:14:31

游戏引擎架构:对象与资源管理核心机制与实战避坑指南

1. 从一次内存泄漏说起:为什么游戏对象管理值得单独拎出来讲前阵子帮一个独立团队看他们的项目,游戏跑到第三关帧率突然从60掉到22,用性能分析工具一抓,发现场景里堆了四千多个已经"死亡"的敌人对象,每个还挂…

2026/10/12 2:14:31

游戏引擎中的对象与资源管理:从生命周期到加载释放

搞引擎的人基本都躲不过这两件事:对象怎么管,资源怎么加载。我在项目里见过太多“在编辑器里跑得好好的,一打包就崩”的情况,十有八九不是对象生命周期错乱,就是资源加载时机不对。这期我接着游戏引擎架构系列&#xf…

2026/10/12 2:09:31

EMC结构设计:缝隙、开孔与搭接如何决定屏蔽效能

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 0:02:13

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/11 0:02:13

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/11 0:02:13

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/12 0:04:22

绝缘子缺陷检测数据集清洗与工业级训练实战指南

简介:本资源是面向电力AI研发人员、工业视觉工程师及智能巡检系统开发者的绝缘子缺陷检测专用YOLO格式数据集,解决无人机航拍场景下绝缘子破损、污闪、积雪等9类典型缺陷的精准识别与定位难题。数据集共2139张真实巡检图像(含训练/验证/测试集…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑