一、项目概述
一个基于 Vue Router 的路由守卫机制实现了前端登录状态校验与权限控制,确保未授权用户无法访问受保护资源,同时结合 NProgress 进度条 优化页面跳转体验,根据用户权限动态展示可访问菜单。
-
路由实例(router)
作为整个路由系统的核心载体,提供路由守卫(
beforeEach、afterEach)的配置入口,是实现权限控制和页面跳转逻辑的基础。 -
登录状态校验工具(getToken)
从本地存储(如 cookie、localStorage)中读取用户登录凭证(token),作为判断用户是否已登录的核心依据,是权限控制的前置条件。
-
用户体验优化插件(NProgress)
页面跳转时显示进度条的轻量级插件,通过
start()和done()方法在路由守卫中控制进度条状态,直观反馈页面加载过程,提升用户体验。 -
缓存工具(useCache)
提供本地缓存能力(如
wsCache),用于持久化存储用户权限列表、用户信息等关键数据,避免页面刷新后数据丢失,支持权限逻辑的跨页面复用。 -
权限接口(getUserPermission)
后端接口封装函数,用于获取当前登录用户的权限列表(如可访问的菜单、操作权限),为前端动态渲染菜单和控制页面访问权限提供数据支撑。
二、核心模块解析
2.1 路由守卫与 NProgress 进度条实现(src/permission.js)
路由守卫是权限控制的核心,通过 router.beforeEach 全局前置守卫实现登录状态与权限的校验逻辑,配合 router.afterEach 处理页面跳转后的收尾工作,其中 NProgress 进度条 用于可视化展示跳转过程,提升用户体验。
javascript
import router from '@/router'
import { getToken } from '@/utils/auth'
import NProgress from 'nprogress' // 引入NProgress库
import 'nprogress/nprogress.css' // 引入默认样式
import { CACHE_KEY, useCache } from '@/hooks/useCache'
import { getUserPermission } from '@/api/login'
// 配置NProgress:隐藏右上角加载动画
NProgress.configure({ showSpinner: false })
// 白名单页面(无需登录即可访问)
const whiteList = ['/login', '/register']
const { wsCache } = useCache()
router.beforeEach((to, from, next) => {
// 路由跳转开始时启动进度条
NProgress.start()
const hasToken = getToken() // 判断是否存在登录令牌
if (hasToken) {
// 已登录状态
if (to.path === '/login' || isWhiteList(to.path)) {
// 已登录用户访问白名单页面,重定向到首页
next({ path: '/' })
// 此处无需手动调用NProgress.done(),最终会由afterEach统一处理
} else {
// 访问受保护页面:获取用户权限并缓存
getUserPermission({}).then(res => {
// 缓存权限与用户信息
wsCache.set(CACHE_KEY.PERMISSION, res.data.menuRespVOS.map(item => item.permission))
wsCache.set(CACHE_KEY.USER, res.data.employeeUserRespVO)
next() // 允许跳转
}).catch(() => {
// 权限获取失败(如token过期),清除缓存并跳转到登录页
wsCache.delete(CACHE_KEY.PERMISSION)
next(`/login?redirect=${encodeURIComponent(to.fullPath)}`)
})
}
} else {
// 未登录状态
if (isWhiteList(to.path)) {
// 允许访问白名单页面
next()
} else {
// 重定向到登录页,并记录目标路径(登录后跳转回原页面)
next(`/login?redirect=${encodeURIComponent(to.fullPath)}`)
}
}
})
// 路由跳转完成后结束进度条
router.afterEach(() => {
NProgress.done()
})
// 白名单判断工具函数
const isWhiteList = (path) => {
return whiteList.some((pattern) => {
return path === pattern || path.startsWith(pattern + '/')
})
}
2.2 路由配置(src/router/index.js)
javascript
import {
createRouter, // 创建路由实例的核心函数
createWebHistory // 用于创建 HTML5 History 模式的路由(无 hash 前缀)
} from 'vue-router'
import Login from '@/views/login/index.vue' // 登录页面组件
const Layout = () => import('@/layout/Layout.vue') // 布局组件(采用懒加载优化性能)
// 导入菜单图标资源
import iconDevice from '@/assets/images/menu/icon-device.png'
import iconBackup from '@/assets/images/menu/icon-backup.png'
// 导出路由配置数组(供权限控制、菜单生成等场景复用)
export const routes = [
{
path: '/login', // 登录页路由路径
name: 'Login', // 路由唯一标识(用于编程式导航)
component: Login, // 关联的页面组件
meta: {
hidden: true // 元信息:标记为在侧边栏菜单中隐藏
}
},
{
path: '/', // 根路径
redirect: '/device/cloudphone', // 重定向到默认首页(我的云机)
meta: { hidden: true } // 重定向路由无需在菜单显示
},
{
path: '/device', // 设备管理模块根路径
name: 'Device', // 模块唯一标识
meta: {
title: '设备管理', // 菜单显示名称
breadcrumbNoLink: true // 面包屑配置:该节点不显示链接
},
icon: iconDevice, // 菜单图标
component: Layout, // 使用布局组件(包含侧边栏、头部等公共部分)
children: [ // 子路由(对应子菜单)
{
path: 'cloudphone', // 子路由路径(完整路径为 /device/cloudphone)
name: 'DeviceCloudphone',
component: () => import('@/views/device/cloudphone/index.vue'), // 懒加载子页面组件
meta: { title: '我的云机' }, // 子菜单显示名称
icon: iconCloudPhone // 子菜单图标
},
....
]
},
....
{
path: '/:pathMatch(.*)*', // 通配符路由,匹配所有未定义的路径
name: 'NotFound',
component: () => import('@/views/error/404.vue'), // 404错误页面
meta: { hidden: true }
}
]
// 创建路由实例
const router = createRouter({
history: createWebHistory(), // 使用HTML5 History模式(需要后端配合配置)
routes // 应用路由配置
})
export default router // 导出路由实例供Vue应用使用
2.3 权限辅助模块(src/layout/Layout.vue )
javascript
// 从缓存获取用户信息和权限
const { wsCache } = useCache()
const userinfo = wsCache.get(CACHE_KEY.USER)
const permissions = wsCache.get(CACHE_KEY.PERMISSION)
// 原始路由配置(从 router/index.js 导入)
import { routes } from '@/router'
// 过滤路由函数:根据用户权限和角色筛选可见菜单
const filterRoutes = (routes) => {
return routes.filter(route => {
// 1. 过滤隐藏的路由(meta.hidden = true)
if (route.meta?.hidden) return false
// 2. 处理子路由(递归过滤)
if (route.children && route.children.length) {
route.children = filterRoutes(route.children)
// 若子路由过滤后为空,父路由也不显示
if (route.children.length === 0) return false
}
// 3. 基于用户角色的权限过滤
// 示例1:非主账号隐藏个人中心
if (route.path === '/center' && userinfo?.masterFlag === 0) {
return false
}
// 示例2:员工管理仅特定角色可见
if (route.path === '/device/staff' && userinfo?.positionType === 30) {
return false
}
// 4. 基于权限码的过滤(如需要精确到按钮级权限)
if (route.meta?.permission && !permissions?.includes(route.meta.permission)) {
return false
}
return true
})
}
// 生成最终显示的菜单
const menuItems = filterRoutes(routes)
2.4 权限指令(src/directives/index.js)
javascript
// 权限指令核心逻辑
import { useCache, CACHE_KEY } from '@/hooks/useCache'
// 判断是否拥有指定权限
const hasPermission = (permission) => {
const { wsCache } = useCache()
const permissions = wsCache.get(CACHE_KEY.PERMISSION) || []
return permissions.includes(permission)
}
// 判断是否拥有指定角色
const hasRole = (role) => {
const { wsCache } = useCache()
const userinfo = wsCache.get(CACHE_KEY.USER) || {}
return userinfo.roles?.includes(role)
}
// 注册权限指令:v-hasPermi
export const setupHasPermi = (app) => {
app.directive('hasPermi', {
mounted(el, binding) {
const { value } = binding
if (!hasPermission(value)) {
// 无权限时移除元素或隐藏
el.parentNode?.removeChild(el)
// 或 el.style.display = 'none'
}
}
})
}
// 注册角色指令:v-hasRole
export const setupHasRole = (app) => {
app.directive('hasRole', {
mounted(el, binding) {
const { value } = binding
if (!hasRole(value)) {
el.parentNode?.removeChild(el)
}
}
})
}
// 在main.js中注册
import { setupHasPermi, setupHasRole } from '@/directives'
const app = createApp(App)
setupHasPermi(app)
setupHasRole(app)
2.5 使用场景示例
javascript
<!-- 仅拥有 device:delete 权限的用户可见 -->
<el-button v-hasPermi="'device:delete'">删除设备</el-button>
<!-- 仅管理员角色可见 -->
<el-button v-hasRole="'admin'">管理员操作</el-button>
三、常见问题及解决方案
4.1 进度条不显示或样式异常
- 问题原因 :
- 未引入
nprogress.css或引入路径错误 - 自定义 CSS 冲突导致进度条被隐藏
- 未引入
- 解决方案 :
- 确认
import 'nprogress/nprogress.css'语句存在且路径正确 - 检查全局 CSS 中是否有覆盖
#nprogress相关样式的代码,必要时使用!important强制应用样式
- 确认
4.2 进度条卡在某个状态不消失
- 问题原因 :
beforeEach守卫中未调用next(),导致路由跳转未完成,afterEach不执行- 异步操作(如
getUserPermission)未正确处理异常,导致next()未被调用
- 解决方案 :
- 确保所有分支逻辑中都调用了
next()(包括then和catch回调) - 对异步操作添加超时处理,避免因接口无响应导致进度条一直显示:
- 确保所有分支逻辑中都调用了
javascript
// 示例:为getUserPermission添加超时控制
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('请求超时')), 10000)
})
Promise.race([getUserPermission({}), timeoutPromise])
.then(res => { ... })
.catch(err => { ... })
4.3 频繁跳转时进度条闪烁
- 问题原因 :短时间内多次触发路由跳转(如快速点击多个菜单),导致
start()和done()频繁调用 - 解决方案 :
-
在路由守卫中添加防抖逻辑,限制短时间内的跳转频率
-
保留默认行为,因 NProgress 内部已做优化,多次调用
start()不会重复创建进度条
-
四、总结
本项目通过 Vue Router 路由守卫结合 NProgress 进度条,实现了兼具安全性和用户体验的权限控制体系。其中 NProgress 的引入显著提升了页面跳转过程的透明度,使用户能够清晰感知操作状态。核心亮点包括:
- 权限与体验并重:在严格的登录校验和权限控制基础上,通过进度条优化用户体验
- 轻量高效:NProgress 体积小(约 3KB),无冗余依赖,不影响页面性能
- 可定制化:支持通过配置项和 CSS 自定义进度条样式,适配项目主题