> 技术文档 > 手撕RuoYi-Vue代码 | 前端登录全流程详解(附流程图)_若依登录流程

手撕RuoYi-Vue代码 | 前端登录全流程详解(附流程图)_若依登录流程

作者声明:本文章仅为学习交流,不可用于其他用途
若依/RuoYi官网
RuoYi_Gitee

使用技术栈

​​使用技术栈:Vue.js​+​​Vue Router+router+Vuex+​​Element UI​

前端登录流程

流程图解

#mermaid-svg-VUGIxiKtchokREqT {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-VUGIxiKtchokREqT .error-icon{fill:#552222;}#mermaid-svg-VUGIxiKtchokREqT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VUGIxiKtchokREqT .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-VUGIxiKtchokREqT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VUGIxiKtchokREqT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VUGIxiKtchokREqT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VUGIxiKtchokREqT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VUGIxiKtchokREqT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VUGIxiKtchokREqT .marker.cross{stroke:#333333;}#mermaid-svg-VUGIxiKtchokREqT svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VUGIxiKtchokREqT .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VUGIxiKtchokREqT text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-VUGIxiKtchokREqT .actor-line{stroke:grey;}#mermaid-svg-VUGIxiKtchokREqT .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-VUGIxiKtchokREqT .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-VUGIxiKtchokREqT #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-VUGIxiKtchokREqT .sequenceNumber{fill:white;}#mermaid-svg-VUGIxiKtchokREqT #sequencenumber{fill:#333;}#mermaid-svg-VUGIxiKtchokREqT #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-VUGIxiKtchokREqT .messageText{fill:#333;stroke:#333;}#mermaid-svg-VUGIxiKtchokREqT .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VUGIxiKtchokREqT .labelText,#mermaid-svg-VUGIxiKtchokREqT .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-VUGIxiKtchokREqT .loopText,#mermaid-svg-VUGIxiKtchokREqT .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-VUGIxiKtchokREqT .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-VUGIxiKtchokREqT .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-VUGIxiKtchokREqT .noteText,#mermaid-svg-VUGIxiKtchokREqT .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-VUGIxiKtchokREqT .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VUGIxiKtchokREqT .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VUGIxiKtchokREqT .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VUGIxiKtchokREqT .actorPopupMenu{position:absolute;}#mermaid-svg-VUGIxiKtchokREqT .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-VUGIxiKtchokREqT .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VUGIxiKtchokREqT .actor-man circle,#mermaid-svg-VUGIxiKtchokREqT line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-VUGIxiKtchokREqT :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} User Login.vue API Vuex Router 输入账号密码 提交登录请求 返回Token 存储Token到Cookies 跳转到首页 检查Token有效性 获取用户权限信息 返回角色/权限 生成动态路由 渲染授权页面 User Login.vue API Vuex Router

流程图

#mermaid-svg-qE3kWTRt5BPPe6DW {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-qE3kWTRt5BPPe6DW .error-icon{fill:#552222;}#mermaid-svg-qE3kWTRt5BPPe6DW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qE3kWTRt5BPPe6DW .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-qE3kWTRt5BPPe6DW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qE3kWTRt5BPPe6DW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qE3kWTRt5BPPe6DW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qE3kWTRt5BPPe6DW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qE3kWTRt5BPPe6DW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qE3kWTRt5BPPe6DW .marker.cross{stroke:#333333;}#mermaid-svg-qE3kWTRt5BPPe6DW svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qE3kWTRt5BPPe6DW .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qE3kWTRt5BPPe6DW .cluster-label text{fill:#333;}#mermaid-svg-qE3kWTRt5BPPe6DW .cluster-label span{color:#333;}#mermaid-svg-qE3kWTRt5BPPe6DW .label text,#mermaid-svg-qE3kWTRt5BPPe6DW span{fill:#333;color:#333;}#mermaid-svg-qE3kWTRt5BPPe6DW .node rect,#mermaid-svg-qE3kWTRt5BPPe6DW .node circle,#mermaid-svg-qE3kWTRt5BPPe6DW .node ellipse,#mermaid-svg-qE3kWTRt5BPPe6DW .node polygon,#mermaid-svg-qE3kWTRt5BPPe6DW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qE3kWTRt5BPPe6DW .node .label{text-align:center;}#mermaid-svg-qE3kWTRt5BPPe6DW .node.clickable{cursor:pointer;}#mermaid-svg-qE3kWTRt5BPPe6DW .arrowheadPath{fill:#333333;}#mermaid-svg-qE3kWTRt5BPPe6DW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qE3kWTRt5BPPe6DW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qE3kWTRt5BPPe6DW .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-qE3kWTRt5BPPe6DW .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-qE3kWTRt5BPPe6DW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qE3kWTRt5BPPe6DW .cluster text{fill:#333;}#mermaid-svg-qE3kWTRt5BPPe6DW .cluster span{color:#333;}#mermaid-svg-qE3kWTRt5BPPe6DW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qE3kWTRt5BPPe6DW :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} 安全防护 动态路由流程 Token流程 注册流程 登录流程 路由守卫流程 有Token 未加载 已加载 无Token 验证码校验 成功 失败 验证码校验 成功 失败 登录/注册成功 有效 无效 无Token 依赖Token Token加密存储 API请求限流 验证码防刷 敏感操作二次验证 生成权限路由表 用户角色加载 router.addRoutes 仅展示可访问路由 存储到Cookies Token生成 后续请求携带Token 后端校验Token 允许访问 清除Token并重定向登录 输入注册信息/验证码 访问/register 提交 验证码正确? 后端处理注册 显示验证码错误 自动登录或跳转登录页 显示注册错误 输入用户名/密码/验证码 访问/login 提交 验证码正确? 后端验证登录 显示验证码错误 存储Token 跳转首页/redirect 显示登录错误 NProgress.start 开始 存在Token? 目标路径是/login? 重定向到首页 NProgress.done 是否在白名单? 直接放行 用户角色已加载? 调用GetInfo 成功? 生成动态路由 router.addRoutes 替换路由并放行 登出并跳转登录页 是否在白名单? 重定向到登录页?redirect参数 结束

login.vue

login页面代码的逻辑流程如下:
1.用户点击登录按钮,触发 handleLogin 方法。
2.验证表单数据是否符合规则。
3.如果验证通过,设置加载状态并处理“记住密码”功能。
4.调用 Vuex 的 Login Action。
5.登录成功后,跳转到指定页面;登录失败则重置状态并刷新验证码。

mermaid流程图

