问题现象
当前端的路由是从后端获取并通过router.addRoute()添加到vue-router时,为了避免页面刷新后由于没有路由报错或让用户重新登录,一般都会把用户信息保存到storage里,加载时再写到store中,或者重新请求获取路由列表,最后在路由守卫里做如下操作:
let tryToMatch = false
router.beforeEach(to => {
console.log('to path', to.fullPath)
if (to.matched.length === 0) {
// 加载过动态路由后还没有找到,那就跳转到404页面
// 这里没有在router中通过通配符转向404页面,是因为第一次未匹配时路由还没有加载,需要先进入该方法
if (tryToMatch) {
tryToMatch = false
return { name: '404' }
}
const store = useUserStore() // 获取piana store
// 判断有没有用户登录信息,没有调回登录页
if (!store.isLogin) {
return '/login'
}
// 如果有路由信息,就添加动态路由
if (store.resources.length > 0) {
addAsyncRoutes(store.resources)
}
tryToMatch = true
return to.path
}
return true
})
如果没有匹配到路由,就通过用户信息去添加动态路由,然后再重新路由到这个页面。
咋一看没有问题,运行起来也没有问题。但控制台却有一行警告:
[Vue Router warn]: No match found for location with path "/xxx"
vue-router在beforeEach前发出了一个找不到路由的警告。这倒不影响运行,其实完全可以忽略。但你来都来了,肯定跟我一样,怎么能容忍一刷新就有一个警告呢?
问题分析
结合在路由守卫里打印的日志来看,这个警告是在router.beforeEach()之前发出来的,而这个main.ts是在哪呢?
是在vue.use()的时候触发的,而vue.use()又会调用router.instal()方法,但我肯定不会闲的去改源码,那么我们的目标就是在router.instal()前,store初始化后把动态路由先加进去。
解决思路
router有个isReady()函数,先看看它能不能在install前触发:
// ......
router.isReady().then(() => {
console.log('router is ready')
})
export default router
嗯,确实和它的名字一样,不可能在install前触发
再试试能不能通过替换install函数的方式触发:
const install = router.install
router.install = function (app) {
console.log('before router install')
install(app)
}
这下页面直接打不开了,这么做导致原方法中的this变成了undefined,进而导致报错。
那如何在不修改原函数的情况下,添加自定义逻辑呢,突然想到要不试试Proxy呢。
// 代理处理器
const handler = {
// 当代理对象是个函数时,当调用函数会触发该方法
apply(t, i, a) {
console.log('before router install')
// t是目标对象(函数),i是被调用时的上下文对象,a是被调用时的参数数组
// 执行原函数
t.apply(i, a)
}
} as ProxyHandler<any>
router.install = new Proxy(router.install, handler)
我创建了一个router.install的代理并覆盖router.install方法,同时通过Handler在函数调用时打印一条日志,由于router.install没有返回值,所以直接apply就可以了
很好,那接下来只需把添加动态路由的逻辑挪到handler里面就可以了
最终方案
- 首先将初始化动态路由移动到install前完成
const handler = {
apply(t, i, a) {
const store = useUserStore()
if (store.resources && store.resources.length > 0) {
addAsyncRoutes(store.resources)
}
t.apply(i, a)
}
} as ProxyHandler<any>
router.install = new Proxy(router.install, handler)
- 调整路由守卫,只需要保留未登录跳回首页的功能
router.beforeEach(to => {
console.log('to path', to.fullPath)
const store = useUserStore()
const isManageRoute = to.path.startsWith('/manage')
// 未登录跳回登录页
if (isManageRoute && !store.isLogin) {
return '/login'
}
return true
})
- 在基础路由中,通过通配符将不存在的页面跳转到404页面
const basicRoutes: Array<RouteRecordRaw> = [
{
path: '/',
redirect: '/login'
},
{
path: '/login',
name: 'Login',
component: () => import('...')
},
{
path: '/manage',
name: 'ManageLayout',
redirect: '/manage/home',
component: () => import('...'),
children: [
{
path: 'home',
name: 'Home',
meta: { title: '首页' },
component: () => import('...')
},
{
path: ':pathMatch(.*)*',
redirect: '/manage/404'
},
{
path: '404',
name: 'Manage404',
meta: { title: '404' },
component: () => import('...')
}
]
},
{
path: '/:pathMatch(.*)*',
redirect: '/404'
},
{
path: '/404',
name: '404',
meta: { title: '404' },
component: () => import('...')
}
]
我这里使用了2个404页,分别用于没有找到动态路由时(登录后的系统页面),在ManageLayout内展示一个内嵌的404,另一个用于没找到静态路由时,展示一个全屏的404。 由于有该重定向兜底,所以不会再出现匹配不到路由的警告。
如果你需要通过URL参数登录(例如Oauth2,或者通过token参数获取用户信息),你有2种方式:
1.不使用通配符转向404页面,还是在路由守卫里判断有无匹配到路由,通过URL参数登录并添加动态路由后,再转向该页面(这种方式避免了通配符总会跳向404,导致登录后不能跳回原链接对应页面的问题,不过又会出现No match found for location with path 警告)
2.保留通配符转向404页面,在路由守卫里,判断是否有对应URL参数并完成登录,最后跳转到系统首页(适合不需要跳向任意页面,即固定向一个静态配置了的页面跳转的模式)
再试一下,已经看不到警告了:
缺点
由于Proxy的handler中不能执行Promise后再去执行t.apply(),不能实现需要先请求后端再添加动态路由的方式。