Vue3 出来后,Pinia 成了"官方推荐状态管理",文档清爽,API 看上去现代化,我第一时间把原项目从 Vuex 切成了 Pinia。
起初一切顺利------setup 写法简洁,类型提示优秀,组件用起来也比老 Vuex 舒服。
但到了项目第 2 个月,我开始频繁在脑中冒出一句话:
"我当初为什么要换掉 Vuex?"
Pinia 真正的问题:看上去简单,实际不适合复杂项目
一开始的惊艳 来自它那些极简 API:defineStore
、state
、actions
、getters
。
你可以这么写一个模块:
javascript
export const useUserStore = defineStore('user', {
state: () => ({ name: '', token: '' }),
actions: {
login(payload) {
// 调接口,然后存数据
}
}
})
你在组件中就这么用:
php
const user = useUserStore()
user.login({ name: 'jack' })
到这里一切都很美好,直到你开始做更复杂的事:
- 跨模块共享状态 + 联动逻辑
- 动态模块加载 / 清除(例如用户退出登录后要清空 store)
- 服务端预注入数据 SSR
- 多实例 store(例如 tab 页中的独立状态)
Pinia 的那套简洁 API,突然变得不够用了,而你开始手写一堆「自己想不到的 hack」。
动态注册?对不起,Pinia 不支持模块热插拔
Vuex 模块可以 registerModule
/ unregisterModule
,你可以根据场景动态挂载模块:
arduino
store.registerModule('user', userModule)
比如后台管理系统中,某些页面权限不同,挂载不同 store 模块是一种必要手段。
而在 Pinia 中?模块一旦定义就永久注册,没有取消注册的 API。
这意味着:
- 退出登录后你清不了用户模块,得手动
reset()
(还不能漏) - tab 页中有多个用户表单时,状态会互相污染,无法多实例隔离
- 想动态挂载模块,自己实现生命周期管理、状态清除、命名空间管理...
你看似在用现代化 API,实则在为它造一个你原本不需要造的轮子。
跨模块依赖 + actions 调用,完全无管理
Vuex 中你可以:
scss
dispatch('moduleA/doSomething')
在 Pinia 中?你只能:
scss
const a = useModuleA()
a.doSomething()
这没有命名空间、没有 action 显式调用路径、没有中间层统一 dispatch,调用链在大型项目中完全不可控。
最终你得到的是:
- 不清楚某个行为被哪些模块调用
- 不知道模块调用是否存在循环依赖
- 不好加日志、埋点、权限校验、接口 mock
组件间的调用关系被分散到了文件内部,调试时你面对的不是"一个中心化的 store 流程",而是一堆互相引用的 setup 函数。
SSR 是个伪支持:真正复杂的服务端注入场景会爆炸
Pinia 声称"支持 SSR"。但这支持是浅层的。
你试试下面这个场景:
- SSR 服务端拉取完用户信息,注入 store 中
- 客户端 hydration 要同步这些状态
- 多个模块状态之间有依赖,还得监听响应式变化
你需要用 pinia.state.value
做手动注入,还得处理每个 store 的独立实例,完全没有官方指导,也不封装中间件。
而在 Vuex 中,至少你可以通过 store.replaceState()
直接更新所有状态,它生来就是中心化的设计。
Pinia 到现在都没有一个像 Vuex 的 plugin / middleware 一样的机制,它并不适合复杂业务和团队协作。
最后压死骆驼的稻草:store 的封装性极差
你做中大型项目,一定会写很多"带状态的业务逻辑":
- 弹窗的打开关闭 + 表单数据
- 分页表格的查询条件 + loading 状态 + 总条数
- 页面缓存数据,用于页面跳转回来后还原
这些在 Vuex 中,你可以写一个 namespaced module,定义状态、mutations、actions,很清晰。
而在 Pinia 中?你得这么做:
javascript
const useSearchStore = defineStore('search', {
state: () => ({
keyword: '',
currentPage: 1,
pageSize: 20,
list: []
}),
actions: {
async fetchList() {
// ...
}
}
})
然后组件用 useSearchStore。问题是:
- 这些状态是全局的!你不能开两个页面用两个 store 实例
- 模块是单例,状态复用完全靠你手写 reset、clear 之类的方法
- 想自动恢复状态?想要模块有生命周期?你自己写吧
最终你会发现:Pinia 根本不适合处理非全局性状态,但我们大多数页面状态,恰恰是局部的。
所以我回到了 Vuex
是的,我删掉了 pinia
,重新把项目切回了 Vuex。
回去的理由其实很简单:
- Vuex 是中心化的,有结构,有规范,有治理手段
- 支持动态模块,适合按需加载、缓存管理、页面还原
- 插件机制健全,做埋点、权限、日志都能统一处理
- 多人协作时,有命名空间,函数调用清晰
别误会,我不是说 Vuex 完美无缺。
我只是意识到,Pinia 解决的是 Vue2 setup 写法的痛点,不是业务复杂度的痛点。
总结:Pinia 很酷,但它不是状态管理的答案
如果你项目是:
- 很小的后台项目
- 状态非常简单
- 不考虑多模块、用户权限、模块热切换、数据注入
那么 Pinia 当然可以用,甚至用组合式函数都能代替。
但如果你是:
- 多 tab 页面
- 状态要可挂载、可销毁
- 有登录登出、缓存还原、行为跟踪
那请远离 Pinia。
因为你迟早会写出一套比 Vuex 还复杂的补丁系统,只为弥补它的"不支持"。