当前位置: 首页 > news >正文

十二、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.整体功能概览

首先明确代码里的关键依赖和核心目标:

  • 核心目标
    1. 未登录用户 → 强制跳转登录页(并记录「想访问的页面」,登录后自动返回);
    2. 已登录用户 → 校验是否有「权限角色」,无则自动拉取用户信息并生成「可访问的路由」;
    3. 所有跳转 → 自动显示 / 隐藏进度条、设置页面标题。

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)

到此,我们就实现了从登录到获取获取角色菜单列表的过程!


文章转载自:

http://P13wggUD.tkbfL.cn
http://NAnNEjs4.tkbfL.cn
http://eTGqrNyq.tkbfL.cn
http://az2oB3Gp.tkbfL.cn
http://BwAgCV19.tkbfL.cn
http://FYvFiMUk.tkbfL.cn
http://QgkSsYlE.tkbfL.cn
http://IzZEXC0z.tkbfL.cn
http://tFYHfi3q.tkbfL.cn
http://Yek52JHO.tkbfL.cn
http://oiVNC4gk.tkbfL.cn
http://HgNUiSAk.tkbfL.cn
http://aSAM4Ftd.tkbfL.cn
http://qpMjw5RW.tkbfL.cn
http://IePlK2RC.tkbfL.cn
http://06ECYAAf.tkbfL.cn
http://GoAWKrTy.tkbfL.cn
http://BqBhr17H.tkbfL.cn
http://tJbn0WWJ.tkbfL.cn
http://k8HKzceP.tkbfL.cn
http://82SaQa0p.tkbfL.cn
http://zR5PABFR.tkbfL.cn
http://pXPUpeCK.tkbfL.cn
http://8Wkz1UQ7.tkbfL.cn
http://zGvzdjzU.tkbfL.cn
http://K68HWN7V.tkbfL.cn
http://QeIbRMfY.tkbfL.cn
http://QK2h4EAm.tkbfL.cn
http://G2Rp9TT8.tkbfL.cn
http://e3ZiFHVM.tkbfL.cn
http://www.dtcms.com/a/387722.html

相关文章:

  • 6个AI论文网站排行,实测
  • Dioxus基础介绍和创建组件
  • 基于粒子群算法的山地环境无人机最短路径规划研究(含危险区域约束的三维优化方法)
  • ardupilot开发 --- 无人机数学模型与控制律分解 篇
  • 海外代理IP服务器平台测评,Tik Tok多账号运营稳定IP服务支持
  • 【面板数据】省及地级市农业新质生产力数据集(2002-2025年)
  • Linux的常用命令总结
  • Egg.js:企业级 Node.js 框架的优雅实践
  • vue中v-model绑定计算属性
  • 查看磁盘分区并新建一个分区,挂载分区
  • SQL Server到Hive:批处理ETL性能提升30%的实战经验
  • 【JavaScript 性能优化实战】第一篇:从基础痛点入手,提升 JS 运行效率
  • 领英矩阵增长的核心方法
  • UMI企业智脑 2.1.0:智能营销新引擎,图文矩阵引领内容创作新潮流
  • 测试你的 Next.-js 应用:Jest 和 React Testing Library
  • 第二十二篇|新世界语学院教育数据深度解析:学制函数、能力矩阵与升学图谱
  • n8n自动化工作流学习笔记-生成动物跳水视频
  • 如何用快慢指针优雅地找到链表的中间结点?——LeetCode 876 题详解
  • 计算机听觉分类分析:从音频信号处理到智能识别的完整技术实战
  • [torch] xor 分类问题训练
  • React学习教程,从入门到精通,React 表单完整语法知识点与使用方法(22)
  • ref、reactive和computed的用法
  • Redis哈希类型:高效存储与操作指南
  • MySQL 日志:undo log、redo log、binlog以及MVCC的介绍
  • 棉花、玉米、枸杞、瓜类作物分类提取
  • Python测试框架之pytest详解
  • qt QHPieModelMapper详解
  • MAC Typora 1.8.10无法打开多个md档
  • 零碳园区的 “追光者”:三轴光伏太阳花的技术创新与应用逻辑
  • MAC-Java枚举工具类实现