vue3权限管理——(路由权限)动态路由设置
1.大概思路
- 设置基础路由login和home等页面;
- 登录后从后端获取user,token,rights等数据,并将数据同时存储到vuex和sessionStorage中
- 将后端获取的权限数据(作为不同用户显示不同菜单及不同路由的依据)和路由页面进行映射;
- 写公共方法添加动态路由;
- 在路由全局守卫中判断是否已添加过动态路由,如果已添加直接next()未添加调用添加动态路由方法,并且放行(next({ ...to, replace: true }););
- 添加动态路由位置第二种方法:全局路由中只负责判断是否有动态路由和放行,调用添加动态路由方法,在登录后立即调用,且以防页面手动刷新时进行404页面,需要在App.vue的create()方法中再次调用
2.设置基础路由,login页面,Home页面等
//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,
});
2.从后端获取菜单权限数据并设置到vuex和sessionStorage中
这里使用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,}
})
)
3.登录后将数据存到vuex和sessionStorage中
//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
}
4.设置菜单权限数据和路由的映射
注意此处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,
}
5.设置可导出添加动态路由公共方法
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' });
}
5.全局路由守卫中鉴权及激活动态路由
注意在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();}
});
6.第二种方法激活动态路由——在Login.vue和App.vue中调用
这种方式不会每次路由都调用一次添加动态路由方法,个人觉得这种方式性能比较好
//App.vue
import { initDynamicRoutes } from "@/route/route"
export default {name: 'App',created(){initDynamicRoutes();}
}
</scri
//Login.vuetry {await this.$store.dispatch("user/login", {...this.user});//登录成功后,根据用户的rights动态添加路由initDynamicRoutes();this.$router.push({ name: "Home" });} catch (e) {}
7.注意点:
7.1 必须使用router.addRoute()方法进行添加动态路由
添加动态路由方法中,是使用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);}});
7.2 错误页面添加时机
错误页面必须也使用动态添加方法,并且在路由动态添加完成后再添加,否则如果在基础路由中定义错误页面,那么每次点击路由都会首先找路由,没找到就会匹配到错误页面,从而跳转到错误页面
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' });
7.3判断动态路由是否启用的标识
如果直接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.4警告:[Vue Router warn]: No match found for location with path "/generateCronjob"
以7.2的方式,在动态添加完其他路由后,再添加错误页面就能成功导到不同路由页面,但是警告依然会存在