Pinia状态管理:模块化实践与持久化方案,避免不了?也可以。
Pinia 状态管理需解决 store 文件膨胀和刷新后状态丢失的问题,做法是按业务领域拆分独立 store,避免跨 store 直接修改 state,改用 action 调用并配合 storeToRefs 解构,持久化则可选用 pinia-plugin-persistedstate 等插件替代手写 localStorage,以减少同步、字段筛选和 SSR 兼容等隐患。
你有没有这种时刻:项目里 Pinia 的 store 文件越来越多,代码写得倒是爽,但回头一看,src/store 目录已经乱成一锅粥。更头疼的是,用户刷新页面,好不容易存进去的用户信息和偏好设置全没了,又要重新登录、重新配置。
这几乎是每一个 Vue 3 + Pinia 项目逃不掉的“成长痛”。今天这篇文章不端着,就从实际开发出发,聊聊 Pinia 的模块化怎么写才不拧巴,以及持久化这个“麻烦事”,到底是被动接受,还是另有解法。
模块化:别把 store 当成一个垃圾桶
很多人写 Pinia,脑子里还装着 Vuex 的全局思维——所有状态都塞进一个大大的 store 里。结果就是组件里引用的时候,代码长成 store.user.avatar、store.cart.count,看着难受,改着更难受。
Pinia 真正的爽点在于,它彻底打破了“单一状态树”的压迫感。模块化不是被迫分文件,而是按领域划分边界。我的建议是两个字:拆分。
每条业务线对应一个 store 文件,比如 useUserStore 管用户登录态和资料,useCartStore 管购物车,useAppStore 管全局 UI 状态(比如侧边栏收起/展开)。别嫌文件多,项目大了你就知道好处了。
模块化之后,最忌讳的事情是:在 A store 里去修改 B store 的 state。总觉得这样“省事”,一次性把逻辑写完了,实际上会让跨模块的隐式依赖越来越多,查 bug 的时候想摔键盘。
如果非要有跨模块操作,正确姿势是利用 action 之间的调用。在 Pinia 里,你完全可以 const userStore = useUserStore() 然后调用 userStore.someAction(),因为 store 是单例的。注意养成习惯:别在 setup 之外直接解构 store,需要用 storeToRefs 只解构 state 和 getters,action 直接解构即可。
// stores/user.ts
export const useUserStore = defineStore('user', () => {
// state
const token = ref('')
const profile = ref<UserProfile | null>(null)
// actions
const login = async (payload) => { ... }
const logout = () => { ... }
return { token, profile, login, logout }
})
// components/Login.vue
const userStore = useUserStore()
const { token, profile } = storeToRefs(userStore)
const { login } = userStore
持久化:官方插件省心,但也不是唯一解
状态管理聊到一半,永远绕不开“刷新就没了”的问题。很多人第一反应是不想引入插件,觉得配置麻烦。但现实是,持久化这件事,你自己手写 localStorage 方案,坑是真的多——啥时候该同步?什么字段该存?过期了怎么处理?再往后如果上了 SSR,还得考虑 localStorage 是否存在。
其实社区里已经有一个口碑很好的方案:pinia-plugin-persistedstate。这玩意儿是“无脑可用”,它会自动帮你在 store 里存到 localStorage 或 sessionStorage。最惊艳的是它支持按 store 单独配置,不想持久化的 store 压根不用理会,一点侵入感都没有。
// main.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export const useUserStore = defineStore('user', {
state: () => ({ token: '', refreshToken: '' }),
// 只持久化 token,不清空 refreshToken
persist: {
key: 'user-token',
pick: ['token'],
storage: sessionStorage,
}
})
特别是 pick 和 paths 这种可选择持久化字段的设计,可以直接过滤掉 isLoading 之类的临时状态,干净利落。
但注意,这里有一个很重要的开发习惯:持久化依赖的是 JSON 序列化和反序列化,所以 store 里尽量不要存不可序列化的对象(Map、Date 等等),否则刷新回来数据会变形,出 bug 的时候非常蒙圈。
不引插件,DIY 持久化可以吗?
有些团队是有意见的——什么增加一个依赖只是为了存点 token?这不对吧?可以,也确实可以。
顺手写一个 setup store 的自己动手方案。利用 store 的 $subscribe,它比 watch 更可靠,因为它可以监听到 state 的所有变化(包括 mutations 之外的直接赋值)。搭配一个 storage 的自动序列化工具,十几行代码也能把持久化做了。
export const useSettingsStore = defineStore('settings', () => {
const theme = ref('light')
const fontSize = ref(14)
function updateTheme(val: string) { theme.value = val }
function updateFontSize(val: number) { fontSize.value = val }
return { theme, fontSize, updateTheme, updateFontSize }
})
// 在应用启动时初始化一次
const settingsStore = useSettingsStore()
settingsStore.$subscribe((mutation, state) => {
localStorage.setItem('app-settings', JSON.stringify(state))
})
这个方案的缺点也很明确:写的逻辑很粗,没有插件的容错和版本管理,也无法轻松应对复杂 project。你可以理解为这是一个“轻量可跑、后续可能踩坑”的方案。但不得不承认,对于超小型项目,自己写更显真功夫。
写在最后
技术选型这件事,说到底是取舍的艺术。很多问题并不是“某种技术能不能解决”,而是“我们用哪一种方式解决时,后续的维护成本最低”。
就我个人的经验而言,中小型项目直接用 pinia-plugin-persistedstate 就好,省心省力;如果项目简单到爆,或者你暂时还没有引插件的计划,试着自己写个 $subscribe 的极简方案也未尝不可,至少比在代码里到处撒 localStorage.setItem 要强太多。
记住一个原则:不为了用插件而用插件,但也不要为了证明技术能力,去刻意重新发明轮子。搞定问题、跑得顺畅、看得清楚,模块化加持久化就落了一半地。
转载请注明出处,版权归原作者所有。
星耀SVIP






