什么是路由鉴权?
假设你系统有张三,李四两个用户,且总计有如下3个路由,现在你需要实现:张三,能访问所有路由,李四只能访问C路由。那么这就是路由鉴权
/hello/a: A路由
/test/b: B路由
/ok/c: C路由
一个怪异现象
难道vue中路由鉴权方案就只有
router.addRoute 动态添加路由
一种实现方案吗? 拜托,明明有更友好的方案啊,为什么好多人像魔障了一般,就只认这一种方案呢?
为什么我有这种感觉?
- 我看过几个开源项目,路由鉴权采用都是动态添加路由的方案
- 我接手过一些需要路由鉴权的工作项目,基本也都是采用动态添加路由的方案
- 有几次面试,也被问到,如何实现路由鉴权,面试官的预期答案是动态添加路由
如何通过 router.addRoute 动态添加路由方式实现路由鉴权?
基本实现步骤
- 在全局前置路由守卫中,判断有加载远程路由数据,未加载远程路由数据,则继续执行第2步
- 加载远程路由数据
- 将远程路由数据转换为
RouteRecordRaw
对象 - 将
RouteRecordRaw
对象通过:router.addRoute(routeRaw)
添加到路由表 next({ ...to, replace: true })
. 为什么要执行这一步看这里:vue-router 4.x: addRoute
使用vite和webpack在具体实现上的不同之处
你的路由信息可能是这样的:
{ name: 'demo02', path: '/demo02', component: '/views/demo/demo02.vue' }
webpack添加动态路由
function routeInfoToRouteRecordRaw(routeInfo: RouteInfo): RouteRecordRaw {
const { name, path, component } = routeInfo
let importPartPath = component.replace(/^\/+/, '') // 过滤字符串前面所有 '/' 字符
importPartPath = importPartPath.replace(/\.\w+$/, '') // 去除.vue后缀名
// 最后importPartPath类似这样: views/demo/demo02
// 最后import(xx)的xx内容类似这样: @/views/demo/demo02.vue
return {
name,
path,
component: () => import('@/' + importPartPath + '.vue')
}
}
vite添加动态路由
// 通过 vite 的 glob 加载所有组件
const modules = import.meta.glob('@/views/**/*.vue') // 导入
/*
modules类似这样
{
'/src/views/demo/demo02.vue': () => import("/src/views/demo/demo02.vue")
}
*/
function routeInfoToRouteRecordRaw(routeInfo: RouteInfo): RouteRecordRaw {
const { name, path, component } = routeInfo
let importPartPath = component.replace(/^\/+/, '') // 过滤字符串前面所有 '/' 字符
importPartPath = importPartPath.replace(/\.\w+$/, '') // 去除.vue后缀名
// 最后importPartPath类似这样: views/demo/demo02
return {
name,
path,
// 这里为什么和webpack的方式不一样? 因为vite不支持import(xxx)中包含变量,因此要改为vite glob方式引入指定目录所有组件,再按照key取出来
component: modules[`/src/${importPartPath}.vue`]
}
}
vue3 + vite:打包部署后,动态组件渲染404问题解决_vite 404-CSDN博客
vue3-vite动态路由导入组件不能使用模板字符串的问题_vue3 import 动态模板字符串-CSDN博客
在路由守卫中添加动态路由
// 远程路由是否加载
let remoteRouteLoaded = false
router.beforeEach(async (to, from, next) => {
if (!remoteRouteLoaded) {
//远程路由设置为已加载
remoteRouteLoaded = true
// 获取远程路由(这里就可以根据不同的用户由后端返回不同的路由了) :routeInfoList 类似: [{ name: 'demo02', path: '/demo02', component: '/views/demo/demo02.vue' }]
const routeInfoList = await loadRemoteRoue()
// 将远程路由信息,转换为 RouteRecordRaw 对象,并添加到路由表中
const retArr: RouteRecordRaw[] = routeInfoList.map((item) => routeInfoToRouteRecordRaw(item))
retArr.forEach((item) => router.addRoute(item))
// 当前在未添加远程路由前,可能已经匹配了旧的路由组件,而实际预期是想去新路由组件,因此需要再执行如下代码,使得能进入预期的新路由组件
next({ path: to.path, query: to.query, hash: to.hash, replace: true })
} else {
// 动态路由已添加,则直接next()即可
next()
}
})
有的同学可能会说,我的动态添加的不是根路由而是子路由,实现步骤与上面基本一致,只是添加路由时,先指定上级路由名 vue-router 4.x 添加嵌套路由
路由配置
未使用动态添加路由方案,你原本的路由配置可能像这样
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'index',
component: ScreenSizeView
},
{
path: '/demo01',
name: 'demo01',
component: () => import('@/views/demo/demo01.vue')
},
{
path: '/demo02',
name: 'demo02',
component: () => import('@/views/demo/demo02.vue')
},
{
path: '/demo03',
name: 'demo03',
component: () => import('@/views/demo/demo03.vue')
},
// 上面路由配置都未匹配则匹配该路由组件
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound }
]
})
使用动态添加路由方案后,你的路由配置可能像这样(demo03,demo02,demo01都通过动态路由方式加载进来)
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'index',
component: ScreenSizeView
},
// 上面路由配置都未匹配则匹配该路由组件
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound }
]
})
为什么不建议这么做? 原因如下
看了上面的逻辑,可能有的同学,会想:“这不是挺好的吗?”,我的回答是:“NO, NO, NO这并不好”,原因如下:
前端的整体结构变得不清晰了
原本你只需要通过看路由配置,就能清晰的知道这个前端项目有哪些路由,路由匹配表达式,以及对应的路由组件是哪个。就像下面这样
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'index',
component: ScreenSizeView
},
{
path: '/demo01',
name: 'demo01',
component: () => import('@/views/demo/demo01.vue')
},
{
path: '/demo02',
name: 'demo02',
component: () => import('@/views/demo/demo02.vue')
},
{
path: '/demo03',
name: 'demo03',
component: () => import('@/views/demo/demo03.vue')
},
// 上面路由配置都未匹配则匹配该路由组件
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound }
]
})
你改为动态添加路由方式之后,你已经无法通过路由配置,对这个项目的整体结构有一个清晰的认知了。就像下面这样(你不知道这个项目还有哪些路由,也不知道会匹配哪些路由组件)
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'index',
component: ScreenSizeView
},
// 上面路由配置都未匹配则匹配该路由组件
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound }
]
})
有部分同学可能会嘴硬一下:"写文档,我们会在文档中描述清楚!"
还有部分同学可能会说: “没有完美的解决方案,有舍就有得,我认为得到的比失去的多”
引用了新的API,增加了学习成本,沟通成本
你问一个vue开发,有没有用过vue-router
,绝大多数人都能肯定的告诉你用过。
你问一个vue开发,有没有用过动态添加路由,大部分人可能会犹豫一下,然后,再回复用过。
为什么会犹豫一下呢?
- 可能在想你是指动态添加路由,还是指动态路由
- 自己没实现过,但做过的项目中,确实有用过该方案,但未详细关注过
- 没用过,但听过,为了找工作,那就回复用过呗,大不了事后马上去恶补一下这个知识点
有同学会想:“你说增加了学习成本也就罢了,哪里又增加了什么沟通成本?你怕不是在忽悠人吧”
请回顾之前的这句代码const modules = import.meta.glob('@/views/**/*.vue')
, 这是将view
目录下所有组件都加载进来了,然后添加动态路由组件时,也是匹配的这个modules
中的内容。
这有什么问题吗?
view
目录成了一个约定,如果不放在view目录,那将无法匹配到这个路由组件,但路由定义本身是没有限制一定要放在哪个目录的
有同学又会想:“这点学习成本和沟通成本你都不愿意增加?你别做开发了!”
不知你有没有遇到过一种情况,工期紧张的时候,你可能因为一个平时一眼就能看出来的小问题,而卡半天?所以,勿以善小而不为 勿以恶小而为之
404路由变得不明确了
原本的404路由只有一个意思,未匹配的路由都进404
现在使用动态添加路由之后,404就存在两种意思
- 当前用户没这个路由权限,所以404
- 系统中确实没有这个路由,所以404
要明确是没有路由权限就变复杂了
可能有的系统要求,没权限,要求进入没权限的界面,而不是404,那这时采用动态添加路由的方案,就变得难以实现这个特性了。
meta信息没了
原本路由配置中,是可以包含meta信息,用于实现一些特殊功能的(如:路由缓存控制
),但当前这种动态添加路由的方式,路由信息中已经没有meta信息了,虽说,再加个这个配置也不是不行,但这进一步增加了管理员配置菜单的复杂度。本来管理员,只需要简单配置下菜单就好了,现在连meta信息都放上去了,那又有了一堆约定,这是写在文档中好呢,还是不写?即使当前写了,多经手几个人,后续的人员还会维护这个文档吗?甚至这个文档会一直流传下去吗?
路由鉴权的另一种实现方式
无论采用哪种方式,实现路由鉴权,路由定义也是菜单定义,后端肯定维护的是菜单信息,而不是直接维护路由信息
那么,我们完全可以依然,将路由定义保留在前端项目本身,让路由鉴权只是实现路由鉴权即可。
let remoteMenuLoaded = false
let routeNameSet = new Set<string>()
router.beforeEach(async (to, from, next) => {
// 获取目标路由名
const { name: toName, fullPath: toFullPath } = to
// console.log(to, from)
if (!remoteMenuLoaded) {
remoteMenuLoaded = true
// 从后端获取菜单信息
const menuList = await loadMenu()
// 提取菜单信息中的路由名,将其放入一个set中,如果toName包含在该路由名set中,说明有该路由权限,否则没有
routeNameSet = extractRouteName(menuList)
}
if(toName && !routeNameSet.has(toName)){
// 无权限,则转入无权限路由
next({name:'NotPermission'})
}else{
// 有权限或匹配的404路由,则放行
next()
}
})
以上都是个人实践得出的看法,如果错误或更好的见解,欢迎在评论区中指出