Vue3状态管理的项目复盘:Pinia从引入到深度使用的踩坑与最佳实践
一、从Vuex到Pinia的为什么
项目经历了Vue2+Vuex → Vue3+Pinia的迁移。转Pinia的原因不是"Vuex不好",而是"Vuex在Vue3中失去了优势":
- Vuex 4的类型推导依然很弱(依赖字符串类型的action名称)
- Mutation的概念在Vue3 Composition API中显得多余(直接修改state)
- 模块嵌套在Vuex中通过
namespaced: true实现,出了名的难用
Pinia的对比优势:完整的TypeScript支持(不需要额外类型声明)、setup store写法更接近Composition API、不需要mutation概念。
二、Pinia的最佳实践
写法选择:Options Store vs Setup Store
// Options Store —— 类似Vuex,概念清晰 export const useUserStore = defineStore('user', { state: () => ({ name: '', token: '', }), getters: { isLoggedIn: (state) => !!state.token, }, actions: { async login(credentials: Credentials) { const res = await api.login(credentials); this.token = res.token; this.name = res.name; }, }, }); // Setup Store —— 类似Composition API,更灵活 export const useUserStore = defineStore('user', () => { const name = ref(''); const token = ref(''); const isLoggedIn = computed(() => !!token.value); async function login(credentials: Credentials) { const res = await api.login(credentials); token.value = res.token; name.value = res.name; } function logout() { token.value = ''; name.value = ''; } return { name, token, isLoggedIn, login, logout }; });推荐Setup Store——在复杂Store中可以自由使用watch、watchEffect、组合式函数,灵活度更高。
Store的拆分粒度:
// 不好——一个巨大Store const useAppStore = defineStore('app', () => { // 用户 + 权限 + 主题 + 通知 + 设置... 全部在一个Store }); // 好——按领域拆分 const useAuthStore = defineStore('auth', () => {}); const usePermissionStore = defineStore('permission', () => {}); const useThemeStore = defineStore('theme', () => {});跨Store引用:
// permission store 引用 auth store export const usePermissionStore = defineStore('permission', () => { const authStore = useAuthStore(); // 在action/内部函数中调用,不在顶层 const hasPermission = (code: string) => { if (!authStore.isLoggedIn) return false; return permissions.value.includes(code); }; return { hasPermission }; });三、踩过的坑
坑1:Store在setup之外使用报错
在Vue组件之外(如router guard、axios interceptor)需要ensure正确安装:
// router/guards.ts import { useAuthStore } from '@/stores/auth'; router.beforeEach((to) => { // 错误!pinia可能还未安装 const authStore = useAuthStore(); }); // 正确——延迟获取 router.beforeEach((to) => { const pinia = createPinia(); // 或从app获取已安装的实例 const authStore = useAuthStore(pinia); });坑2:解构导致的响应式丢失
// 错误——解构后失去响应式 const { name, token } = useUserStore(); // 正确——使用storeToRefs保持响应式 import { storeToRefs } from 'pinia'; const { name, token } = storeToRefs(useUserStore()); // actions不需要storeToRefs——它们不是响应式的 const { login } = useUserStore();坑3:$reset不会重置所有状态
$reset()只重置state()中返回的初始值。通过ref()定义的局部变量不会被重置(Setup Store)。需要手动实现reset:
export const useFormStore = defineStore('form', () => { const formData = ref<FormData>({}); function $reset() { formData.value = {}; } return { formData, $reset }; });坑4:持久化插件的选择
import { createPinia } from 'pinia'; import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; const pinia = createPinia(); pinia.use(piniaPluginPersistedstate); // 在Store中启用持久化 export const useAuthStore = defineStore('auth', () => { const token = ref(''); return { token }; }, { persist: { key: 'auth-token', storage: localStorage, pick: ['token'], // 只持久化token,不保存其他状态 }, });四、迁移的数据
Vuex → Pinia迁移:修改了47个文件,耗时约3周。
| 指标 | Vuex | Pinia |
|---|---|---|
| 代码行数(状态管理部分) | 3200 | 1800 |
| 新增Store的开发速度 | 慢 | 快 |
| TypeScript类型安全 | 60% | 95% |
| 运行时错误(与状态相关) | 8个/月 | 1个/月 |
运行时错误的显著下降主要来自:Pinia的TypeScript类型检查在编译时就能发现"访问了不存在的state属性"或"传错了action参数"。
五、总结
Pinia的实践经验:
- Setup Store vs Options Store——推荐Setup Store,灵活度更高,尤其是复杂场景
- 按业务领域拆分Store——每个Store专注一个领域
storeToRefs保持解构后的响应式——最容易被忽略的坑- 持久化插件按需使用——只保存需要跨页面保持的数据(如token)
- Store间引用要谨慎——只允许下层Store引用上层(如permission引用auth)
Vuex到Pinia的迁移性价比很高——1800行代码替代3200行,减少44%,TypeScript支持从"能用"到"无缝"。最大的提升不是代码量,而是开发体验——添加一个新状态不需要在5个地方(state/getter/mutation/action/component)修改,只需要在Store定义+组件使用两处改动。