Solon + EasyQuery + ElementPlus 实现后台管理系统之 04-Axios 优化与路由守卫
AI 摘要
Axios 优化
Axios 存在的问题
由于后端接口大部分需要登录状态下才能请求访问,前后端验证登录状态,凭借的是在请求时请求头中传递的 Token 令牌,也就是说,每一次请求后端接口,都要在请求头中传递 Token 令牌。
// 发送请求,获取登录用户信息实体
axios.get('/passport/currentUser', {
// 发送请求时,添加请求头Header,传递Token
headers: {
'authtoken': localStorage.getItem('authtoken')
}
}).then((res) => {
if (res.data.code === 200) {
// 将登录用户信息实体存储到数据仓库中
useCurrentUserStore().setCurrentUser(res.data.data)
} else {
ElMessage.error(res.data.message)
}
}).catch((error) => {
ElMessage.error(error.message)
})再者,Axios 发完请求之后,要对响应状态做判断,如果响应状态不是 200,需要提示错误信息。
综上所述:
- 前端大量 Axios 请求后端,需要携带 Token。
- 后端响应数据给前端时,前端需要进行状态判断。
- 以上工作,几乎是每一个 Axios 请求必备的,如果在每一次 Axios 请求时都手动操作处理,会出现严重的冗余问题。
Axios 优化
核心注意事项:
- 在 Axios 请求拦截器中,从 localStorage 中获取 Token 令牌,并根据实际情况存到请求头中。
- 在 Axios 响应拦截器中,直接对响应状态进行判断,最后直接返回响应数据,简化接口调用处的取值逻辑。
编写 Axios 配置文件(plugins/axios.js),优化请求拦截器和响应拦截器。
// Axios请求拦截器
_axios.interceptors.request.use(
function(config) {
// 从localStorage中取出Token,如果Token存在,则放入请求头
let authtoken = localStorage.getItem("authtoken");
if (authtoken) {
config.headers.authtoken = authtoken;
}
return config;
},
function(error) {
// Do something with request error
return Promise.reject(error);
}
);
// Axios响应拦截器
_axios.interceptors.response.use(
function(response) {
if(response.data.code === 401) {
// 如果响应数据中code为401即未登录,则清空locaoStorage跳转到登录页面
// 可能:未登录、Token 失效
localStorage.clear()
ElMessage.error("请先登录")
router.push("/login")
// 抛出错误
return Promise.reject(response.data.message)
} else if(response.data.code !== 200) {
// 响应数据中code不为200,则提示错误信息
ElMessage.error(response.data.message)
// 抛出错误
return Promise.reject(response.data.message)
}
// 直接返回响应数据,简化接口调用处的取值逻辑
return response.data;
},
function(error) {
// 处理响应失败(HTTP状态码非2xx)
ElMessage.error(error.message)
return Promise.reject(error);
}
);编写登录组件(views/Login.vue),优化 Axios 请求:
// 这里执行实际的登录逻辑
// 发送Axios请求
axios.post('/passport/login', qs.stringify(loginForm)).then((res) => {
ElMessage.success('登录成功')
// 将Token存储到localStorage中
// Axios响应拦截器处理,res即后端响应给前端的数据,不再是res.data.data
localStorage.setItem('authtoken', res.data)
// 跳转到后台首页
router.push('/admin/index')
})编写后台首页组件(views/Index.vue),优化 Axios 请求:
// 发送请求,获取登录用户信息实体
axios.get('/passport/currentUser').then((res) => {
useCurrentUserStore().setCurrentUser(res.data)
})路由守卫
登录状态认证存在的问题
在未登录的情况下,可以直接访问到后台首页视图,不符合业务逻辑。虽然在后台首页组件加载时,会发送 Axios 请求到后端获取登录用户信息实体,未登录的情况下,会在 Axios 响应拦截器中对 401 状态进行处理,但本质上是不能在未登录的情况下触发 Axios 请求的。
登录状态认证优化
核心注意事项:
- 通过 Vue-Router 提供的路由守卫,进行前端路由拦截。
- 如果 Token 存在且访问了登录路由,直接跳转到后台首页。
- 如果 Token 不存在且访问了登录路由,直接跳过,不做额外处理。
- 如果 Token 不存在且访问了后台其他路由,直接跳转到登录页面。
- Token 存在且访问了后台其他路由,直接跳过,不做额外处理。
编写路由配置文件(router/index.js),完善路由守卫:
router.beforeEach((to, from, next) => {
let authtoken = localStorage.getItem("authtoken")
// 通过to.path获取访问的路由地址,并进行判断
if(to.path === '/login' && authtoken) {
// 如果Token存在且访问了登录路由,跳转到首页页面
ElMessage.success('欢迎回来')
next({path: '/admin/index'})
} else if(to.path === '/login') {
// 跳过登录路由
next()
} else if(!authtoken) {
// 如果Token不存在,即未登录,跳转到登录页面
ElMessage.error('请先登录')
next({path: '/login'})
}
// 其他正常情况,继续访问
next()
})视图组件优化
抽取公共布局
后台页面,事实上只有核心区域 main 会发生变化,侧边栏、顶部导航栏、底部版权声明栏是不需要变化的。
核心注意事项:
- 可以通过嵌套路由抽取公共布局,也可以通过组件的形式抽取公共布局。
- 通过嵌套路由抽取公共布局时,父路由的 component 必须是布局组件,其核心区域使用 router-view 出口,用于承载子路由内容,子路由的 component 对应具体的页面组件,会被渲染到父路由的 router-view 中。
- 抽取后公共布局后,子路由的对应的业务组件只需写核心内容(如表格、表单),公共由父路由的对应的布局组件自动承载。
后台首页只有核心区域变化而已,其他区域都不变。
重命名后台首页组件(views/Index.vue)为布局组件(views/Layouts.vue),并修改主要内容区:
<!-- 主要内容区 -->
<router-view/>编写后台首页组件(views/Index.vue),提供后台首页视图:
<script setup>
import {useCurrentUserStore} from "@/stores/currentUser.js";
</script>
<template>
<el-main class="layout-main">
<div class="welcome-container">
<h2>欢迎您,{{ useCurrentUserStore().currentUser.realname }}!</h2>
<p>祝您工作愉快!</p>
</div>
</el-main>
</template>编写路由配置文件(router/index.js),提供嵌套路由信息:
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
// 根路径跳转到登录路由
path: '',
redirect: '/login'
},
{
// 登录
path: '/login',
component: () => import('@/views/Login.vue'),
},
{
// 后台
path: '/admin',
component: () => import('@/views/Layouts.vue'),
// 使用嵌套路由
// 嵌套路由的component会渲染到父路由的router-view中
children: [
{
// 后台首页
path: 'index', // 子路由路径前不能加/号,访问路径为父路由路径+/子路由路径
component: () => import('@/views/Index.vue'),
}
]
},
],
})ElementPlus ICON
核心注意事项:
- 参考文档:https://elementplus.fenxianglu.cn/zh-CN/component/icon.html
- 在后台管理系统中,可以使用 ELementPlus 的图标组件对细节进行优化。
- 通过 npm 安装 ElementPlus ICON 依赖,并进行全局注册,参照文档设置图标即可。
在终端中执行指令,安装 ElementPlus ICON:
npm install @element-plus/icons-vue编写项目入口文件(main.js),批量注册 ElementPlus ICON:
// 引入ElementPlus图标
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
// 批量注册ElementPlus图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}登录组件样式优化
编写登录组件(views/Login.vue),优化样式:
<script setup>
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import router from "@/router/index.js";
import qs from "qs";
// 表单数据
const loginForm = reactive({
username: '',
password: ''
})
// 表单引用
const loginFormRef = ref()
// 表单验证规则
const loginRules = {
username: [
{ required: true, message: '请输入账户账号', trigger: 'blur' },
{ min: 6, max: 18, message: '账户账号长度应为6-18个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入账户密码', trigger: 'blur' },
{ min: 6, max: 18, message: '账户密码长度应为6-18个字符', trigger: 'blur' }
]
}
// 登录处理函数
const handleLogin = async () => {
if (!loginFormRef.value) return
await loginFormRef.value.validate((valid) => {
if (valid) {
// 这里执行实际的登录逻辑
// 发送Axios请求
axios.post('/passport/login', qs.stringify(loginForm)).then((res) => {
ElMessage.success('登录成功')
// 将Token存储到localStorage中
// Axios响应拦截器处理,res即后端响应给前端的数据,不再是res.data.data
localStorage.setItem('authtoken', res.data)
// 跳转到后台首页
router.push('/admin/index')
})
} else {
ElMessage.error('请正确填写表单信息')
return false
}
})
}
// 重置表单
const resetForm = () => {
loginFormRef.value.resetFields()
}
</script>
<template>
<el-container>
<el-main>
<el-row justify="center">
<el-col :span="8">
<el-card style="margin-top: 5rem">
<template #header>
<div style="text-align: center; padding: 0.5rem 0">
<h2 style="margin-top: 0; margin-bottom: 10px">SAdmin后台管理系统</h2>
<span>欢迎使用SAdmin后台管理系统,请先登录</span>
</div>
</template>
<el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" label-width="80px" label-position="top">
<el-form-item label="账户账号" prop="username" size="large">
<el-input v-model="loginForm.username" placeholder="请输入账户账号"/>
</el-form-item>
<el-form-item label="账户密码" prop="password" size="large">
<el-input v-model="loginForm.password" type="password" placeholder="请输入账户密码" show-password/>
</el-form-item>
<div style="width: 100%; text-align: center">
<el-button type="primary" size="large" @click="handleLogin">登录</el-button>
<el-button size="large" @click="resetForm">重置</el-button>
</div>
</el-form>
</el-card>
</el-col>
</el-row>
</el-main>
</el-container>
</template>在浏览器中测试:
根组件样式优化
编写根组件(App.vue),优化样式:
<template>
<router-view/>
</template>
<style>
/** 去除body外边距 **/
body {
margin: 0;
}
</style>布局组件样式优化
编写布局组件(views/Layouts.vue),优化样式:
<script setup>
import { ref } from 'vue'
import {useCurrentUserStore} from "@/stores/currentUser.js";
import router from "@/router/index.js";
// 模拟菜单数据
const menuData = ref([
{
id: 1,
title: '系统管理',
children: [
{
id: 11,
title: '用户管理',
path: '/users'
},
{
id: 12,
title: '角色管理',
path: '/role'
},
{
id: 13,
title: '部门管理',
path: '/dept'
},
{
id: 20,
title: '权限管理',
path: '/perms'
}
]
},
{
id: 2,
title: '日志管理',
children: [
{
id: 21,
title: '登录日志',
path: '/loginlog'
},
{
id: 22,
title: '操作日志',
path: '/actionlog'
}
]
}
])
// 发送请求,获取登录用户信息实体
axios.get('/passport/currentUser').then((res) => {
// Axios响应拦截器处理,res即后端响应给前端的数据
useCurrentUserStore().setCurrentUser(res.data)
})
// 注销登录
const logout = () => {
axios.post('/passport/logout').then((res) => {
// 登出成功,清空locaoStorage跳转到登录页面
localStorage.clear()
router.push('/login')
})
}
</script>
<template>
<el-container class="layout-container">
<!-- 左侧菜单栏 -->
<el-aside width="200px">
<!--
router="true":启用该模式会在激活导航时以index作为path进行路由跳转
参考文档:https://element-plus.org/zh-CN/component/menu#menu-attributes
-->
<el-menu default-active="1" class="el-menu-vertical" :router="true" background-color="#545c64" text-color="#fff" active-text-color="#ffd04b">
<el-menu-item index="/admin/index">
<span>SAdmin后台管理系统</span>
</el-menu-item>
<template v-for="item in menuData" :key="item.id">
<el-sub-menu v-if="item.children && item.children.length > 0" :index="item.id">
<template #title>
<span>{{ item.title }}</span>
</template>
<!--
index注意路径拼接前缀/admin
-->
<el-menu-item v-for="child in item.children" :key="child.id" :index="'/admin' + child.path">
{{ child.title }}
</el-menu-item>
</el-sub-menu>
<el-menu-item v-else :index="'/admin' + item.path">
<template #title>{{ item.title }}</template>
</el-menu-item>
</template>
</el-menu>
</el-aside>
<!-- 右侧内容区 -->
<el-container>
<!-- 顶部导航栏 -->
<el-header class="layout-header">
<div class="header-right">
<el-dropdown>
<div class="el-dropdown-link">
欢迎您,{{ useCurrentUserStore().currentUser.realname }}
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>修改信息</el-dropdown-item>
<el-dropdown-item>修改密码</el-dropdown-item>
<el-dropdown-item>登录日志</el-dropdown-item>
<el-dropdown-item>操作日志</el-dropdown-item>
<el-dropdown-item @click="logout">注销</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<!-- 主要内容区 -->
<router-view/>
<!-- 底部版权信息 -->
<el-footer class="layout-footer">
<div class="footer-content">
<p>© 2025 后台管理系统. All Rights Reserved.</p>
</div>
</el-footer>
</el-container>
</el-container>
</template>
<style>
.layout-container {
height: 100vh;
}
.el-aside {
background-color: #545c64;
}
.el-menu-vertical {
height: 100%;
border: none;
}
.layout-header {
background-color: #ffffff;
color: #333;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 20px;
}
.header-right {
display: flex;
align-items: center;
}
.el-dropdown-link {
cursor: pointer;
color: #333;
display: flex;
align-items: center;
outline: none !important;
box-shadow: none !important;
}
.layout-main {
background-color: #f5f7fa;
padding: 20px;
}
.layout-footer {
background-color: #ffffff;
color: #666;
text-align: center;
padding: 20px;
border-top: 1px solid #ebeef5;
}
.footer-content p {
margin: 0;
}
</style>
后台首页组件样式优化
编写后台首页组件(views/Index.vue),优化样式:
<script setup>
import {useCurrentUserStore} from "@/stores/currentUser.js";
</script>
<template>
<el-main class="layout-main">
<el-card>
<template #header>
<div class="card-header">
<span>后台首页</span>
</div>
</template>
欢迎您,{{useCurrentUserStore().currentUser.realname}},祝您工作愉快!
</el-card>
</el-main>
</template>在浏览器中测试:
文章中可能会存在些许错别字内容描述不完整、表述不准确、排版布局异常等问题,文章中提及的软件、依赖、框架等程序可能随其版本更新迭代而产生变化,文章中的相关代码片段、例图、文本等内容仅供参考。
如若转载,请注明出处:https://www.duox.dev/post/122.html