十二、vue3后台项目系列——设置路由守卫,获取角色权限,获取角色路由列表、页面请求进度条
前言:建议配合十一章进行阅读比较好,我们将从以下顺序说起:
1.获取角色权限
2.添加页面上请求的进度条显示
3.配置路由守卫
4.根据路由元信息动态设置页面标题
5.根据角色权限获取对应的路由列表为后续显示的菜单栏做准备
一、获取角色权限
在登录之后,我们要里面调用获取用户信息的接口来获取相关的信息,里面就包含了角色信息。
有了角色信息我们就可以做权限匹配,执行相关的角色权限操作。其他用户信息也是必要,也是在这时获取。
我们使用的pinia,我们将相关逻辑放到状态管理仓中,统一管理。store/modules.userStore.js
import { defineStore } from "pinia";
import { login, logout, getInfo } from "@/api/user";
import { getToken, setToken, removeToken } from "@/utils/auth";
import router, { resetRouter } from "@/router";
import { usePermissionStore, useTagsViewStore } from "@/store";
import { ElMessage } from "element-plus";export const useUserStore = defineStore("user", {// state:定义数据,使用箭头函数返回一个对象,以确保状态是独立的state: () => ({token: getToken(),name: "",avatar: "",introduction: "",roles: [],}),// getters:派生状态,类似于计算属性getters: {// 你可以在这里定义一些计算属性,例如:// isAdmin: (state) => state.roles.includes('admin')},// actions:定义业务逻辑,可以进行异步操作和直接修改 stateactions: {// 用户登录async userLogin(userInfo) {try {const { username, password } = userInfo;const response = await login({username: username.trim(),password: password,});const res = response;console.log(res);if (res.code == 200) {ElMessage({message: "登录成功",type: "success",duration: 5 * 1000,});}// 直接通过 this 修改 statethis.token = res.data.token;console.log("this.token",this.token);setToken(this.token);return Promise.resolve(res);} catch (error) {return Promise.reject(error);}},// 获取用户信息async getUserInfo() {try {const response = await getInfo(this.token);console.log("response用户信息",response);if (!response.data) {return Promise.reject("登录过期,请重新登录。");}const { roles, name, avatar, introduction } = response.data;if (!roles || roles.length <= 0) {return Promise.reject("角色不能为空数组");}// 直接通过 this 修改 statethis.roles = roles;this.name = name;this.avatar = avatar;this.introduction = introduction;return Promise.resolve(response);} catch (error) {return Promise.reject(error);}},// 用户登出async userLogout() {try {await logout(this.token);this.token = "";this.roles = [];removeToken();resetRouter();// 跨 Store 调用const tagsViewStore = useTagsViewStore();tagsViewStore.delAllViews();return Promise.resolve();} catch (error) {return Promise.reject(error);}},// 移除 tokenasync resetToken() {this.token = "";this.roles = [];removeToken();},// 动态修改权限async changeRoles(role) {// 生成对应角色的tokenconst token = role + "-token";// 直接在action中修改状态this.token = token;// 保存token到本地存储setToken(token);// 获取用户信息(包含角色)const { roles } = await this.getInfo();// 更新当前store的rolesthis.roles = roles;// 重置路由resetRouter();// 获取权限store实例const permissionStore = usePermissionStore();// 生成可访问的路由const accessRoutes = await permissionStore.generateRoutes(roles);// 动态添加路由accessRoutes.forEach((route) => {router.addRoute(route);});// 获取标签视图store实例并重置const tagsViewStore = useTagsViewStore();tagsViewStore.delAllViews();},},
});
在获取到之后,及时将用户的基本的信息存储到状态中。这个roles就是获取菜单权限列表的关键。
二、添加页面上请求的进度条显示
nprogress
是一个轻量级的进度条库,常用于在页面加载、路由切换或数据请求时显示加载进度,提升用户体验。
安装nprogress:
npm install -d nprogress
基本使用是这样的:
// 引入库和样式
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';// 开始显示进度条(从0%开始)
NProgress.start();// 模拟加载过程(例如数据请求)
setTimeout(() => {// 结束进度条(平滑过渡到100%后消失)NProgress.done();
}, 2000);
// 其他常用方法
// 设置进度(0-1之间的数值)
NProgress.set(0.4);// 增加一点进度(随机增加少量值)
NProgress.inc();// 强制结束(立即消失,无过渡动画)
NProgress.done(true);
使用场景:
场景 1:路由切换时显示
在 Vue Router 或 React Router 中,可在路由守卫中使用:
// Vue Router 示例
router.beforeEach((to, from, next) => {NProgress.start(); // 开始next();
});router.afterEach(() => {NProgress.done(); // 结束
});
场景 2:Axios 请求拦截
在请求拦截器中控制进度条,覆盖所有网络请求:
import axios from 'axios';// 请求拦截器
axios.interceptors.request.use(config => {NProgress.start();return config;
});// 响应拦截器
axios.interceptors.response.use(response => {NProgress.done();return response;},error => {NProgress.done(); // 错误时也需要结束return Promise.reject(error);}
);
自定义样式:
/* 改变进度条颜色 */
#nprogress .bar {background: #29d !important;height: 3px !important;
}/* 改变加载 spinner 颜色 */
#nprogress .spinner-icon {border-top-color: #29d;border-left-color: #29d;
}/* 隐藏 spinner */
#nprogress .spinner {display: none !important;
}
配置选项:
NProgress.configure({minimum: 0.1, // 最小进度值(默认0.08)easing: 'ease', // 动画缓动函数speed: 500, // 动画速度(毫秒)showSpinner: false, // 是否显示 spinnertrickle: true, // 是否自动递增trickleSpeed: 200, // 自动递增间隔(毫秒)parent: 'body' // 进度条的父容器
});
在我们的项目使用是:
在路由守卫文件src/permission.js中添加,在每次页面跳转之前添加。
关键代码是:
import NProgress from 'nprogress' // 进度条
import 'nprogress/nprogress.css' // 进度条样式NProgress.configure({ showSpinner: false }) // NProgress配置router.beforeEach(async(to, from, next) => {// 启动进度条NProgress.start()
}router.afterEach(() => {// 完成进度条NProgress.done()
})
三、配置路由守卫(src/permission.js)
需要在main.js文件中导入
import './permission' // 引入路由守卫
import { useUserStore,usePermissionStore } from '@/store'
import { ElMessage } from 'element-plus'
import NProgress from 'nprogress' // 进度条
import 'nprogress/nprogress.css' // 进度条样式
import { getToken } from '@/utils/auth' // 从cookie获取token
import getPageTitle from '@/utils/get-page-title'// 导入路由配置
import router from './router'NProgress.configure({ showSpinner: false }) // NProgress配置const whiteList = ['/login'] // 无需重定向的白名单router.beforeEach(async(to, from, next) => {// 启动进度条NProgress.start()// 设置页面标题document.title = getPageTitle(to.meta.title)// 确定用户是否已登录(有token)const hasToken = getToken()if (hasToken) {if (to.path === '/login') {// 如果已登录,访问登录页时重定向到首页next({ path: '/' })NProgress.done() // 完成进度条} else {// 获取用户Store实例const userStore = useUserStore()// 确定用户是否已获取权限角色const hasRoles = userStore.roles && userStore.roles.length > 0if (hasRoles) {// 已有角色权限,直接访问next()} else {try { // 获取用户信息// 注意:roles必须是一个数组,例如: ['admin'] 或 ['developer','editor']const res = await userStore.getUserInfo()const roles = res.data.roles// 获取权限Store实例const permissionStore = usePermissionStore()// 根据角色生成可访问的路由映射const accessRoutes = await permissionStore.generateRoutes(roles)console.log("accessRoutes",accessRoutes);// 动态添加可访问路由accessRoutes.forEach(route => {router.addRoute(route)})// 确保addRoute完成的hack方法// 设置replace: true,这样导航就不会留下历史记录next({ ...to, replace: true })} catch (error) {// 移除token并跳转到登录页重新登录const userStore = useUserStore()await userStore.resetToken()next(`/login?redirect=${to.path}`)NProgress.done()}}}} else {/* 没有token的情况 */if (whiteList.includes(to.path)) {// 在免登录白名单,直接进入next()} else {// 其他没有访问权限的页面被重定向到登录页next(`/login?redirect=${to.path}`)NProgress.done()}}
})router.afterEach(() => {// 完成进度条NProgress.done()
})
这段代码是 Vue 项目中路由导航守卫的核心配置,作用是在用户切换页面(路由)前,统一处理「登录状态校验、权限控制、页面标题设置、加载进度条」等核心逻辑,确保只有符合条件的用户才能访问对应页面。下面分「整体功能」和「步骤拆解」两部分解释:
1.整体功能概览
首先明确代码里的关键依赖和核心目标:
- 核心目标:
- 未登录用户 → 强制跳转登录页(并记录「想访问的页面」,登录后自动返回);
- 已登录用户 → 校验是否有「权限角色」,无则自动拉取用户信息并生成「可访问的路由」;
- 所有跳转 → 自动显示 / 隐藏进度条、设置页面标题。
2.详细步骤拆解(按代码执行顺序)
代码主要分两部分:router.beforeEach
(路由跳转前的「前置守卫」,核心逻辑)和 router.afterEach
(路由跳转后的「后置守卫」,收尾逻辑)。
1. 初始化准备(代码开头)
先导入所需工具和配置,做基础设置:
- 导入
useUserStore
(用户状态:Token、角色、用户信息)、usePermissionStore
(权限状态:生成权限路由); - 导入
NProgress
并配置(关闭加载时的小圆圈showSpinner: false
),同时引入进度条样式; - 定义
whiteList
(免登录白名单):只有/login
页面允许未登录用户访问; - 导入路由实例
router
,后续用它处理路由跳转和动态添加路由。
2. 前置守卫:router.beforeEach(跳转前的核心校验)
每次用户切换路由(比如从 /login
到 /dashboard
),都会先执行这个函数。函数接收 3 个参数:
to
:要跳转到的「目标路由」(比如/dashboard
);from
:从哪个「来源路由」跳转过来(比如/login
);next
:控制路由是否继续跳转的「放行函数」(必须调用,否则路由会卡住)。
下面按代码逻辑分「有 Token(已登录)」和「无 Token(未登录)」两种场景拆解:
场景 1:用户有 Token(已登录,通过 getToken()
从 Cookie 取到 Token)
const hasToken = getToken() // 从Cookie获取Token,判断是否已登录
if (hasToken) {// 子场景1.1:已登录却要访问「登录页」→ 强制跳转到首页(避免重复登录)if (to.path === '/login') {next({ path: '/' }) // 跳转到首页NProgress.done() // 进度条提前结束(因为已跳转,不用等后续)} // 子场景1.2:已登录且访问的是「非登录页」→ 校验权限else {// 第一步:判断用户是否已有「角色权限」(比如 ['admin'] 或 ['editor'])const userStore = useUserStore()const hasRoles = userStore.roles && userStore.roles.length > 0// 子场景1.2.1:已有角色权限 → 直接放行,允许访问目标页面if (hasRoles) {next() // 正常跳转} // 子场景1.2.2:无角色权限 → 自动拉取用户信息+生成权限路由else {try {// ① 拉取用户信息:从后端获取当前用户的角色(如 ['admin'])const res = await userStore.getUserInfo() const roles = res.data.roles // 拿到用户角色(必须是数组格式)// ② 生成「用户可访问的路由」:根据角色过滤出有权限的路由const permissionStore = usePermissionStore()const accessRoutes = await permissionStore.generateRoutes(roles) // (比如 admin 能看到 /permission 路由,editor 看不到)// ③ 动态添加路由:把生成的权限路由「挂载」到路由实例上accessRoutes.forEach(route => {router.addRoute(route) // 这样用户才能访问这些权限路由})// ④ hack 方法:确保动态路由添加完成后再跳转// 用 { ...to, replace: true } 重新触发一次路由,避免「路由未加载完导致404」// replace: true 表示不留下历史记录(比如登录后跳转到目标页,回退不会回到登录页)next({ ...to, replace: true })} // 异常处理:拉取用户信息失败(如 Token 过期、无效)catch (error) {// ① 清除无效 Token(重置用户状态)const userStore = useUserStore()await userStore.resetToken() // ② 跳转到登录页,并记录「当前想访问的页面」(redirect参数)// 比如用户想访问 /dashboard,会跳转到 /login?redirect=/dashboard,登录后自动返回next(`/login?redirect=${to.path}`) NProgress.done() // 进度条结束}}}
}
场景 2:用户无 Token(未登录)
else {// 子场景2.1:目标页面在「免登录白名单」(只有 /login)→ 直接放行if (whiteList.includes(to.path)) {next() // 允许访问登录页} // 子场景2.2:目标页面不在白名单(如 /dashboard、/permission)→ 强制跳登录页else {// 跳转到登录页,并携带「想访问的页面」(redirect参数)next(`/login?redirect=${to.path}`) NProgress.done() // 进度条结束}
}
3. 后置守卫:router.afterEach(跳转后的收尾)
不管路由跳转成功与否,最终都会执行这个函数,只有一个作用:
- 结束
NProgress
进度条(隐藏顶部加载条),告诉用户「页面跳转完成」。
router.afterEach(() => {NProgress.done() // 进度条结束
})
3.关键逻辑总结(一句话理解)
- 未登录用户:想访问任何非登录页 → 踢去登录页,并记下来「想去哪」;
- 已登录用户:
- 想访问登录页 → 踢去首页;
- 第一次访问非登录页 → 自动拉角色、生成权限路由,再放行;
- 已拉过角色 → 直接放行;
- 所有跳转:顶部显示进度条,页面标题自动更新,跳转完进度条消失。
四、根据路由元信息动态设置页面标题
在utils中定义一个js文件:get-page-title.js
import defaultSettings from '@/settings'const title = defaultSettings.title || '忘川上回望'export default function getPageTitle(pageTitle) {if (pageTitle) {return `${pageTitle} - ${title}`}return `${title}`
}
setting文件是对整个系统的管理和小设置(创建这个文件需要与main.js文件同级就好):
// src/settings.js
export default {title: '忘川上回望',/*** @type {boolean} true | false* @description 是否显示右侧设置面板*/showSettings: true,/*** @type {boolean} true | false* @description 是否需要标签页(tagsView)*/tagsView: true,/*** @type {boolean} true | false* @description 是否固定页面头部(Header)*/fixedHeader: false,/*** @type {boolean} true | false* @description 是否在侧边栏(Sidebar)显示 Logo*/sidebarLogo: false,/*** @type {string | array} 'production' | ['production', 'development']* @description 是否需要显示错误日志组件* 默认仅在生产环境(production)显示* 若需在开发环境也显示,可传入 ['production', 'development']*/errorLog: 'production'
}
配合配置路由守卫的文件使用src/permission.js:
//关键代码。完整的看前面
import getPageTitle from '@/utils/get-page-title'
router.beforeEach(async(to, from, next) => {// 启动进度条NProgress.start()// 设置页面标题document.title = getPageTitle(to.meta.title)// 确定用户是否已登录(有token)const hasToken = getToken()
五、根据角色权限获取对应的路由列表为后续显示的菜单栏做准备
我先给下我配置的路由文件,里面分为公共路由和权限路由
router/index.js
// 1. 引入所需模块
import { createRouter, createWebHashHistory } from "vue-router";// 2. 引入布局组件
import Layout from "@/layout/index.vue";// 3. 定义固定路由(所有人能访问)
export const constantRoutes = [// 重定向路由,用于处理路由跳转{path: "/redirect",component: Layout,hidden: true,children: [{path: "/redirect/:path(.*)",component: () => import("@/views/public/redirect/index.vue"),},],},// 左侧菜单栏第一个页面{path: "/",component: Layout,redirect: "/dashboard",children: [{path: "dashboard",component: () => import("@/views/public/dashboard/index.vue"),name: "Dashboard",meta: { title: "Dashboard", icon: "dashboard", affix: true },},],},// 其他固定路由...{path: "/login",component: () => import("@/views/public/login/index.vue"),hidden: true,},{path: "/404",component: () => import("@/views/public/error-page/404.vue"),hidden: true,},{path: "/401",component: () => import("@/views/public/error-page/401.vue"),hidden: true,},
];// 4. 定义动态路由(需要权限)
export const asyncRoutes = [{path: '/permission',component: Layout,redirect: '/permission/index',alwaysShow: true,name: 'Permission',meta: {title: 'Permission',icon: 'lock',roles: ['admin', 'editor'] },children: [{path: 'index',component: () => import('@/views/permission/index.vue'),name: 'PagePermission',meta: {title: 'Page Permission',roles: ['admin']}}]},// 其他动态路由...
];// 5. 创建路由实例
const router = createRouter({history: createWebHashHistory(), // 使用哈希模式routes: constantRoutes, // 初始只加载固定路由
});// 6. 重置路由的函数(用于退出登录等场景)
export function resetRouter() {const newRouter = createRouter({history: createWebHashHistory(),routes: constantRoutes,});router.matcher = newRouter.matcher; // 重置路由
}// 7. 导出路由实例
export default router;
在permissionStore状态管理中,写好了处理这些列表的方法,然后做了存储。这里会根据角色进行过滤出对应的菜单列表。
import { defineStore } from 'pinia'
import { asyncRoutes, constantRoutes } from '@/router'/*** 使用meta.role判断当前用户是否有访问权限* @param roles 用户角色列表* @param route 路由配置*/
function hasPermission(roles, route) {if (route.meta && route.meta.roles) {// 判断用户角色中是否有路由所需角色return roles.some(role => route.meta.roles.includes(role))} else {// 没有配置角色的路由默认可以访问return true}
}/*** 递归过滤异步路由表* @param routes 异步路由列表* @param roles 用户角色列表*/
function filterAsyncRoutes(routes, roles) {const res = []routes.forEach(route => {const tmp = { ...route }if (hasPermission(roles, tmp)) {if (tmp.children) {// 递归处理子路由tmp.children = filterAsyncRoutes(tmp.children, roles)}res.push(tmp)}})return res
}export const usePermissionStore = defineStore('permission', {state: () => ({routes: [], // 完整路由列表(常量路由+动态路由)addRoutes: [] // 动态添加的路由列表}),actions: {/*** 生成路由* @param roles 用户角色列表* @returns Promise 包含有权限的路由列表*/generateRoutes(roles) {return new Promise(resolve => {let accessedRoutes// 如果是管理员角色,拥有所有异步路由权限if (roles.includes('admin')) {accessedRoutes = asyncRoutes || []} else {// 非管理员角色,根据角色过滤路由accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)}// 直接在action中修改状态,无需通过mutationthis.addRoutes = accessedRoutesthis.routes = constantRoutes.concat(accessedRoutes)console.log("该角色完整的路由",this.routes);resolve(this.routes)})},/*** 重置路由状态(可选,用于登出时)*/resetRoutes() {this.routes = []this.addRoutes = []}}
})
而在使用时,也是在路由守卫中的没有获取用户角色时,获取角色,然后在次调用permissionStore状态管理里面的方法进行过滤。
还是一样,完整的permission.js看前面,这里指出关键的位置:
else {try { // 获取用户信息// 注意:roles必须是一个数组,例如: ['admin'] 或 ['developer','editor']const res = await userStore.getUserInfo()const roles = res.data.roles// 获取权限Store实例const permissionStore = usePermissionStore()// 根据角色生成可访问的路由映射const accessRoutes = await permissionStore.generateRoutes(roles)console.log("accessRoutes",accessRoutes);// 动态添加可访问路由accessRoutes.forEach(route => {router.addRoute(route)})// 确保addRoute完成的hack方法// 设置replace: true,这样导航就不会留下历史记录next({ ...to, replace: true })}
const accessRoutes = await permissionStore.generateRoutes(roles)
到此,我们就实现了从登录到获取获取角色菜单列表的过程!