#mermaid-svg-hV986dg8ztTQhyou {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-hV986dg8ztTQhyou .error-icon{fill:#552222;}#mermaid-svg-hV986dg8ztTQhyou .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hV986dg8ztTQhyou .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-hV986dg8ztTQhyou .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hV986dg8ztTQhyou .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hV986dg8ztTQhyou .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hV986dg8ztTQhyou .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hV986dg8ztTQhyou .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hV986dg8ztTQhyou .marker.cross{stroke:#333333;}#mermaid-svg-hV986dg8ztTQhyou svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hV986dg8ztTQhyou .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-hV986dg8ztTQhyou .cluster-label text{fill:#333;}#mermaid-svg-hV986dg8ztTQhyou .cluster-label span{color:#333;}#mermaid-svg-hV986dg8ztTQhyou .label text,#mermaid-svg-hV986dg8ztTQhyou span{fill:#333;color:#333;}#mermaid-svg-hV986dg8ztTQhyou .node rect,#mermaid-svg-hV986dg8ztTQhyou .node circle,#mermaid-svg-hV986dg8ztTQhyou .node ellipse,#mermaid-svg-hV986dg8ztTQhyou .node polygon,#mermaid-svg-hV986dg8ztTQhyou .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hV986dg8ztTQhyou .node .label{text-align:center;}#mermaid-svg-hV986dg8ztTQhyou .node.clickable{cursor:pointer;}#mermaid-svg-hV986dg8ztTQhyou .arrowheadPath{fill:#333333;}#mermaid-svg-hV986dg8ztTQhyou .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hV986dg8ztTQhyou .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hV986dg8ztTQhyou .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-hV986dg8ztTQhyou .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-hV986dg8ztTQhyou .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hV986dg8ztTQhyou .cluster text{fill:#333;}#mermaid-svg-hV986dg8ztTQhyou .cluster span{color:#333;}#mermaid-svg-hV986dg8ztTQhyou div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hV986dg8ztTQhyou :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} 格式错误 格式正确 用户不存在 密码错误 验证成功 访问登录页面 输入用户名密码 前端验证格式 显示错误提示 axios提交登录请求 后端验证凭证 返回用户不存在 返回密码错误 生成JWT Token 返回用户信息和Token 存储Token到Vuex/Pinia 跳转到Dashboard 登录成功

登陆表单

登陆表单使用了

<template> <!-- <!-- 这是一个带有 class 属性为 \"login\" 的 div 元素,没有使用 v-if bang\'ding --> <div class=\"login\"> <!-- 这是一个带有 class 属性为 \"login-form\" 的 el-form 元素,使用了 v-model 指令绑定了 loginForm 数据 --> <el-form ref=\"loginForm\" :model=\"loginForm\" :rules=\"loginRules\" class=\"login-form\"> <!-- title: process.env.VUE_APP_TITLE --> <h3 class=\"title\">{{title}}</h3> <!-- <el-form-item> 本身提供统一的 ​​标签、输入框、错误提示​​ 的布局结构 prop=\"username\" 声明该表单项对应表单数据模型中的 username 字段 --> <el-form-item prop=\"username\"> <!-- el-input 是 Element UI 提供的输入框组件,v-model 指令用于双向绑定数据 --> <!-- type=\"text\" 指定输入框的类型为文本框,auto-complete=\"off\" 禁用浏览器自动完成功能 --> <!-- placeholder=\"账号\" 提示用户输入账号信息 --> <!-- slot=\"prefix\" 用于在输入框前添加一个图标,icon-class=\"user\" 指定图标的名称 --> <!-- class=\"el-input__icon input-icon\" 用于设置图标的样式 --> <el-input v-model=\"loginForm.username\" type=\"text\" auto-complete=\"off\" placeholder=\"账号\" > <svg-icon slot=\"prefix\" icon-class=\"user\" class=\"el-input__icon input-icon\" /> </el-input> </el-form-item> <!-- el-form-item 组件用于显示密码输入框,prop=\"password\" 声明该表单项对应表单数据模型中的 password 字段 --> <!-- type=\"password\" 指定输入框的类型为密码框,auto-complete=\"off\" 禁用浏览器自动完成功能 --> <!-- placeholder=\"密码\" 提示用户输入密码信息 --> <!-- slot=\"prefix\" 用于在输入框前添加一个图标,icon-class=\"password\" 指定图标的名称 --> <!-- class=\"el-input__icon input-icon\" 用于设置图标的样式 --> <!-- @keyup.enter.native=\"handleLogin\" 监听回车键事件,触发登录方法 --> <el-form-item prop=\"password\"> <el-input v-model=\"loginForm.password\" type=\"password\" auto-complete=\"off\" placeholder=\"密码\" @keyup.enter.native=\"handleLogin\" > <svg-icon slot=\"prefix\" icon-class=\"password\" class=\"el-input__icon input-icon\" /> </el-input> </el-form-item> <!-- 验证码 --> <el-form-item prop=\"code\" v-if=\"captchaEnabled\"> <!-- el-form-item 组件用于显示验证码输入框,prop=\"code\" 声明该表单项对应表单数据模型中的 code 字段 --> <!-- type=\"text\" 指定输入框的类型为文本框,auto-complete=\"off\" 禁用浏览器自动完成功能 --> <!-- placeholder=\"验证码\" 提示用户输入验证码信息 --> <!-- slot=\"prefix\" 用于在输入框前添加一个图标,icon-class=\"validCode\" 指定图标的名称 --> <!-- class=\"el-input__icon input-icon\" 用于设置图标的样式 --> <!-- v-model=\"loginForm.code\" 双向绑定验证码输入框的值 --> <!-- style=\"width: 63%\" 设置输入框的宽度为 63% --> <!-- @keyup.enter.native=\"handleLogin\" 监听回车键事件,触发登录方法 --> <!-- el-input 组件用于显示验证码输入框,v-model 指令用于双向绑定数据 --> <el-input v-model=\"loginForm.code\" auto-complete=\"off\" placeholder=\"验证码\" style=\"width: 63%\" @keyup.enter.native=\"handleLogin\" > <svg-icon slot=\"prefix\" icon-class=\"validCode\" class=\"el-input__icon input-icon\" /> </el-input> <!-- el-form-item 组件用于显示验证码图片,:src 指令绑定验证码图片的 URL --> <div class=\"login-code\"> <img :src=\"codeUrl\" @click=\"getCode\" class=\"login-code-img\"/> </div> </el-form-item> <el-checkbox v-model=\"loginForm.rememberMe\" style=\"margin:0px 0px 25px 0px;\">记住密码</el-checkbox> <!-- el-form-item 组件用于显示登录按钮,style=\"width:100%\" 设置按钮的宽度为 100% --> <!-- :loading=\"loading\" 绑定 loading 状态,:type=\"primary\" 设置按钮的类型为主要按钮 --> <!-- size=\"medium\" 设置按钮的大小为中等 --> <!-- @click.native.prevent=\"handleLogin\" 监听点击事件,触发登录方法 --> <el-form-item style=\"width:100%;\"> <el-button :loading=\"loading\" size=\"medium\" type=\"primary\" style=\"width:100%;\" @click.native.prevent=\"handleLogin\" > <span v-if=\"!loading\">登 录</span> <span v-else>登 录 中...</span> </el-button> <div style=\"float: right;\" v-if=\"register\"> <router-link class=\"link-type\" :to=\"\'/register\'\">立即注册</router-link> </div> </el-form-item> </el-form> <!-- 底部 --> <div class=\"el-login-footer\"> <span>Copyright © 2018-2025 ruoyi.vip All Rights Reserved.</span> </div> </div></template>

