《新项目用 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 可能更适合看审计粒度

相关新闻

【药物–靶标分子间作用力网络解析③】盐桥在药物设计中的双面作用

【药物–靶标分子间作用力网络解析③】盐桥在药物设计中的双面作用

引言 前三篇我们聊了氢键、卤键,今天来到第四篇——盐桥。如果说氢键是药物与蛋白之间的"精准对接",卤键是"冷门但高效的秘密武器",那盐桥就是这个作用力网络里"力量感最强"的一环。 为什么这么说&#xff1…

2026/10/26 17:55:25 阅读更多 →
Git误操作急救指南:20+场景恢复与预防策略

Git误操作急救指南:20+场景恢复与预防策略

1. Git误操作急救手册:开发者必备的版本控制生存指南每次看到同事在终端里疯狂敲git reflog时涨红的脸,我就知道又一个灵魂即将被版本控制系统折磨。作为分布式版本控制的标杆工具,Git在赋予我们强大能力的同时,也像一把双刃剑——…

2026/10/25 21:07:50 阅读更多 →
为什么“行业资源包+场景模板“能让数字孪生项目交付周期缩短40%

为什么“行业资源包+场景模板“能让数字孪生项目交付周期缩短40%

周期压缩不靠加班,靠的是新项目开工时,有一半的活儿已经在库里做完了。 交付周期能缩短 40%,核心机制是把项目里的重复劳动前置沉淀成可调用资产。22 个行业资源包解决"模型不用从零建",180 应用场景模板解决"业务…

2026/10/25 9:05:59 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/10/26 12:00:21 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/10/25 22:36:10 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/10/26 4:52:12 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/10/26 12:00:21 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/10/26 5:00:37 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/10/26 3:16:46 阅读更多 →

日新闻

周新闻

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/26 12:00:19 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/25 13:21:54 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/26 8:34:37 阅读更多 →