奇技淫巧:解决vue-router动态添加路由刷新时提示 No match found for location with path

4,986 阅读4分钟

问题现象

当前端的路由是从后端获取并通过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"

image.png

vue-router在beforeEach前发出了一个找不到路由的警告。这倒不影响运行,其实完全可以忽略。但你来都来了,肯定跟我一样,怎么能容忍一刷新就有一个警告呢?

问题分析

结合在路由守卫里打印的日志来看,这个警告是在router.beforeEach()之前发出来的,而这个main.ts是在哪呢?

image.png

是在vue.use()的时候触发的,而vue.use()又会调用router.instal()方法,但我肯定不会闲的去改源码,那么我们的目标就是在router.instal()前,store初始化后把动态路由先加进去。

解决思路

router有个isReady()函数,先看看它能不能在install前触发:

// ......
router.isReady().then(() => {
  console.log('router is ready')
})

export default router

image.png

嗯,确实和它的名字一样,不可能在install前触发

再试试能不能通过替换install函数的方式触发:

const install = router.install
router.install = function (app) {
  console.log('before router install')
  install(app)
}

image.png

这下页面直接打不开了,这么做导致原方法中的this变成了undefined,进而导致报错。

image.png

那如何在不修改原函数的情况下,添加自定义逻辑呢,突然想到要不试试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就可以了

image.png

很好,那接下来只需把添加动态路由的逻辑挪到handler里面就可以了

最终方案

  1. 首先将初始化动态路由移动到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)
  1. 调整路由守卫,只需要保留未登录跳回首页的功能
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
})
  1. 在基础路由中,通过通配符将不存在的页面跳转到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参数并完成登录,最后跳转到系统首页(适合不需要跳向任意页面,即固定向一个静态配置了的页面跳转的模式)

再试一下,已经看不到警告了:

image.png

缺点

由于Proxy的handler中不能执行Promise后再去执行t.apply(),不能实现需要先请求后端再添加动态路由的方式。