同学们好,我是 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')
}
八、小结:一份自检清单
设计全局状态树时,可以按下面检查:
学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。
后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。
关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。
如果你觉得这篇内容对你有帮助,不妨点赞+收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。
我是 Eugene,你的电子学友,我们下一篇干货见~
网硕互联帮助中心


评论前必须登录!
注册