云计算百科
云计算领域专业知识百科平台

Vue状态管理扫盲篇:如何设计一个合理的全局状态树 | 用户、权限、字典、布局配置

同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~

(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)

你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?

你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?

就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。

一天只有24小时,时间永远不够用,常常感到力不从心。

技术行业,本就是逆水行舟,不进则退。

如果你也有同样的困扰,别慌。

从现在开始,跟着我一起心态归零,利用碎片时间,来一次彻彻底底的基础扫盲。

这一次,我们一起慢慢来,扎扎实实变强。

不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,

咱们一起稳步积累,真正摆脱"面向搜索引擎写代码"的尴尬。


一、先搞清楚:什么是全局状态?为什么要设计?

1.1 一句话理解

全局状态:多个页面、多个组件都需要读取和修改的同一份数据,放在一个统一的“仓库”里统一管理。

典型场景:

  • 登录后的用户信息,很多页面都要用
  • 按钮/菜单权限,决定能否显示或操作
  • 字典数据(如性别、状态、类型),下拉框到处用到
  • 侧边栏是否折叠、主题色等布局配置

如果每个组件各自发请求、各自存一份,就会:

  • 重复请求、浪费资源
  • 数据不同步,容易出 bug
  • 刷新页面后数据丢失

把这些数据抽成全局状态树统一管理,可以:

  • 只请求一次,多处复用
  • 单一数据源,更新一致
  • 持久化后刷新也能恢复

1.2 用 Vuex / Pinia 做什么?

  • Vuex:Vue 2 官方状态库,概念多(state、mutation、action、module)。
  • Pinia:Vue 3 官方推荐,API 更简洁,支持 TypeScript。

后面示例会以 Pinia 为主,思路和模块划分同样适用于 Vuex。


二、整体设计思路:按业务域拆分模块

不要把用户、权限、字典、布局全部塞进一个 store 里,建议按业务域拆成模块:

store/
├── modules/
│ ├── user.ts # 用户信息
│ ├── permission.ts # 权限(路由、按钮)
│ ├── dict.ts # 字典
│ └── layout.ts # 布局配置
├── index.ts # 入口,挂载所有模块

原则:

  • 每个模块只负责一类数据
  • 模块间尽量少耦合
  • 跨模块逻辑放在 action 里或单独的 service 中

三、模块一:用户状态(User)

3.1 存什么?

  • 基本信息:id、username、nickname、avatar、email 等
  • 登录态:token、tokenExpire
  • 组织信息(若有多租户):orgId、orgName 等

3.2 基本结构示例

// store/modules/user.ts
import { defineStore } from 'pinia'

export interface UserInfo {
id: string
username: string
nickname: string
avatar?: string
email?: string
phone?: string
}

export const useUserStore = defineStore('user', {
state: () => ({
token: '' as string,
userInfo: null as UserInfo | null,
// 可选:记录登录时间,用于 token 续期判断
loginTime: 0 as number
}),

getters: {
isLoggedIn: (state) => !!state.token,
displayName: (state) => state.userInfo?.nickname || state.userInfo?.username || ''
},

actions: {
setToken(token: string) {
this.token = token
},
setUserInfo(info: UserInfo | null) {
this.userInfo = info
},
login(token: string, userInfo: UserInfo) {
this.token = token
this.userInfo = userInfo
this.loginTime = Date.now()
},
logout() {
this.token = ''
this.userInfo = null
this.loginTime = 0
}
},

// 持久化:token 和 userInfo 要持久化,刷新后还能用
persist: {
key: 'user-store',
storage: localStorage,
paths: ['token', 'userInfo']
}
})

3.3 常见坑点

坑现象建议
token 不持久化 刷新页面就掉线 用 pinia-plugin-persistedstate 等持久化 token
敏感信息进 localStorage token 可能被 XSS 利用 敏感系统用 httpOnly cookie 存 token
userInfo 和 token 不同步 有 token 没 userInfo,或反之 登录时一起设置;退出时一起清空

四、模块二:权限状态(Permission)

