Pinia 状态管理:模块化、持久化与"权限联动"落地
很多项目上 Pinia 不难用,但容易用成两种极端:
- 全部状态都塞进 store,组件越来越"胖"
- store 只存 token,其它状态各自维护,协作成本变高
这篇按"项目落地"的方式讲 Pinia:
- store 如何分模块(领域边界)
- state/ getter / action 如何设计(可维护)
- 持久化存什么、不存什么(安全与一致性)
- 与 Axios 拦截器、权限路由怎么联动成闭环
目标是:写出来的 store 不仅能跑,还能在项目迭代中长期稳定。
1. Pinia 在项目里解决什么问题
- 跨页面共享状态:登录态、用户信息、字典、主题配置
- 减少 props/emit 链路:避免层层传递
- 统一副作用入口:把"请求 + 缓存 + 失效"放在 store action
2. store 怎么分模块(建议的边界)
按"领域"拆,而不是按"页面"拆:
useUserStore:token、用户信息、角色/权限、登出useAppStore:主题/布局、侧边栏折叠、全局 loadinguseDictStore:字典缓存(下拉框、枚举)
避免:
- 为每个页面新建一个 store(复用差、维护重)
3. 选项式 vs 组合式:先统一团队写法
Pinia 支持两种写法:
- 选项式(Options Store):更像 Vuex,结构清晰
- 组合式(Setup Store):更贴近 Composition API,可自由组合逻辑
建议:团队里统一一种写法,避免同一项目两套风格混用。
4. 一个实用的 user store 形态(建议做成"登录态权威源")
ts
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
userInfo: null as null | { id: number; name: string; role: string },
}),
getters: {
isLogin: (s) => !!s.token,
role: (s) => s.userInfo?.role,
},
actions: {
setToken(token: string) {
this.token = token
},
setUserInfo(info: any) {
this.userInfo = info
},
async logout() {
this.token = ''
this.userInfo = null
},
},
})
建议:
- 让"读状态"都从 getters 出口走(组件依赖更清晰)
- action 负责副作用(请求、缓存、失效)
5. 持久化:token/用户信息怎么存更稳
常见做法:
- 仅持久化
token userInfo在刷新后重新拉取(更可信、更安全)
如果你希望持久化(例如减少首屏请求),至少做到:
- token 过期后要能自动清理
- 角色变化后要能"强制刷新权限"
(持久化插件的接入方式很多,这里核心是原则,不强调具体库。)
工程建议(更稳):
- 只持久化 token
- userInfo 在刷新后重新拉取(避免角色变化、权限变更导致前端缓存"过期")
- 登出时清理 token 并重置所有权限相关 store
6. Pinia 和 Axios 拦截器联动
典型目标:
- 请求头自动携带 token
- 401 统一踢下线/跳登录
伪代码示意:
js
axios.interceptors.request.use((config) => {
const userStore = useUserStore()
if (userStore.token) config.headers.Authorization = userStore.token
return config
})
axios.interceptors.response.use(
(res) => res,
(err) => {
if (err.response?.status === 401) {
const userStore = useUserStore()
userStore.logout()
router.replace('/login')
}
return Promise.reject(err)
}
)
关键点:
- 让"登录态失效"的处理在一个地方收敛
- store 只做状态与动作,路由跳转由响应拦截器或路由守卫兜底
7. Pinia 和权限路由联动(动态菜单/路由)
常见需求:
- 不同角色展示不同菜单
- 路由守卫按角色放行/拒绝
落地方式:
userStore.userInfo.role作为权限源permissionStore.routes作为可访问路由表- 登录后拉取用户信息 -> 生成动态路由 ->
router.addRoute
建议:
- 动态路由生成要可重入(刷新、重新登录都能跑一遍)
- 退出登录要清理动态路由与缓存菜单
工程落地的关键点:
- userStore 只负责"登录态、用户信息、权限源数据"
- permissionStore 负责"根据权限源生成可访问路由/菜单"
- 路由守卫负责"什么时候生成、什么时候注入、什么时候重建"
8. 常见坑(反模式清单)
- 组件外直接解构 store:会丢响应
- 把接口响应原样塞进 store:字段不稳定,后续很难维护
- 登录态与权限不同步:只持久化 token,没刷新 userInfo 导致"菜单错乱"
再补两个非常常见的坑:
- store 里放 UI 临时状态(例如某个弹窗开关):会导致 store 膨胀、依赖混乱
- action 里直接 return 后端原始结构:页面不得不写兼容逻辑,违背"契约"
9. 总结
- Pinia 的价值在"状态与副作用统一收敛"
- store 以领域拆分,不要按页面拆
- token 持久化,用户信息建议刷新后重新拉取
- 结合 Axios 拦截器与路由守卫,实现登录态与权限的闭环