ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Vue+Vuex+ElementUI后台管理系统架构与权限控制实战解析

Vue+Vuex+ElementUI后台管理系统架构与权限控制实战解析 简介基于Vue、Vuex与ElementUI搭建的后台管理系统源码包面向正在学习Vue全家桶的中级前端开发者也适合需要快速搭建后台界面模板的实战人群。整个工程压缩后仅191KB共51个文件涵盖19个Vue组件、14个JavaScript脚本、4个JSON配置以及样式、静态图标、说明文档等结构紧凑且模块划分清晰。已有947人学习下载可用于对照练习组件化开发、状态管理和路由权限控制等关键技能。源码中包含views视图模块、router路由配置、store全局状态、mock模拟数据、utils工具函数及layout布局等目录完整呈现了后台管理系统从登录态管理、导航布局到业务页面联动的开发思路。通过阅读和二次修改可以快速掌握Vuex中state、mutations、actions、getters的实际用法并熟悉ElementUI表格、表单、弹窗等组件在真实项目中的组织方式是一份低门槛、可运行的入门级工程范例。1. 一套能落地的 VueVuexElementUI 后台管理系统重头戏在架构不在页面拿到基于vue、vuex、ElementUI的后台管理系统源码.zip先别急着找启动按钮得先判断它值不值得读。这套技术栈高度一致vue 管视图、vuex 管全局状态、ElementUI 管组件恰好覆盖后台系统的界面、权限、数据交互三类需求。源码的价值不在页面多不多而在目录拆得清不清、状态收不收、路由守卫和请求拦截器成不成体系。做过几个后台项目的工程师都有同感能撑两三年不重写的后台工程靠的不是某个页面写得多精彩而是登录、权限、请求三条链路没有散落在各个组件里。后头加按钮权限、换主题、接微前端改的还是这三条链路。下文按 src 工程结构、vuex 状态划分、动态路由权限、ElementUI 落地细节一路展开最后收在打包后布局异常的排查上。2. 从 zip 到跑起来后台管理系统 src 目录结构与 vue 安装及环境配置2.1 先认清一套可维护的 src 目录是怎么拆的成熟的 vue 后台管理系统源码src 下通常固定挂着七个目录api、views、components、layout、store、router、utils。api 只放接口定义views 只放页面级组件store 按业务域拆 vuex 模块router 里把静态路由和动态路由分开维护utils 里放 request 封装和 token 读写工具。判断一套源码能不能继续维护第一眼就看这七个目录是否存在、有没有互相乱引。最常见的坏味道是组件里直接写 axios。当时图省事后面要统一加超时重试、错误上报、token 刷新时就得把每个页面翻一遍。api 目录存在的意义就是把请求从哪来、参数是什么、返回什么收敛到一个文件里页面只关心业务状态。layout 目录容易被新手跳过。后台管理系统的壳——侧边栏 Sidebar、顶栏 Navbar、内容区 AppMain——都在这路由的 component 通常直接指向 Layout由 Layout 里的 router-view 渲染子页面。tag 页签和面包屑一般也在 layout 里做全局监听这个目录拆得好不好直接决定后面做多页签复用时是改一个文件还是改十个。2.2 vue 安装及环境配置的三个关键点这种源码多数基于 Vue 2.6 加 element-ui而不是 Vue 3 的 element-plus。element-ui 的组件生命周期和 Vue 2 的响应式机制绑定很深真要升级到 element-plus等于同时把 vue-router 和 vuex 都升到 v4那是一个独立的迁移工程不是改两个包名的事。所以接手后先读 package.json 的版本号再决定后面所有写法。第一道坎是 Node 版本。依赖里有 node-sass 时Node 17 以上大概率编译报错。常见做法是用 nvm 锁一个 16.x# 用 nvm 固定 Node 版本避免 node-sass / sass-loader 编译报错 nvm install 16.20.2 nvm use 16.20.2 node -v # 期望输出 v16.20.2 # 网络条件差时先换镜像源再装依赖 npm config set registry https://registry.npmmirror.com npm install # 启动开发服务 npm run dev逻辑说明nvm 不是必须的但后台源码最常见的第一道坎就是本机 Node 版本过高导致依赖编译失败npm config set registry 只改当前机器的注册表地址不污染项目代码npm install 中途失败时先删 node_modules 和 package-lock.json 再重装比逐个找报错省时间。注意启动命令在 package.json 的 scripts 里定义有的工程是 npm run serve有的直接 npm start先看一眼再执行。2.3 启动后先验证这几处再动手改业务代码服务起来之后不要急着看页面先打开控制台 Network确认登录请求的域名和路径对不对。多数后台源码把环境相关配置放在项目根目录的 .env 系列文件和 vue.config.js 里我一般会按下面四个位置依次过一遍检查点位置异常时的表现接口基础地址.env.development 的 VUE_APP_BASE_API请求 404 或打到本机 8080跨域转发vue.config.js 里 devServer 的转发配置CORS 报错或 URL 没走转发路由守卫src/permission.js 或 router 目录登录后跳不进首页、反复重定向登录接口路径src/api/user.js页面报错但后端没收到请求这四个位置是后台源码阅读顺序的前四站。先确认接口地址和转发配置再确认守卫放行逻辑最后才看页面代码。顺序反了很常见页面改了一堆最后发现是转发没把 /api 指到后端浪费几个小时。3. 登录链路与 Vuex 状态设计token、用户信息、请求拦截器3.1 为什么 vuex 要按模块拆分而不是一个大 store 到底后台管理系统的全局状态可以分成三类用户态、界面态、权限态。全塞进一个 store 文件里mutations 会越写越长多个模块同时改同一个字段时根本查不清来源。按模块拆是 vuex 官方的推荐做法也几乎是所有后台源码的统一约定vuex 模块职责存放的典型数据user登录态与用户信息token、userInfo、rolespermission路由与按钮权限routes、permissionsapp界面状态sidebar 折叠态、tag 页签、device现在新项目很多人选 pinia但在 Vue 2 存量后台源码里vuex 模块化配合 namespaced 隔离依然是最常见的选择。vue-devtools 插件的时间旅行调试对 vuex 的 mutation 记录最直观排查谁改了 token这类问题时直接看 devtools 里的 mutation 列表比打日志快得多。模块之间要引用外部状态时用 rootGetters 明确声明来源别把同一份共享状态塞进两个模块各存一遍。3.2 用 action 把登录、恢复用户信息、登出串成一条链登录链路不应该在组件里用 then 链乱接。常见做法是把它收敛到 user 模块的 action 里组件只负责拿到表单数据后调用一个 dispatch// src/store/modules/user.js import { login, getUserInfo, logout } from /api/user import { getToken, setToken, removeToken } from /utils/auth const state { token: getToken() || , userInfo: {} } const mutations { SET_TOKEN(state, token) { state.token token }, SET_USER_INFO(state, info) { state.userInfo info } } const actions { // 登录拿到 token 后只做两件事写 state、写本地存储 async login({ commit }, loginForm) { const { token } await login(loginForm) commit(SET_TOKEN, token) setToken(token) }, // 刷新页面后恢复登录态返回值会被路由守卫用到 async getUserInfo({ commit }) { const info await getUserInfo() commit(SET_USER_INFO, info) return info }, // 登出token 和 userInfo 一起清别漏 logout({ commit }) { removeToken() commit(SET_TOKEN, ) commit(SET_USER_INFO, {}) } }逻辑说明token 初始化时从 localStorage 读是为了刷新页面后不丢登录态getUserInfo 的返回值里一般带 roles 或 permissions 字段供后面的动态路由过滤使用登出时如果后端要求服务端也失效 token要先在 logout action 里调一次登出接口再清本地顺序反了会拿到 401。3.3 axios 拦截器统一收口带 token、处理业务码、401 跳登录后台源码里的 request.js 是最值得先读的文件之一它承担三件事请求自动带 token、响应统一处理业务码、401 统一登出并跳转。// src/utils/request.js import axios from axios import { Message } from element-ui import store from /store import router from /router const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }) // 请求拦截器每个请求自动带上 token service.interceptors.request.use(config { if (store.getters.token) { config.headers.Authorization Bearer store.getters.token } return config }, error Promise.reject(error)) // 响应拦截器code 非 200 统一弹错误提示 service.interceptors.response.use(response { const res response.data if (res.code ! 200) { Message.error(res.message || 接口异常) return Promise.reject(new Error(res.message)) } return res.data }, error { // 401token 过期或无效清状态并带 redirect 回登录页 if (error.response error.response.status 401) { store.dispatch(user/logout) router.push({ path: /login, query: { redirect: router.currentRoute.fullPath } }) } Message.error(error.message) return Promise.reject(error) }) export default service参数说明baseURL 优先读环境变量里的 VUE_APP_BASE_API不同环境切换只改 .env 文件业务成功码有的后端约定 200、有的约定 0改一处判断即可redirect 参数让登录成功后用 router.replace 跳回原页面这是后台系统的常见体验细节。token 过期要不要做无感刷新取决于后端是不是双 token 设计只有 access_token 就老老实实跳登录有 refresh_token 才值得在响应拦截器里加重试队列。4. 动态路由与按钮级权限vue 路由权限控制的标准实现4.1 静态路由与动态路由的分工后台源码里的 router 目录几乎都会拆成两张表。constantRoutes 是登录页、404、主框架 Layout 这类所有角色都要有的路由永远注册asyncRoutes 是业务页按角色过滤后再执行 router.addRoutes 追加注册。为什么要这么拆因为很多后台的菜单是后端按角色下发的前端需要做到没有权限的角色连路由都不存在而不是单纯把菜单隐藏。这里要强调一个边界前端权限控制解决的是体验问题——菜单不显示、路由不可达、按钮置灰真正的数据安全靠后端接口校验。动态路由再复杂也拦不住有人改 localStorage 里的角色和权限点所以后端接口的鉴权不能省。权限层级控制对象典型实现失效表现路由权限页面级动态 addRoutes直接访问 URL 空白或 404按钮权限操作级v-permission 指令按钮不渲染或置灰接口权限数据级后端校验 token401 / 4034.2 用 vuex 的 permission 模块把路由过滤收进 action过滤逻辑放在 permission 模块里比放在路由守卫的文件里更干净。下面这段是 vuex 里做权限过滤的典型写法// src/store/modules/permission.js import { constantRoutes, asyncRoutes } from /router // 只校验 meta 里声明了 roles 的路由没声明视为所有人可访问 function hasPermission(roles, route) { if (route.meta route.meta.roles) { return roles.some(role route.meta.roles.includes(role)) } return true } const actions { generateRoutes({ commit }, roles) { return new Promise(resolve { const accessed asyncRoutes.filter(route hasPermission(roles, route)) // 存进 vuex 供侧边栏菜单渲染使用 commit(SET_ROUTES, constantRoutes.concat(accessed)) resolve(accessed) }) } }逻辑说明hasPermission 里的 roles 来自登录后 getUserInfo 的返回已经存在 user 模块里generateRoutes 在路由守卫 src/permission.js 里被调用拿到过滤结果后先执行 router.addRoutes(accessed) 让 URL 真正可访问再 commit 进 vuex 给侧边栏菜单用。这两个动作缺一个都会出问题只存 vuex 会出现菜单有但页面进不去只 addRoutes 会出现 URL 能进但菜单不显示。补充一点这套写法对应的是 vue-router 3vue-router 4 里 addRoutes 被移除要用 addRoute 循环逐个追加。4.3 按钮级权限v-permission 自定义指令的写法和参数路由级权限管到页面按钮级权限管到增删改查按钮。最常见的实现是自定义指令在元素插入时检查权限点没有权限就把这个元素从 DOM 上摘掉// src/directive/permission.js import store from /store export default { inserted(el, binding) { const required Array.isArray(binding.value) ? binding.value : [binding.value] const userPermissions store.getters.permissions || [] const hasPermission required.every(p userPermissions.includes(p)) if (!hasPermission) { el.parentNode el.parentNode.removeChild(el) } } }模板里的用法el-button v-permission[system:user:create] typeprimary新增用户/el-button参数说明binding.value 传权限点数组多个权限点之间是 and 关系用 every 判断userPermissions 由后端登录时下发存进 vuex 的 user 模块。指令定义好后要在入口文件里注册Vue.directive(permission, permission) 一行搞定。删除 DOM 是最直接的做法缺点是按钮被移除后无法恢复如果产品要求无权限时置灰并悬浮提示把 removeChild 改成给 el 加 disabled 属性和自定义样式就行。5. ElementUI 落地细节表格溢出、表单校验、弹窗加载 PDF5.1 表格文字超出隐藏show-overflow-tooltip 与悬浮显示全文写法后台列表字段一多单元格文字就会被挤断。ElementUI 的 el-table-column 自带 show-overflow-tooltip单行超长时省略号加悬浮提示够用但不完美——它只显示一行想展示完整的多行内容还是得靠自定义悬浮层。!-- 单行省略 悬浮 title 提示列表里 90% 的字段用这个就够了 -- el-table-column propdescription label描述 min-width180 show-overflow-tooltip /如果要悬浮显示全部字段常见做法是包一层 el-tooltip并在内容区域再渲染一次完整文本el-table-column label备注 min-width200 template slot-scope{ row } el-tooltip placementtop :disabled!row.remark :contentrow.remark span classcell-ellipsis{{ row.remark }}/span /el-tooltip /template /el-table-column说明这样做的代价是表格里这个字段渲染了两份内容数据量大时影响渲染性能所以一般只对备注、描述这类低频且超长内容用常规短字段交给 show-overflow-tooltip 就够。配套的 .cell-ellipsis 样式在 ElementUI 文档的表格布局章节里有现成写法记得加 overflow: hidden 和 text-overflow: ellipsis。模板语法按 Vue 2 写如果工程已经迁到 Vue 3slot-scope 要换成 v-slot 并配合 #default 具名插槽。5.2 表单校验自定义 validator 与编辑场景的动态规则el-form 的 rules 依赖 prop 与 model 字段一一对应。业务校验里最常写错的是正则没考虑中文比如角色名允许 2-20 位中文、字母、数字或下划线export default { data() { // 自定义校验必须调用 callback() 结束否则表单永远处于校验中 const validateRoleName (rule, value, callback) { if (!/^[a-zA-Z0-9_\u4e00-\u9fa5]{2,20}$/.test(value)) { callback(new Error(2-20位中文、字母、数字或下划线)) } else { callback() } } return { rules: { roleName: [{ required: true, trigger: blur, validator: validateRoleName }] } } }, methods: { submit() { this.$refs.form.validate(valid { if (valid) this.save() }) } } }el-form refform :modelform :rulesrules label-width96px el-form-item label角色名 proproleName el-input v-modelform.roleName / /el-form-item /el-form说明trigger 用 blur 是失焦时校验change 是输入时校验密码和金额字段建议 blur新增和编辑共用同一个弹窗表单时打开弹窗后要先 this.$nextTick(() this.$refs.form.clearValidate())否则上次的错误提示会一直挂着影响体验。5.3 弹窗加载 PDFblob 方式与 token 兼容后台经常要预览合同、报表这类 PDF。最省事的写法是 iframe 直接指向后端文件 URL但它有硬伤带鉴权的文件没法把 token 塞进 URL浏览器地址栏直接访问也拿不到 Authorization 头。换成 axios 拉 blob、再在内存里生成临时 URL 的方式更稳方式优点缺点iframe 直接指向 URL代码最少无需额外请求无法携带 Authorization 头axios blob createObjectURL复用统一拦截器自动带 token需要手动释放对象 URL// src/api/file.js import request from /utils/request // responseType 必须是 blob否则拉下来的是乱码 export const getFileBlob id request.get(/file/${id}, { responseType: blob })// 预览组件内 async openPdf(row) { const blob await getFileBlob(row.id) this.previewUrl URL.createObjectURL( new Blob([blob], { type: application/pdf }) ) this.previewVisible true }, beforeDestroy() { // 释放对象 URL长列表频繁预览时避免内存持续上涨 if (this.previewUrl) URL.revokeObjectURL(this.previewUrl) }参数说明request 拦截器会自动加上 Authorization 头这是选 blob 方式的最大理由Blob 的 type 要写 application/pdf否则 iframe 可能触发下载而不是预览beforeDestroy 里 revokeObjectURL 释放内存配合 el-dialog 的 destroy-on-close 属性效果更好。6. 打包后布局异常后台管理系统生产构建的四个排查点本地 npm run dev 一切正常npm run build 部署上去布局就乱了这是后台管理系统源码被问得最多的问题之一。这类现象九成出在下面四个点按顺序排查最快。第一个是路由模式。vue-router 开了 history 模式nginx 没配 try_files刷新业务子页面直接 404。第二个是 publicPath。构建配置里写成绝对路径部署到子目录时 CSS、字体全部 404页面裸奔自然布局异常。第三个是 element-ui 字体文件element-icons.woff 加载失败时图标全消失菜单、按钮全错位。第四个是 CSS 构建压缩后顺序错乱关掉压缩对比验证即可。server { listen 80; server_name admin.example.com; root /data/dist; index index.html; location / { # history 路由的核心匹配不到文件时回退到 index.html try_files $uri $uri/ /index.html; } location /static/ { expires 1y; add_header Cache-Control public, immutable; } }try_files 的作用是访问 /system/user 时服务器上不存在这个文件就回退到 index.html由前端路由接管渲染。static 目录下的资源带 hash 后缀可以放心缓存一年这是后台系统性能优化里性价比最高的一条。验证方法构建完后先 npx serve -s dist 在本地起静态服务刷新子路由看是 404 还是正常404 就是后端没配 try_files再打开 Network 面板过滤 .woff字体 404 就检查 publicPath 和部署目录层级最后把侧边栏折叠态和浏览器窗口宽度都试一遍区分是样式问题还是组件状态问题。注意改完 nginx 配置先执行 nginx -t 检查语法再 reload避免把线上的服务改挂。定位 element-ui 字体问题有个快速技巧在构建产物 dist 目录里直接搜 element-icons.woff确认文件有没有被拷出来——文件在但还是 404问题就出在 publicPath 路径层级文件不在则是构建配置没带上静态资源回头看 vue.config.js 的 assetsDir 和 publicPath 两处。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进