4.1 存什么?

  • 路由权限:用户可访问的路由/菜单,用于动态路由和侧边栏
  • 按钮权限:如 user:add、user:edit,用于控制按钮显隐

4.2 常见后端数据结构(示例)

// 后端可能返回的权限结构
{
"menus": [
{
"path": "/user",
"name": "用户管理",
"children": [
{ "path": "/user/list", "name": "用户列表", "perms": ["user:list"] },
{ "path": "/user/add", "name": "新增用户", "perms": ["user:add"] }
]
}
],
"permissions": ["user:list", "user:add", "user:edit", "user:delete"]
}

4.3 权限 Store 示例

// store/modules/permission.ts
import { defineStore } from 'pinia'

export interface MenuItem {
path: string
name: string
icon?: string
perms?: string[]
children?: MenuItem[]
}

export const usePermissionStore = defineStore('permission', {
state: () => ({
menus: [] as MenuItem[],
permissions: [] as string[] // 扁平化的权限码,用于按钮级控制
}),

getters: {
hasPermission: (state) => (code: string) => {
// 超级管理员通常拥有所有权限
if (state.permissions.includes('*')) return true
return state.permissions.includes(code)
}
},

actions: {
setMenus(menus: MenuItem[]) {
this.menus = menus
},
setPermissions(perms: string[]) {
this.permissions = perms
},
reset() {
this.menus = []
this.permissions = []
}
}
})

4.4 按钮级权限:自定义指令

// directives/permission.ts
import type { Directive } from 'vue'
import { usePermissionStore } from '@/store/modules/permission'

export const vPermission: Directive = {
mounted(el, binding) {
const permissionStore = usePermissionStore()
const code = binding.value as string
if (!code) return
if (!permissionStore.hasPermission(code)) {
// 无权限:直接移除 DOM
el.parentNode?.removeChild(el)
}
}
}

// main.ts 或入口文件注册
// app.directive('permission', vPermission)

使用示例:

<template>
<el-button v-permission="'user:add'">新增用户</el-button>
<el-button v-permission="'user:edit'">编辑</el-button>
</template>

4.5 常见坑点

坑现象建议
刷新后权限丢失 侧边栏空了或路由 403 登录后把 menus/permissions 持久化,或刷新后重新拉取
指令在 store 未初始化时执行 报错或误判 在路由守卫中确保权限已加载再渲染页面
权限码不统一 前端 user:add 后端 user_add 和后端约定统一格式

五、模块三:字典状态(Dict)

5.1 存什么?

下拉选项、状态文案等:如 gender(男/女)、userStatus(启用/禁用)等。

5.2 设计要点

  • 按 dictType 分组存储
  • 支持懒加载(用到再请求)
  • 适当缓存,减少请求

5.3 字典 Store 示例

// store/modules/dict.ts
import { defineStore } from 'pinia'

export interface DictItem {
label: string
value: string | number
[key: string]: any
}

export const useDictStore = defineStore('dict', {
state: () => ({
// dictType -> DictItem[]
dictMap: {} as Record<string, DictItem[]>,
// 记录哪些类型已加载过,避免重复请求
loadedTypes: [] as string[]
}),

getters: {
getDict: (state) => (dictType: string) => {
return state.dictMap[dictType] || []
},
getDictLabel: (state) => (dictType: string, value: string | number) => {
const list = state.dictMap[dictType] || []
const item = list.find((d) => d.value === value)
return item?.label ?? value
}
},

actions: {
async loadDict(dictType: string) {
if (this.loadedTypes.includes(dictType)) {
return this.dictMap[dictType]
}
// 实际项目中替换为你的 API 请求
const res = await fetch(`/api/dict/${dictType}`).then((r) => r.json())
const list = res.data || []
this.dictMap[dictType] = list
this.loadedTypes.push(dictType)
return list
},
setDict(dictType: string, list: DictItem[]) {
this.dictMap[dictType] = list
if (!this.loadedTypes.includes(dictType)) {
this.loadedTypes.push(dictType)
}
}
}
})

