//route.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{ name: 'Login', path: '/login', component: Login },
{
name: 'Home',
path: '/',
component: Home,
// 到home页面后直接跳转到excelPreview页面
redirect: '/uploadSpec?active=0',
meta: {
authentication: true
},
// 整个页面为Home,如果想要点击el-aside左侧菜单,将对应路由内容显示在el-main区域,需要将其他路由路径设置为Home的子路径,且path前没有/
children: [
{
name: 'UploadSpec',
path: 'uploadSpec',
title: '上传spec',
component: UploadSpec,
// 组件内传参
meta: {
keepAlive: true,
authentication: true,
// 用于设置tab名
title: '上传spec',
}
},
]
},
// vue3不再使用path:'*'正则匹配,而是使用/pathMatch(.*)*或/pathMatch(.*)或/catchAll(.*)
// { name: 'NotFound', path: '/404', component: NotFound },
// { path: '/:pathMatch(.*)', redirect: '/404' },
];
const router = createRouter({
history: createWebHistory(),
routes,
});
这里使用mock模拟后端返回数据
//mock/user.js
import Mock from 'mockjs'
const users = [
{'id': 1, path:'/uploadSpec','authName': "上传spec", 'icon': '',children:[]},
{'id': 2, path:'/showSpec', 'authName': "Spec预览", 'icon': '',children:[]},
{'id': 3, path:'/generateTxt', 'authName': "生成测试数据", 'icon': '',children:[]},
{'id': 4, path:'/generateCronjob', 'authName': "生成转码程序", 'icon': '',children:[]},
{'id': 5, path:'/pdfCompare', 'authName': "PDF文档对比", 'icon': '',children:[]},
{'id': 6, path:'/resourceUpdate', 'authName': "资源更新管理", 'icon': '',children:[]},
{'id': 7, path:'/generateTestCase', 'authName': "自动生成ST/SIT案例", 'icon': '',children:[]},
{'id': 8, path:'/userManagement', 'authName': "用户管理", 'icon': 'User'},
]
Mock.mock("/user/login", Mock.mock({
"code": 200,
"success": true,
"data": {
users: users,
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlck5hbWUiOiJhZG1pbiIsIm5pY2tOYW1lIjoi6LaF566hIiwiaWNvbiI6IiIsInJvbGVJZCI6MSwic3ViIjoiYWRtaW4iLCJleHAiOjE2OTI3NzMzNTMsImp0aSI6ImZkNmVkOWZiMjdiYzQxODg5OWRmYmYzNzhlMTMzZmQ0In0.APGpN-i2edKwPQA52LP10aDEM2DZi7G71k8f_njGcpE"
}
})
)
Mock.mock("/user/me", Mock.mock({
"code": 200,
"success": true,
"data": {
"id": 1,
"userName": "admin",
"nickName": "超管",
"icon": "",
"roleId": 1,
"rights": users,
}
})
)
//Login.vue
await this.$store.dispatch("user/login", {
...this.user
});
//store/user/user.js
import * as api from '@/api/api'
import { ElMessage } from 'element-plus'
const state = () => {
return {
user:JSON.parse(sessionStorage.getItem("user") || '{}'),
rights: JSON.parse(sessionStorage.getItem("rights") || '[]')
}
}
const getters = () => {}
const actions = {
// 注意async位置和箭头函数写法
login: async({ commit }, user) => {
// 调用登录接口
try {
let result = await api.login(user);
if (result.data.code === 200) {
sessionStorage.setItem("token", result.data.data.token);
// 调用获取user信息接口
let loginUser = await api.getLoginUser();
// 调用mutations的login方法
if (loginUser.data.code === 200) {
commit('login', loginUser.data.data);
} else {
ElMessage.error("登录失败:用户信息获取失败");
}
} else {
ElMessage.error("登录失败:" + result.data.errorMsg);
}
} catch (error) {
throw error;
}
},
}
const mutations = {
initUser: (state) => {
// 从localStorage中获取数据设置进user中,否则通过刷新页面时,获取不到state中的user信息
state.user = JSON.parse(sessionStorage.getItem("loginUser"));
},
login: (state, user) => {
// 登录成功后将user信息存到state中,且缓存到localStorage中
state.user = user;
state.rights = user.rights;
sessionStorage.setItem("loginUser", JSON.stringify(user));
sessionStorage.setItem("rights", JSON.stringify(user.rights));
},
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}
注意此处routeMapping的key和uploadSpecRule的path以及和后端返回数据rights的user.js的path必须一致;且路径必须有 "/"
const uploadSpecRule = {
name: 'UploadSpec',
path: '/uploadSpec',
title: '上传spec',
component: UploadSpec,
meta: {
keepAlive: true,
authentication: true,
title: '上传spec',
}
};
const showSpecRule = {
name: 'ExcelPreview',
path: '/showSpec',
title: 'Spec预览 | 生成测试数据',
component: ExcelPreview,
meta: {
keepAlive: true,
authentication: true,
title: 'Spec预览',
}
};
const generateTxtRule = {
name: 'GenerateTxt',
path: '/generateTxt',
title: '生成测试数据',
component: GenerateTxt,
meta: {
keepAlive: true,
authentication: true,
title: '生成测试数据',
}
};
const generateCronjobRule = {
name: 'GenerateCronjob',
path: '/generateCronjob',
title: '生成转码程序',
component: GenerateCronjob,
meta: {
keepAlive: true,
authentication: true,
title: '生成转码程序',
}
};
const pdfCompareRule = {
name: 'PDFCompare',
path: '/pdfCompare',
title: 'PDF文档对比',
component: PDFCompare,
meta: {
keepAlive: true,
authentication: true,
title: 'PDF文档对比',
}
};
const resourceUpdateRule = {
name: 'ResourceUpdate',
path: '/resourceUpdate',
title: '资源更新管理',
component: ResourceUpdate,
meta: {
keepAlive: true,
authentication: true,
title: '资源更新管理',
}
};
const generateTestCaseRule = {
name: 'GenerateTestCase',
path: '/generateTestCase',
title: '自动生成ST/SIT案例',
component: GenerateTestCase,
meta: {
keepAlive: true,
authentication: true,
title: '自动生成ST/SIT案例',
}
};
const userManagementRule = {
name: 'UserManagement',
path: '/userManagement',
title: '用户管理',
component: UserManagement,
meta: {
keepAlive: true,
authentication: true,
title: '用户管理',
}
};
const routeMapping = {
'/uploadSpec': uploadSpecRule,
'/showSpec': showSpecRule,
'/generateTxt': generateTxtRule,
'/generateCronjob': generateCronjobRule,
'/pdfCompare': pdfCompareRule,
'/resourceUpdate': resourceUpdateRule,
'/generateTestCase': generateTestCaseRule,
'/userManagement': userManagementRule,
}
export const initDynamicRoutes = async () => {
const rightsList = Store.state.user.rights;;
rightsList.length > 0 && rightsList.forEach(item => {
if (item.path) {
const temp = routeMapping[item.path];
router.addRoute("Home", temp);
}
});
router.addRoute( { name: 'NotFound', path: '/404', title:"页面不存在",component: NotFound });
router.addRoute( { path: '/:pathMatch(.*)', redirect: '/404' });
}
注意在vue-router4.X中,动态路由主要通过两个函数实现。router.addRoute()
和 router.removeRoute()
。它们只注册一个新的路由,也就是说,如果新增加的路由与当前位置相匹配,就需要你用 router.push()
或 router.replace()
来手动导航,才能显示该新路由。
也就是说以前的rouer.addRoutes()添加即可使用动态路由的方式不行了
// 路由守卫鉴权处理
router.beforeEach(async (to, from, next) => {
let token = sessionStorage.getItem("token");
let isToken = !!token;
let loginUser = Store.state.user.user;
if (isToken) {
if (loginUser) {
// 如果是login,且已经登录了直接跳转到home页面
if (to.name === "Login") return next();
// 已经设置过动态路由直接放行,没有则需要通过手动调用 router.replace()进行路由显示
if (isDynamic) {
next();
} else {
await initDynamicRoutes();
isDynamic = true;
next({ ...to, replace: true });
}
} else {
sessionStorage.clear();
next("/login");
}
} else {
isDynamic = false;
next();
}
});
这种方式不会每次路由都调用一次添加动态路由方法,个人觉得这种方式性能比较好
//App.vue
import { initDynamicRoutes } from "@/route/route"
export default {
name: 'App',
created(){
initDynamicRoutes();
}
}
//Login.vue
try {
await this.$store.dispatch("user/login", {
...this.user
});
//登录成功后,根据用户的rights动态添加路由
initDynamicRoutes();
this.$router.push({ name: "Home" });
} catch (e) {}
添加动态路由方法中,是使用router.addRoute()方法进行添加,Home表示将路由嵌套在Home页面下
rightsList.length > 0 && rightsList.forEach(item => {
if (item.path) {
const temp = routeMapping[item.path];
router.addRoute("Home", temp);
}
});
我试过之前的添加方法已经不能成功添加动态路由了
const currentRoutes = router.options.routes;
rightsList.length > 0 && rightsList.forEach(item => {
if (item.path) {
const temp = routeMapping[item.path];
currentRoutes[1].children.push(temp);
}
});
错误页面必须也使用动态添加方法,并且在路由动态添加完成后再添加,否则如果在基础路由中定义错误页面,那么每次点击路由都会首先找路由,没找到就会匹配到错误页面,从而跳转到错误页面
rightsList.length > 0 && rightsList.forEach(item => {
if (item.path) {
const temp = routeMapping[item.path];
router.addRoute("Home", temp);
// currentRoutes[1].children.push(temp);
}
});
router.addRoute( { name: 'NotFound', path: '/404', title:"页面不存在",component: NotFound });
router.addRoute( { path: '/:pathMatch(.*)', redirect: '/404' });
如果直接next()会找不到动态路由,而直接next({ ...to, replace: true });会导致永远去找动态路由
使用标识后,如果有动态路由就加载动态路由,没有直接放行
if (isToken) {
if (loginUser) {
// 如果是login,且已经登录了直接跳转到home页面
if (to.name === "Login") return next();
// 已经设置过动态路由直接放行,没有则需要通过手动调用 router.replace()进行路由显示
if (isDynamic) {
next();
} else {
// await initDynamicRoutes();
isDynamic = true;
next({ ...to, replace: true });
}
} else {
sessionStorage.clear();
next("/login");
}
} else {
isDynamic = false;
next();
}
以7.2的方式,在动态添加完其他路由后,再添加错误页面就能成功导到不同路由页面,但是警告依然会存在
针对这个警告,报错的意思就是打开地址/generateCronjob时没有找到。其实菜单数据是登录后就放到vuex和localStorage里面的,并且针对这个数据进行动态路由的设置。如果没有将数据存放在localStorage或者localStorage中的数据改变以后那么菜单就会对应改变。
问题就出在localStorage里面。如果设置动态路由是根据state.menuList去设置,而且对state.menuList进行了更改,那么menuList自然就变了,而且localStorage里面的数据也变了。再到页面上通过动态路由去跳转页面自然找不到了。所以就会报警告。其实跟设置标识与否无关(这个标识其实和下面代码的checkRouter作用一样的)
// 获取菜单栏数据
let result = await getMenuList(formLogin);
// 动态设置menu
await store.commit("layout/addMenuList", result.data);
// 动态添加路由
await store.commit("layout/setDynamicRoutes", router);
// 路由守卫中再次设置路由(此时是在地址栏直接回车时处理)
store.commit("layout/setDynamicRoutes", router);
// 判断路由在配置的动态路由中是否存在,不存在则跳转到home,存在则直接跳转
const checkRouter = (path) =>{
let hasCheck = router.getRoutes().filter(item=>item.path == path).length;
if (hasCheck) {
return true
} else {
return false
}
}
router.beforeEach(async (to, from, next) => {
// 要重新获取token
const token = store.state.user.token;
//注意这里的逻辑: 如果咩有token且路由不为/login则直接跳转到login页面
// 如果路由存在且有token则直接到home页面否则直接next
if (!token && to.path !== '/login') {
next('/login');
// 否则检查路由是否存在,不存在直接跳转home页面
}else if(!checkRouter(to.path)){
next('/main');
}else {
next();
}
});
async setDynamicRoutes(state, router) {
// 循环menuList设置动态路由
let menuList = JSON.parse(JSON.stringify(state.menuList));
// 通过menuList给routes循环添加数据
let menuRoutes = getMenuRoutes(menuList);
// 通过import.meta.glob获取模块化文件,类似与webpack的require.context()
// ../../views/home/Home.vue: () => import("/src/views/home/Home.vue")
const modules = import.meta.glob("../../views/**/*.vue");
menuRoutes.length>0 && menuRoutes.map(route=>{
let url = `../../views${route.path}/${route.component}.vue`
route.component = modules[url];
// 添加动态路由
router.addRoute('home', route)
})
}
const getMenuRoutes = (menuList) =>{
let routes = [];
const deepList = (list) =>{
while(list.length){
// 删除最后一个
let item = list.pop();
// 有action表示是最底层子级菜单
if(item.action){
routes.push({
name: item.menuName,
path: item.path,
component: item.component,
meta: {
title: item.menuName
}
});
}
// 如果有children说明还有子级,需要循环处理子级菜单
if(item.children && !item.action){
deepList(item.children)
}
}
}
deepList(menuList)
return routes;
}