模块导入

// 从 API 模块中导入 getCodeImg()方法,用于获取验证码图片。import { getCodeImg } from \"@/api/login\";// 从 js-cookie 库中导入 Cookies 对象import Cookies from \"js-cookie\";// encrypt` 和 `decrypt`:加密和解密工具,用于对密码进行加密存储和解密读取。import { encrypt, decrypt } from \'@/utils/jsencrypt\'

登录验证规则

 loginRules: { username: [ { required: true, trigger: \"blur\", message: \"请输入您的账号\" } ], password: [ { required: true, trigger: \"blur\", message: \"请输入您的密码\" } ], code: [{ required: true, trigger: \"change\", message: \"请输入验证码\" }] },

路由

 $route: { // 监听路由变化,当路由变化时,检查是否有重定向的地址 handler: function(route) { // 如果路由的查询参数中有 redirect,则将其赋值给 redirect 属性 // 否则将 redirect 属性设置为 undefined this.redirect = route.query && route.query.redirect; }, immediate: true }

处理登录表单的验证和提交

 getCode() { // 调用 getCodeImg 方法获取验证码图片 // 使用 Promise 的 then 方法处理异步请求的结果 // captchaEnabled 属性用于判断是否启用验证码功能 getCodeImg().then(res => { // 如果返回的结果中没有 captchaEnabled 属性,则默认启用验证码功能 // 条件 ? 值1 : 值2 // ===可以避免类型转换的问题,确保比较的值是相同类型的 this.captchaEnabled = res.captchaEnabled === undefinedtrue : res.captchaEnabled; if (this.captchaEnabled) { // 如果启用验证码功能,则设置验证码图片的 URL 和 UUID this.codeUrl = \"data:image/gif;base64,\" + res.img; this.loginForm.uuid = res.uuid; } });

获取cookie

 getCookie() { // Cookies.get(\"username\") 获取存储在 Cookie 中的用户名 const username = Cookies.get(\"username\"); // Cookies.get(\"password\") 获取存储在 Cookie 中的密码 const password = Cookies.get(\"password\"); // Cookies.get(\'rememberMe\') 获取存储在 Cookie 中的 rememberMe 值 const rememberMe = Cookies.get(\'rememberMe\') // 如果 rememberMe 为 true,则将用户名和密码存储在 loginForm 中 // 否则将 loginForm 中的用户名和密码设置为默认值 this.loginForm = { username: username === undefinedthis.loginForm.username : username, password: password === undefinedthis.loginForm.password : decrypt(password), // rememberMe === undefined ? false : Boolean(rememberMe) 将 rememberMe 转换为布尔值 rememberMe: rememberMe === undefinedfalse : Boolean(rememberMe) }; },

handleLogin

 handleLogin() { // 调用 this.$refs.loginForm.validate 方法验证登录表单 // validate 方法会根据 loginRules 中定义的规则进行验证 this.$refs.loginForm.validate(valid => { // valid 是一个布尔值,表示验证是否通过 if (valid) { // 如果验证通过,则调用 this.$store.dispatch 方法触发登录操作 this.loading = true; // 如果启用验证码功能,则将验证码和 UUID 添加到登录表单中 if (this.loginForm.rememberMe) { Cookies.set(\"username\", this.loginForm.username, { expires: 30 }); Cookies.set(\"password\", encrypt(this.loginForm.password), { expires: 30 }); Cookies.set(\'rememberMe\', this.loginForm.rememberMe, { expires: 30 }); } // 如果 rememberMe 为 false,则删除存储在 Cookie 中的用户名和密码 else { Cookies.remove(\"username\"); Cookies.remove(\"password\"); Cookies.remove(\'rememberMe\'); } /**  * this.$store.dispatch(actionName, payload, options); actionName:要触发的 action 名称,String类型 payload:要传递给 action 的参数,可以是任意类型 options:可选参数,Object类型 options: { root: true } 表示在全局命名空间中触发 action this.loginForm 是登录表单的数据模型,包含用户名、密码、验证码等字段 then 方法用于处理异步请求的结果 * */ this.$store.dispatch(\"Login\", this.loginForm).then(() => { // this.$router.push({ path: this.redirect || \"/\" }) 是登录成功后跳转的路由地址  this.$router.push({ path: this.redirect || \"/\" }).catch(()=>{}); }) // catch 方法用于处理异步请求的错误 .catch(() => { // 如果登录成功,则将 loading 设置为 false // 否则将 loading 设置为 false // 并重新获取验证码图片 this.loading = false; if (this.captchaEnabled) {  this.getCode(); } }); } }); } }

request.js

基于 ​​Axios​​ 的 HTTP 客户端配置,集成了 ​​Element UI​​ 的通知组件,用于显示请求过程中的各种提示信息,并包含了一些自定义的功能,如 ​​Token 认证​​、​​防止重复提交​​、​​错误处理​​以及​​文件下载​​。
实现了以下主要功能:
1.​统一的请求配置​​:

  • 设置默认的请求头、基础 URL 和超时时间。
    ​​2.请求拦截器​​:
  • 自动添加 Token 认证信息。
  • 处理 GET 请求的参数拼接。
  • 防止重复提交请求。
    ​3.​响应拦截器​​:
  • 统一处理不同状态码的响应,包括成功、未授权、服务器错误等。
  • 提供友好的用户提示,使用 Element UI 的 Message 和 Notification 组件。
    ​4.​文件下载功能​​:
  • 提供一个通用的下载方法,简化文件下载的实现。
  • 支持大文件的 Blob 数据处理和下载进度提示。

mermaid流程图

graph TD; A[开始] --> B[创建Axios实例service]; B --> C[设置默认请求头Content-Type: application/json;charset=utf-8]; C --> D[配置baseURL和timeout]; D --> E[请求拦截器]; E --> F{是否需要设置Token?}; F -- 是 --> G[在请求头中添加Authorization: Bearer ]; F -- 否 --> H[跳过设置Token]; E --> I{是否需要防止数据重复提交?}; I -- 是 --> J[如果是GET请求]; J --> K[将params参数拼接到URL中]; I -- 是 --> L[如果是POST或PUT请求]; L --> M[创建requestObj对象]; M --> N[计算请求数据大小]; N --> O{请求数据大小>5MB?}; O -- 是 --> P[跳过重复提交检查]; O -- 否 --> Q[检查缓存中是否存在相同的请求]; Q --> R{存在相同请求?}; R -- 是 --> S[返回Promise.reject数据正在处理,请勿重复提交]; R -- 否 --> T[将requestObj存入缓存]; E --> U[返回配置对象config]; U --> V{发生错误?}; V -- 是 --> W[记录错误日志]; V -- 否 --> X[结束]; W --> X;

​配置 Axios 实例​

/** * downloadLoadingInstance​​: 用于存储全局加载实例,以便在下载文件时显示加载状态。 * ​​isRelogin​​: 一个响应式对象,用于控制重新登录弹窗的显示状态。 */let downloadLoadingInstance;// 是否显示重新登录export let isRelogin = { show: false };/** * service​​:这是你创建的 Axios 实例。 * ​​interceptors.request.use​​:添加一个请求拦截器,接收一个回调函数,该函数会在每次发送请求之前被调用。 * ​config​​:这是即将发送的请求的配置对象,包含 URL、方法、头信息、参数等。 */// 创建axios实例,​​axios.defaults.headers​​: 设置默认的请求头,指定 Content-Type 为 application/json;charset=utf-8。axios.defaults.headers[\'Content-Type\'] = \'application/json;charset=utf-8\'// ​axios.create​​: 创建一个新的 Axios 实例 service,并配置基础 URL 和超时时间。const service = axios.create({ // axios中请求配置有baseURL选项,表示请求URL公共部分 baseURL: process.env.VUE_APP_BASE_API, // 超时 timeout: 10000})
  • downloadLoadingInstance​​: 用于存储全局加载实例,以便在下载文件时显示加载状态。
  • ​​isRelogin​​: 一个响应式对象,用于控制重新登录弹窗的显示状态。
  • ​service​​: 创建一个新的 Axios 实例,并配置基础 URL 和超时时间。
const isToken = (config.headers || {}).isToken === false;
  • config.headers​​:请求头对象,通常用于设置 Authorization、Content-Type 等信息。
    (config.headers || {})​​:
  • 目的​​:确保 config.headers 不为 undefined 或 null,避免在访问 isToken 属性时抛出错误。
  • 解释​​:如果 config.headers 不存在,则使用一个空对象 {} 作为默认值。
    ​​.isToken​​:
  • 含义​​:这是一个自定义的请求头字段,用于指示是否需要添加 Token。
  • 类型​​:布尔值(true 或 false)。
    ​​=== false​​:
  • ​​判断​​:检查 isToken 是否明确设置为 false。
    ​​
    逻辑​​:
  • 如果 config.headers.isToken 存在且为 false,则 isToken 变量为 true,表示不需要添加 Token。
  • 否则,isToken 变量为 false,表示需要添加 Token。

请求拦截器

逻辑流程​​:

  1. 检查请求配置中的 isToken 标志是否为 false。
  2. 如果 isToken 不为 false 且用户已登录(即 getToken() 返回有效的 Token),则在请求头中添加 Authorization 字段,值为 Bearer 。
  3. 如果 isToken 为 false,则不添加 Authorization 字段,适用于那些不需要认证的请求。

## 请求拦截器​

 const isToken = (config.headers || {}).isToken === false // 是否需要防止数据重复提交 const isRepeatSubmit = (config.headers || {}).repeatSubmit === false if (getToken() && !isToken) { // 让每个请求携带自定义token 请根据实际情况自行修改 config.headers[\'Authorization\'] = \'Bearer \' + getToken() } // get请求映射params参数 if (config.method === \'get\' && config.params) { let url = config.url + \'?\' + tansParams(config.params); url = url.slice(0, -1); config.params = {}; config.url = url; } if (!isRepeatSubmit && (config.method === \'post\' || config.method === \'put\')) { const requestObj = { url: config.url, data: typeof config.data === \'object\'JSON.stringify(config.data) : config.data, time: new Date().getTime() } const requestSize = Object.keys(JSON.stringify(requestObj)).length; // 请求数据大小 const limitSize = 5 * 1024 * 1024; // 限制存放数据5M if (requestSize >= limitSize) { console.warn(`[${config.url}]: ` + \'请求数据大小超出允许的5M限制,无法进行防重复提交验证。\') return config; } const sessionObj = cache.session.getJSON(\'sessionObj\') if (sessionObj === undefined || sessionObj === null || sessionObj === \'\') { cache.session.setJSON(\'sessionObj\', requestObj) } else { const s_url = sessionObj.url;  // 请求地址 const s_data = sessionObj.data; // 请求数据 const s_time = sessionObj.time; // 请求时间 const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交 if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) { const message = \'数据正在处理,请勿重复提交\'; console.warn(`[${s_url}]: ` + message) return Promise.reject(new Error(message)) } else { cache.session.setJSON(\'sessionObj\', requestObj) } } } return config}, error => { console.log(error) Promise.reject(error)})

Token认证

  • 检查当前请求是否需要携带 Token(通过 isToken 标志)。
  • 如果用户已登录(getToken() 返回有效的 Token)且当前请求不需要排除 Token,则在请求头中添加 Authorization 字段。
    GET 请求参数处理​​
  • 对于 GET 请求,如果有 params 参数,使用 tansParams 函数将其序列化并拼接到 URL 中。
  • 清空 config.params,因为参数已经拼接到 URL 中。
    ​​防止重复提交​​
  • 针对 POST 和 PUT 请求,检查是否设置了 repeatSubmit 标志。
  • 如果未设置且请求数据大小不超过 5MB:
    创建一个 requestObj 对象,包含请求的 URL、数据和时间戳。
    计算请求数据大小。
    如果请求数据大小超过 5MB,则跳过重复提交检查。
    否则,检查缓存中是否存在相同的请求:
    1.如果存在相同请求,则阻止当前请求并返回一个被拒绝的 Promise,提示用户“数据正在处理,请勿重复提交”。
    2.如果不存在相同请求,则将 requestObj 存入缓存。

响应拦截器

service.interceptors.response.use(res => { // 未设置状态码则默认成功状态 const code = res.data.code || 200; // 获取错误信息 const msg = errorCode[code] || res.data.msg || errorCode[\'default\'] // 二进制数据则直接返回 if (res.request.responseType === \'blob\' || res.request.responseType === \'arraybuffer\') { return res.data } if (code === 401) { if (!isRelogin.show) { isRelogin.show = true; MessageBox.confirm(\'登录状态已过期,您可以继续留在该页面,或者重新登录\', \'系统提示\', { confirmButtonText: \'重新登录\', cancelButtonText: \'取消\', type: \'warning\' }).then(() => { isRelogin.show = false; store.dispatch(\'LogOut\').then(() => { location.href = \'/index\'; }) }).catch(() => { isRelogin.show = false; }); } return Promise.reject(\'无效的会话,或者会话已过期,请重新登录。\') } else if (code === 500) { Message({ message: msg, type: \'error\' }) return Promise.reject(new Error(msg)) } else if (code === 601) { Message({ message: msg, type: \'warning\' }) return Promise.reject(\'error\') } else if (code !== 200) { Notification.error({ title: msg }) return Promise.reject(\'error\') } else { return res.data } }, error => { console.log(\'err\' + error) let { message } = error; if (message == \"Network Error\") { message = \"后端接口连接异常\"; } else if (message.includes(\"timeout\")) { message = \"系统接口请求超时\"; } else if (message.includes(\"Request failed with status code\")) { message = \"系统接口\" + message.substr(message.length - 3) + \"异常\"; } Message({ message: message, type: \'error\', duration: 5 * 1000 }) return Promise.reject(error) })

​处理响应数据​​

  • 提取响应中的状态码 code,如果未设置,默认认为请求成功(200)。
  • 获取错误信息 msg,使用 errorCode 映射表进行转换。
    ​​二进制数据处理​​
  • 如果响应类型为 blob 或 arraybuffer,直接返回响应数据。
    ​​其他状态码处理​​:
  • ​​401(未授权):
    检查是否已经显示重新登录弹窗,避免多次触发。
    使用 MessageBox.confirm 显示确认对话框,提示用户会话已过期。
    用户选择“重新登录”后,调用 Vuex 的 LogOut 方法注销用户,并重定向到登录页面。
    如果用户选择“取消”,仅关闭弹窗。
  • 500(服务器内部错误)​​
    使用 Message 显示错误消息。
    返回一个被拒绝的 Promise,携带错误信息。
  • ​​601(其他特定错误)​​:
    使用 Message 显示警告消息。
    返回一个被拒绝的 Promise,携带通用错误标识。
  • ​​其他非 200 状态码​​:
    使用 Notification.error 显示错误通知。
    返回一个被拒绝的 Promise,携带通用错误标识。
  • ​错误处理​​:
    捕获响应过程中可能出现的任何错误,记录错误日志,并返回一个被拒绝的 Promise。

​文件下载方法​

export function download(url, params, filename, config) { downloadLoadingInstance = Loading.service({ text: \"正在下载数据,请稍候\", spinner: \"el-icon-loading\", background: \"rgba(0, 0, 0, 0.7)\", }) return service.post(url, params, { transformRequest: [(params) => { return tansParams(params) }], headers: { \'Content-Type\': \'application/x-www-form-urlencoded\' }, responseType: \'blob\', ...config }).then(async (data) => { const isBlob = blobValidate(data); if (isBlob) { const blob = new Blob([data]) saveAs(blob, filename) } else { const resText = await data.text(); const rspObj = JSON.parse(resText); const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode[\'default\'] Message.error(errMsg); } downloadLoadingInstance.close(); }).catch((r) => { console.error(r) Message.error(\'下载文件出现错误,请联系管理员!\') downloadLoadingInstance.close(); })}

显示加载状态​​:

  • 使用 Loading.service 显示全局加载动画,提示用户正在下载数据。
    ​​发送下载请求​​:
  • 使用 service.post 发送 POST 请求到指定的 url,并传递 params 参数。
  • 配置请求的 transformRequest,使用 tansParams 函数序列化参数。
  • 设置请求头 Content-Type 为 application/x-www-form-urlencoded。
  • 指定响应类型为 blob,用于处理二进制数据(如文件)。
  • 可选的额外配置通过 config 参数传入。
    ​​处理响应数据​​:
  • 使用 blobValidate 函数验证响应数据是否为有效的 Blob 对象。
    ​​如果是 Blob​​:
  • 创建一个新的 Blob 实例,并使用 saveAs 触发文件下载,文件名为 filename。
    ​​如果不是 Blob​​:
  • 将响应数据转换为文本,并解析为 JSON 对象。
  • 根据返回的 code 获取对应的错误信息,并使用 Message.error 显示错误提示。
    ​​关闭加载状态​​:
  • 无论请求成功还是失败,最终都会关闭加载动画,确保用户体验的一致性。
    ​​错误处理​​:
  • 捕获请求过程中可能出现的任何错误,记录错误日志,并通知用户下载文件时出现问题,建议联系管理员

导出

export default service
  • export default service​​:将封装好的 Axios 实例作为默认导出,供其他模块导入和使用。
  • ​export function download(…)​​:将通用的下载方法作为命名导出,供需要文件下载功能的模块使用。

login.js

流程图

#mermaid-svg-Cjobrn1HIubjicWg {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-Cjobrn1HIubjicWg .error-icon{fill:#552222;}#mermaid-svg-Cjobrn1HIubjicWg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Cjobrn1HIubjicWg .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-Cjobrn1HIubjicWg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Cjobrn1HIubjicWg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Cjobrn1HIubjicWg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Cjobrn1HIubjicWg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Cjobrn1HIubjicWg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Cjobrn1HIubjicWg .marker.cross{stroke:#333333;}#mermaid-svg-Cjobrn1HIubjicWg svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Cjobrn1HIubjicWg .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Cjobrn1HIubjicWg .cluster-label text{fill:#333;}#mermaid-svg-Cjobrn1HIubjicWg .cluster-label span{color:#333;}#mermaid-svg-Cjobrn1HIubjicWg .label text,#mermaid-svg-Cjobrn1HIubjicWg span{fill:#333;color:#333;}#mermaid-svg-Cjobrn1HIubjicWg .node rect,#mermaid-svg-Cjobrn1HIubjicWg .node circle,#mermaid-svg-Cjobrn1HIubjicWg .node ellipse,#mermaid-svg-Cjobrn1HIubjicWg .node polygon,#mermaid-svg-Cjobrn1HIubjicWg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Cjobrn1HIubjicWg .node .label{text-align:center;}#mermaid-svg-Cjobrn1HIubjicWg .node.clickable{cursor:pointer;}#mermaid-svg-Cjobrn1HIubjicWg .arrowheadPath{fill:#333333;}#mermaid-svg-Cjobrn1HIubjicWg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Cjobrn1HIubjicWg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Cjobrn1HIubjicWg .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-Cjobrn1HIubjicWg .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-Cjobrn1HIubjicWg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Cjobrn1HIubjicWg .cluster text{fill:#333;}#mermaid-svg-Cjobrn1HIubjicWg .cluster span{color:#333;}#mermaid-svg-Cjobrn1HIubjicWg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Cjobrn1HIubjicWg :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} 安全退出 调用 logout 用户点击退出 清除 Token 和用户信息 跳转登录页 用户信息 调用 getInfo 获取用户信息 登录后 保存角色/权限, 生成动态路由 注册流程 调用 getCodeImg 获取验证码 用户打开注册页 显示验证码和保存 UUID 用户填写注册信息 调用 register 提交注册 注册成功? 自动登录或跳转登录页 提示错误, 刷新验证码 登录流程 调用 getCodeImg 获取验证码 用户打开登录页 显示验证码图片和保存 UUID 用户输入账号/密码/验证码 调用 login 提交登录 登录成功? 保存 Token, 跳转首页 提示错误, 刷新验证码

代码

// 模块导入import request from \'@/utils/request\'/** * 登录方法 login * 向 /login 发送 POST 请求,提交登录信息。 * isToken: false:不自动在请求头中添加 token。 * repeatSubmit: false:防止用户短时间内重复提交登录请求。 * @param {*} username * @param {*} password * @param {*} code 用户输入的验证码 * @param {*} uuid 验证码的唯一标识(从 getCodeImg 获取) * @returns */export function login(username, password, code, uuid) { const data = { username, password, code, uuid } return request({ url: \'/login\', headers: { isToken: false, repeatSubmit: false }, method: \'post\', data: data })}/** * 注册方法 * data 包含注册信息,用户名、密码、邮箱、验证码等 * 向 /register 发送 POST 请求,提交注册信息 * @param {*} data * @returns */export function register(data) { return request({ url: \'/register\', headers: { isToken: false }, method: \'post\', data: data })}/** * ​​获取用户信息 getInfo * 向 /getInfo 发送 GET 请求,获取当前用户的详细信息(如角色、权限、头像等) * @returns */export function getInfo() { return request({ url: \'/getInfo\', method: \'get\' })}/** * 退出登录 * 功能​​:向 /logout 发送 POST 请求,通知后端清除登录状态。 * 前端配合​​:通常还需清除前端存储的 token 和用户信息。 * @returns */export function logout() { return request({ url: \'/logout\', method: \'post\' })}/** * 获取验证码 getCodeImg​ * 功能​​:向 /captchaImage 发送 GET 请求,获取验证码图片和关联的 uuid。 * 提交登录/注册时需将 code(用户输入)和 uuid 一起发送。 * @returns */export function getCodeImg() { return request({ url: \'/captchaImage\', headers: { isToken: false }, method: \'get\', timeout: 20000 })}

auth.js

持久化登录状态​​:通过 Cookie 存储 Token,用户刷新页面后仍保持登录。
​​权限控制​​:结合路由守卫(如 Vue Router 的 beforeEach)读取 Token 验证权限。
​​安全退出​​:主动删除 Token 实现退出登录。
流程图

#mermaid-svg-86Cbwxh8ha7w4Fwt {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-86Cbwxh8ha7w4Fwt .error-icon{fill:#552222;}#mermaid-svg-86Cbwxh8ha7w4Fwt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-86Cbwxh8ha7w4Fwt .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-86Cbwxh8ha7w4Fwt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-86Cbwxh8ha7w4Fwt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-86Cbwxh8ha7w4Fwt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-86Cbwxh8ha7w4Fwt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-86Cbwxh8ha7w4Fwt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-86Cbwxh8ha7w4Fwt .marker.cross{stroke:#333333;}#mermaid-svg-86Cbwxh8ha7w4Fwt svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-86Cbwxh8ha7w4Fwt .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-86Cbwxh8ha7w4Fwt .cluster-label text{fill:#333;}#mermaid-svg-86Cbwxh8ha7w4Fwt .cluster-label span{color:#333;}#mermaid-svg-86Cbwxh8ha7w4Fwt .label text,#mermaid-svg-86Cbwxh8ha7w4Fwt span{fill:#333;color:#333;}#mermaid-svg-86Cbwxh8ha7w4Fwt .node rect,#mermaid-svg-86Cbwxh8ha7w4Fwt .node circle,#mermaid-svg-86Cbwxh8ha7w4Fwt .node ellipse,#mermaid-svg-86Cbwxh8ha7w4Fwt .node polygon,#mermaid-svg-86Cbwxh8ha7w4Fwt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-86Cbwxh8ha7w4Fwt .node .label{text-align:center;}#mermaid-svg-86Cbwxh8ha7w4Fwt .node.clickable{cursor:pointer;}#mermaid-svg-86Cbwxh8ha7w4Fwt .arrowheadPath{fill:#333333;}#mermaid-svg-86Cbwxh8ha7w4Fwt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-86Cbwxh8ha7w4Fwt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-86Cbwxh8ha7w4Fwt .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-86Cbwxh8ha7w4Fwt .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-86Cbwxh8ha7w4Fwt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-86Cbwxh8ha7w4Fwt .cluster text{fill:#333;}#mermaid-svg-86Cbwxh8ha7w4Fwt .cluster span{color:#333;}#mermaid-svg-86Cbwxh8ha7w4Fwt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-86Cbwxh8ha7w4Fwt :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} 退出流程 调用removeToken删除Token 用户退出 权限校验 存在Token 无Token 调用getToken读取Token 访问受限页面 允许访问 跳转登录页 登录流程 后端返回Token 调用setToken存储到Cookie 用户登录

代码

// 模块导入import request from \'@/utils/request\'/** * 登录方法 login * 向 /login 发送 POST 请求,提交登录信息。 * isToken: false:不自动在请求头中添加 token。 * repeatSubmit: false:防止用户短时间内重复提交登录请求。 * @param {*} username * @param {*} password * @param {*} code 用户输入的验证码 * @param {*} uuid 验证码的唯一标识(从 getCodeImg 获取) * @returns */export function login(username, password, code, uuid) { const data = { username, password, code, uuid } return request({ url: \'/login\', headers: { isToken: false, repeatSubmit: false }, method: \'post\', data: data })}/** * 注册方法 * data 包含注册信息,用户名、密码、邮箱、验证码等 * 向 /register 发送 POST 请求,提交注册信息 * @param {*} data * @returns */export function register(data) { return request({ url: \'/register\', headers: { isToken: false }, method: \'post\', data: data })}/** * ​​获取用户信息 getInfo * 向 /getInfo 发送 GET 请求,获取当前用户的详细信息(如角色、权限、头像等) * @returns */export function getInfo() { return request({ url: \'/getInfo\', method: \'get\' })}/** * 退出登录 * 功能​​:向 /logout 发送 POST 请求,通知后端清除登录状态。 * 前端配合​​:通常还需清除前端存储的 token 和用户信息。 * @returns */export function logout() { return request({ url: \'/logout\', method: \'post\' })}/** * 获取验证码 getCodeImg​ * 功能​​:向 /captchaImage 发送 GET 请求,获取验证码图片和关联的 uuid。 * 提交登录/注册时需将 code(用户输入)和 uuid 一起发送。 * @returns */export function getCodeImg() { return request({ url: \'/captchaImage\', headers: { isToken: false }, method: \'get\', timeout: 20000 })}

permission.js

在路由守卫的beforeEach钩子中,首先启动了NProgress。接着检查是否存在token。如果有token,用户已登录,处理逻辑包括:如果目标路径是登录页,则重定向到首页;如果在白名单,直接放行;否则检查用户角色是否已加载。如果角色未加载,则调用GetInfo获取用户信息,生成动态路由,添加路由后跳转。如果过程中出错,则登出并跳转首页。如果用户没有token,检查是否在白名单,否则重定向到登录页,并携带重定向路径。

#mermaid-svg-wqkXjo7wwQqtk7Up {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-wqkXjo7wwQqtk7Up .error-icon{fill:#552222;}#mermaid-svg-wqkXjo7wwQqtk7Up .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wqkXjo7wwQqtk7Up .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-wqkXjo7wwQqtk7Up .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wqkXjo7wwQqtk7Up .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wqkXjo7wwQqtk7Up .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wqkXjo7wwQqtk7Up .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wqkXjo7wwQqtk7Up .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wqkXjo7wwQqtk7Up .marker.cross{stroke:#333333;}#mermaid-svg-wqkXjo7wwQqtk7Up svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wqkXjo7wwQqtk7Up .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wqkXjo7wwQqtk7Up .cluster-label text{fill:#333;}#mermaid-svg-wqkXjo7wwQqtk7Up .cluster-label span{color:#333;}#mermaid-svg-wqkXjo7wwQqtk7Up .label text,#mermaid-svg-wqkXjo7wwQqtk7Up span{fill:#333;color:#333;}#mermaid-svg-wqkXjo7wwQqtk7Up .node rect,#mermaid-svg-wqkXjo7wwQqtk7Up .node circle,#mermaid-svg-wqkXjo7wwQqtk7Up .node ellipse,#mermaid-svg-wqkXjo7wwQqtk7Up .node polygon,#mermaid-svg-wqkXjo7wwQqtk7Up .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wqkXjo7wwQqtk7Up .node .label{text-align:center;}#mermaid-svg-wqkXjo7wwQqtk7Up .node.clickable{cursor:pointer;}#mermaid-svg-wqkXjo7wwQqtk7Up .arrowheadPath{fill:#333333;}#mermaid-svg-wqkXjo7wwQqtk7Up .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wqkXjo7wwQqtk7Up .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wqkXjo7wwQqtk7Up .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-wqkXjo7wwQqtk7Up .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-wqkXjo7wwQqtk7Up .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wqkXjo7wwQqtk7Up .cluster text{fill:#333;}#mermaid-svg-wqkXjo7wwQqtk7Up .cluster span{color:#333;}#mermaid-svg-wqkXjo7wwQqtk7Up div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wqkXjo7wwQqtk7Up :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} 有Token 未加载 已加载 无Token 路由跳转开始 NProgress.start 是否存在Token? 路径是/login? 重定向到首页 路径在白名单? 直接放行 角色已加载? 加锁并获取用户信息 成功? 生成动态路由 router.addRoutes 强制跳转目标页 登出并提示错误 跳转首页 路径在白名单? 重定向到登录页 NProgress.done NProgress.done 路由跳转结束

代码

import router from \'./router\' // 导入Vue Router实例import store from \'./store\' // 导入Vuex Store实例import { Message } from \'element-ui\' //导入Element UI的消息提示组件import NProgress from \'nprogress\' // 导入进度条库import \'nprogress/nprogress.css\' // 导入进度条样式import { getToken } from \'@/utils/auth\'  // 导入获取Token的工具函数import { isPathMatch } from \'@/utils/validate\' // 导入路径匹配工具import { isRelogin } from \'@/utils/request\' // 导入防止重复登录的锁/** * 配置NProgress​ * 禁用进度条的小圆圈动画 */NProgress.configure({ showSpinner: false })// 定义白名单​const whiteList = [\'/login\', \'/register\']// 无需登录即可访问的路径const isWhiteList = (path) => { // 使用路径匹配函数判断当前路径是否在白名单 return whiteList.some(pattern => isPathMatch(pattern, path))}// 全局前置路由守卫​router.beforeEach((to, from, next) => { // 启动进度条 NProgress.start() // 检查是否存在Token if (getToken()) { // 如果有Token,设置页面标题(如果路由元信息中有title) to.meta.title && store.dispatch(\'settings/setTitle\', to.meta.title) // Case 1: 已登录但尝试访问/login if (to.path === \'/login\') { // 重定向到首页 next({ path: \'/\' }) // 立即结束进度条(避免跳转延迟) NProgress.done() // Case 2: 访问白名单路径(如/register) } else if (isWhiteList(to.path)) { // 直接放行 next() // Case 3: 需要权限验证的路径 } else { // 如果用户角色未加载 if (store.getters.roles.length === 0) { // 加锁防止重复获取用户信息 isRelogin.show = true // 获取用户信息(异步) store.dispatch(\'GetInfo\').then(() => { // 解锁 isRelogin.show = false // 生成动态路由(基于角色权限) store.dispatch(\'GenerateRoutes\').then(accessRoutes => { // 根据roles权限生成可访问的路由表 router.addRoutes(accessRoutes) // 动态添加可访问路由表 next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 }) }).catch(err => { // 获取用户信息失败 store.dispatch(\'LogOut\').then(() => { // 执行登出  Message.error(err) // 显示错误消息  next({ path: \'/\' }) // 跳转首页 }) }) } else { next() // 已加载角色信息,直接放行 } } // 没有Token的情况 } else { // Case 4: 访问白名单路径 if (isWhiteList(to.path)) { // 放行(如登录页) next() // Case 5: 需要登录的路径 } else { // 重定向到登录页,并携带原路径作为redirect参数 next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) // 否则全部重定向到登录页 NProgress.done() } }})// 全局后置路由守卫​router.afterEach(() => { // 结束进度条 NProgress.done()})

validate.js

​​路径匹配验证​​:核心函数 isPathMatch 判断当前路径是否与给定模式匹配,支持动态参数(如 :id)和通配符(*)。
​​权限控制​​:在路由守卫中检查用户访问的路径是否在白名单中,无需登录即可访问(如 /login 和 /register)。
流程图

#mermaid-svg-dR6AffXsKThmkBqB {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-dR6AffXsKThmkBqB .error-icon{fill:#552222;}#mermaid-svg-dR6AffXsKThmkBqB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dR6AffXsKThmkBqB .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-dR6AffXsKThmkBqB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dR6AffXsKThmkBqB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dR6AffXsKThmkBqB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dR6AffXsKThmkBqB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dR6AffXsKThmkBqB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dR6AffXsKThmkBqB .marker.cross{stroke:#333333;}#mermaid-svg-dR6AffXsKThmkBqB svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dR6AffXsKThmkBqB .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dR6AffXsKThmkBqB .cluster-label text{fill:#333;}#mermaid-svg-dR6AffXsKThmkBqB .cluster-label span{color:#333;}#mermaid-svg-dR6AffXsKThmkBqB .label text,#mermaid-svg-dR6AffXsKThmkBqB span{fill:#333;color:#333;}#mermaid-svg-dR6AffXsKThmkBqB .node rect,#mermaid-svg-dR6AffXsKThmkBqB .node circle,#mermaid-svg-dR6AffXsKThmkBqB .node ellipse,#mermaid-svg-dR6AffXsKThmkBqB .node polygon,#mermaid-svg-dR6AffXsKThmkBqB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dR6AffXsKThmkBqB .node .label{text-align:center;}#mermaid-svg-dR6AffXsKThmkBqB .node.clickable{cursor:pointer;}#mermaid-svg-dR6AffXsKThmkBqB .arrowheadPath{fill:#333333;}#mermaid-svg-dR6AffXsKThmkBqB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dR6AffXsKThmkBqB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dR6AffXsKThmkBqB .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-dR6AffXsKThmkBqB .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-dR6AffXsKThmkBqB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dR6AffXsKThmkBqB .cluster text{fill:#333;}#mermaid-svg-dR6AffXsKThmkBqB .cluster span{color:#333;}#mermaid-svg-dR6AffXsKThmkBqB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dR6AffXsKThmkBqB :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} 开始: 检查路径是否匹配模式 转义斜杠 替换动态参数为正则组 替换通配符为.* 生成正则表达式 测试路径是否匹配 返回匹配结果

代码

/** * 路径匹配器 * @param {string} pattern pattern - 匹配模式(如 \'/user/:id\' 或 \'/login*\') * @param {string} path path - 当前路径(如 \'/user/123\') * @returns {Boolean} 是否匹配 */export function isPathMatch(pattern, path) { // 1. 转义斜杠,将模式中的 / 转义为 \\/,避免正则表达式语法错误。 const regexPattern = pattern.replace(/\\//g, \'\\\\/\') // 2. 将类似 :id 的动态参数替换为 ([^\\/]+),匹配非斜杠字符。 .replace(/\\*\\*/g, \'.*\') // 3. 将 * 替换为 .*,匹配任意字符(如 /login* 可匹配 /login/123)。 .replace(/\\*/g, \'[^\\\\/]*\') // 4. new RegExp(^withWildcards):生成严格匹配整个路径的正则表达式。 const regex = new RegExp(`^${regexPattern}$`) // 5. 测试路径是否匹配 return regex.test(path)}

router.js

该文件是 Vue Router 的全局配置文件,主要实现以下核心功能:

  • 配置路由守卫实现权限控制
  • 管理登录状态和Token验证
  • 处理动态路由加载
  • 集成页面加载进度条
  • 白名单路径管理
    流程图

#mermaid-svg-dPCwTOzsOo5GPN6M {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-dPCwTOzsOo5GPN6M .error-icon{fill:#552222;}#mermaid-svg-dPCwTOzsOo5GPN6M .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dPCwTOzsOo5GPN6M .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-dPCwTOzsOo5GPN6M .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dPCwTOzsOo5GPN6M .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dPCwTOzsOo5GPN6M .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dPCwTOzsOo5GPN6M .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dPCwTOzsOo5GPN6M .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dPCwTOzsOo5GPN6M .marker.cross{stroke:#333333;}#mermaid-svg-dPCwTOzsOo5GPN6M svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dPCwTOzsOo5GPN6M .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dPCwTOzsOo5GPN6M .cluster-label text{fill:#333;}#mermaid-svg-dPCwTOzsOo5GPN6M .cluster-label span{color:#333;}#mermaid-svg-dPCwTOzsOo5GPN6M .label text,#mermaid-svg-dPCwTOzsOo5GPN6M span{fill:#333;color:#333;}#mermaid-svg-dPCwTOzsOo5GPN6M .node rect,#mermaid-svg-dPCwTOzsOo5GPN6M .node circle,#mermaid-svg-dPCwTOzsOo5GPN6M .node ellipse,#mermaid-svg-dPCwTOzsOo5GPN6M .node polygon,#mermaid-svg-dPCwTOzsOo5GPN6M .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dPCwTOzsOo5GPN6M .node .label{text-align:center;}#mermaid-svg-dPCwTOzsOo5GPN6M .node.clickable{cursor:pointer;}#mermaid-svg-dPCwTOzsOo5GPN6M .arrowheadPath{fill:#333333;}#mermaid-svg-dPCwTOzsOo5GPN6M .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dPCwTOzsOo5GPN6M .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dPCwTOzsOo5GPN6M .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-dPCwTOzsOo5GPN6M .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-dPCwTOzsOo5GPN6M .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dPCwTOzsOo5GPN6M .cluster text{fill:#333;}#mermaid-svg-dPCwTOzsOo5GPN6M .cluster span{color:#333;}#mermaid-svg-dPCwTOzsOo5GPN6M div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dPCwTOzsOo5GPN6M :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} 路由跳转开始 NProgress.start 有Token? 访问login? 重定向到首页 在白名单中? next 角色已加载? 获取用户信息 生成动态路由 next next 在白名单中? next 跳转登录页 NProgress.done NProgress.done 路由完成 NProgress.done

1.开始路由跳转​​时启动进度条
2.​​Token检测​​作为第一个决策点
​​3.五类处理场景​​:

  • 已登录访问登录页
  • 已登录访问白名单
  • 需要权限验证的路径
  • 未登录访问白名单
  • 未登录访问受保护路径

    ​4.​动态路由加载​​流程包含用户信息获取和路由表生成
    ​5.​进度条控制​​贯穿整个路由生命周期
    ​6.​错误处理​​包含自动登出和错误提示机制
    代码

import router from \'./router\' // 导入路由实例import store from \'./store\' // 导入Vuex状态管理实例import { Message } from \'element-ui\' // ElementUI消息提示组件import NProgress from \'nprogress\' // 页面加载进度条库import \'nprogress/nprogress.css\' // 进度条样式import { getToken } from \'@/utils/auth\' // 获取Token的工具函数import { isPathMatch } from \'@/utils/validate\' // 路径匹配工具import { isRelogin } from \'@/utils/request\' // 防止重复登录的锁NProgress.configure({ showSpinner: false }) // 禁用进度条旋转动画const whiteList = [\'/login\', \'/register\'] // 无需认证的白名单路径const isWhiteList = (path) => whiteList.some(p => isPathMatch(p, path)) // 路径匹配检测// 全局前置路由守卫router.beforeEach((to, from, next) => { NProgress.start() // 开始加载进度条 if (getToken()) { // 存在有效Token的情况 to.meta.title && store.dispatch(\'settings/setTitle\', to.meta.title) // 设置页面标题 if (to.path === \'/login\') { // CASE 1: 已登录访问登录页 next(\'/\') // 重定向到首页 NProgress.done() } else if (isWhiteList(to.path)) { // CASE 2: 访问白名单路径 next() // 直接放行 } else { // CASE 3: 需要权限验证的路径 if (store.getters.roles.length === 0) { // 用户角色未加载 isRelogin.show = true // 加锁防止重复请求 store.dispatch(\'GetInfo\').then(() => { // 获取用户信息 isRelogin.show = false store.dispatch(\'GenerateRoutes\').then(accessRoutes => { // 生成动态路由 router.addRoutes(accessRoutes) // 添加动态路由 next({ ...to, replace: true }) // 强制跳转新路由 }) }).catch(err => { // 获取用户信息失败 store.dispatch(\'LogOut\').then(() => { // 执行登出 Message.error(err) next(\'/\') }) }) } else { // 已加载角色信息 next() // 正常放行 } } } else { // 无Token的情况 if (isWhiteList(to.path)) { // CASE 4: 白名单路径 next() // 允许访问 } else { // CASE 5: 需要登录的路径 next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) // 跳转登录页 NProgress.done() } }})// 全局后置路由守卫router.afterEach(() => { NProgress.done() // 结束进度条})