5.4 在组件中使用

<template>
<el-select v-model="form.gender" placeholder="请选择性别">
<el-option
v-for="item in dictStore.getDict('gender')"
:key="item.value"
:label="item.label"
:value="item.value"
/>

</el-select>
<!– 或用于展示:根据 value 显示 label –>
<span>{{ dictStore.getDictLabel('gender', form.gender) }}</span>
</template>

<script setup lang="ts">
import { useDictStore } from '@/store/modules/dict'
import { onMounted } from 'vue'

const dictStore = useDictStore()

onMounted(async () => {
await dictStore.loadDict('gender')
})
</script>

5.5 常见坑点

坑现象建议
一进来就拉所有字典 首屏慢 按需 loadDict,用到再加载
value 类型不一致 选项选了不显示 统一用 string 或 number,和接口一致
字典更新不生效 改了后台,前端还是旧数据 提供 clearDict(type) 或刷新逻辑,必要时登出清缓存

六、模块四:布局配置(Layout)

6.1 存什么?

侧边栏折叠、主题、标签页、语言等,这类配置通常需要持久化。

6.2 布局 Store 示例

// store/modules/layout.ts
import { defineStore } from 'pinia'

export const useLayoutStore = defineStore('layout', {
state: () => ({
sidebarCollapsed: false,
theme: 'light' as 'light' | 'dark',
// 多标签页的页面栈(可选)
visitedViews: [] as { path: string; title: string }[]
}),

getters: {
sidebarWidth: (state) => (state.sidebarCollapsed ? '64px' : '220px')
},

actions: {
toggleSidebar() {
this.sidebarCollapsed = !this.sidebarCollapsed
},
setTheme(theme: 'light' | 'dark') {
this.theme = theme
document.documentElement.setAttribute('data-theme', theme)
},
addVisitedView(route: { path: string; meta?: { title?: string } }) {
const view = { path: route.path, title: route.meta?.title || '未命名' }
const exist = this.visitedViews.find((v) => v.path === route.path)
if (!exist) this.visitedViews.push(view)
}
},

persist: {
key: 'layout-store',
storage: localStorage,
paths: ['sidebarCollapsed', 'theme']
}
})

6.3 常见坑点

坑现象建议
持久化体积过大 visitedViews 太多 只持久化 sidebarCollapsed、theme 等必要字段
主题切换闪烁 先亮后暗 在 HTML 最前面根据存储的 theme 设置 class,或做骨架屏

七、整体挂载与初始化顺序

7.1 Pinia 入口

// store/index.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

export default pinia

7.2 路由守卫中的初始化流程

// router/index.ts
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
const permissionStore = usePermissionStore()

if (userStore.isLoggedIn) {
// 已登录:确保权限已加载
if (permissionStore.permissions.length === 0) {
try {
await fetchUserPermissions() // 调用你的接口
// 动态添加路由…
} catch (e) {
userStore.logout()
next('/login')
return
}
}
next()
} else {
if (to.path === '/login') {
next()
} else {
next('/login')
}
}
})

退出登录时建议统一清理:

async function logout() {
const userStore = useUserStore()
const permissionStore = usePermissionStore()
userStore.logout()
permissionStore.reset()
router.push('/login')
}


八、小结:一份自检清单

设计全局状态树时,可以按下面检查:

  • 按业务域拆分:user、permission、dict、layout 等独立模块。
  • 明确持久化范围:token、userInfo、布局配置要持久化;权限按需持久化或登录后拉取。
  • 权限与路由联动:登录后加载权限 → 动态路由 → 再渲染页面。
  • 字典按需加载:用到再 loadDict,并做好缓存。
  • 退出时清理:logout 时重置 user、permission 等,避免脏数据。

  • 学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。

    后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。

    关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。

    如果你觉得这篇内容对你有帮助,不妨点赞+收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。

    我是 Eugene,你的电子学友,我们下一篇干货见~

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Vue状态管理扫盲篇:如何设计一个合理的全局状态树 | 用户、权限、字典、布局配置
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!