Pinia状态管理:模块化实践与持久化方案,避免不了?也可以。
你有没有这种时刻:项目里 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` 要强太多。
记住一个原则:不为了用插件而用插件,但也不要为了证明技术能力,去刻意重新发明轮子。搞定问题、跑得顺畅、看得清楚,模块化加持久化就落了一半地。
转载请注明出处,版权归原作者所有。
管理员
黑卡会员