Vue3 动态路由刷新页面白屏解决方案

5,186 阅读3分钟

动态路由的思路很简单:

  1. 登录成功后,请求接口接口获取路由数据
  2. 拼装成前端可用的路由结构
  3. 使用addRoute挂载到router上

就是这样一个简单的想法,一下午都在掉坑,大坑有两个:白屏和死循环,到了晚上才勉强实现了2种路由方式(前端和后端),

页面刷新白屏

第一版,把addRoutes这一步放在了登录成功后的方法中 store/user.ts,这样掉进了今天第一个坑——页面刷新就会白屏。

[Vue Router warn]: No match found for location with path "xxxxx/xxxxx"

究其原因,在我们刷新页面后,通过addRoute动态添加的路由已经不在真正的router上了,所以根本匹配不到对应的路由。

网上有一部分解决办法是把addRoute这一块方法写在router.beforeEach中。

于是我把动态添加路由的那块代码挪到了前置守卫中。

// permission.ts 
router.beforeEach(async (to) => {
  document.title = `${to.meta.title} | mocha vue3 admin`

  const useUser = useUserStore()
  const role = useUser.role

  if (!role && to.path !== '/login') {
    return '/login'
  } else if (to.meta.roles && !to.meta.roles.includes(role)) {
    // 如果没有权限,则进入403
    return '/403'
  } else {
    // 动态添加路由,routes是拼装好的路由
    routes.forEach(r=>router.addRoute(r)
    return true
  }
})

当然还是不行的,因为在前置首位中,照旧匹配不到,刷新依然白屏。 继续寻找解决方案,原来还要把 return true 改成 return { ...to, replace: true }

next({ ...to, replace: true }) 死循环

如果是老一点的文章,大多用的是next,官网最新示例使用的是return。

总之结果差不多,两种写法都是死循环。

routes.forEach(r=>router.addRoute(r)
return { ...to, replace: true }

死循环这个坑,再搞死浏览器数次之后我放弃了,转而寻找别的解决办法。

5.31 更新死循环已解决!!! 记录在文档末尾

白屏解决办法1

白屏的根本原因就是刷新后router上的路由没有了,那么只需要在每次进入之前,让router重新挂载动态路由就好了。

permission.ts,该文件直接在main.ts中引入,那就把addRoute操作直接写在perission.ts中,这样每次刷新页面,都会重新挂载动态路由。事实证明可行。

但新的问题产生了。我想根据登录用户的role角色来动态过滤路由,再挂载到router上,这需要在permission.ts中使用用户store获取role值。

但是pinia在这种ts/js文件中,是拿不到的,前置守卫中可以拿到,但我没有解决死循环的问题。

尝试在store/index中注册一个pinia,然后引入main.ts作为全局实例,然后在别的js文件中使用这个pinia实例,结果是持久化persist失效了。。。

这个问题有两个解决办法:

  1. 每次刷新都去接口获取用户数据,得到role
  2. 获取localstorage中的用户数据

不完美,但是可以正常用起来了。

import router, { routeModuleList } from '~/router'
import Layout from '~/layout/index.vue'
import { UserInfo } from '~/types'
import { RouteRecordRaw } from 'vue-router'
import { useUserStore } from '~/store/user'
import systemApi from '~/api/system'
import permiss from '~/directive/permiss'

// 后端路由模式
const asyncRoutes = await systemApi.getRoutes({ userid: 1 })

const modules = import.meta.glob('~/views/**/**.vue')

function formatAsyncRoutes(routes) {
  routes.forEach((r) => {
    if (r.component === 'layout') {
      r.component = Layout
    } else {
      r.component = modules[`/src/views${r.component}`]
    }
    if (r.children) {
      r.children = formatAsyncRoutes(r.children)
    }
  })
  return routes
}

// 用户信息保存在本地缓存,或者每次刷新都从接口返回
// pinia 在 router钩子之外取不到实例,如果强行破坏单例模式持久化会有问题,所以直接用localstorage

const lsUserData = localStorage.getItem('user')
let userData = <UserInfo>{}
if (lsUserData) {
  userData = JSON.parse(lsUserData)
}

// 过滤不需要显示的路由
function filterRoute(route: RouteRecordRaw) {
  if (!route.meta?.roles) return true
  if (route.meta?.roles && !route.meta.roles?.includes(userData.role)) return false
  return true
}

function filterFunc(routes: RouteRecordRaw[]) {
  routes = routes.filter(filterRoute)

  routes.forEach((element) => {
    if (element.children) {
      element.children = filterFunc(element.children)
    }
    if (
      element.children &&
      !element.children?.some((val) => element.path + '/' + val.path === element.redirect)
    ) {
      element.redirect = element.path + '/' + element.children[0].path
    }
  })
  return routes
}

// 路由模式:前端/后端
const permissionMode = import.meta.env.VITE_PERMISSIOIN_MODE
let filteredRoutes = []
if (permissionMode === 'FRONT') {
  filteredRoutes = routeModuleList
}
if (permissionMode === 'BACK') {
  filteredRoutes = formatAsyncRoutes(asyncRoutes as unknown as RouteRecordRaw)
}

// addRoutes
filteredRoutes.forEach(async (val) => await router.addRoute(val))

router.beforeEach(async (to) => {
  document.title = `${to.meta.title} | mocha vue3 admin`

  const useUser = useUserStore()
  const role = useUser.role

  if (!role && to.path !== '/login') {
    return '/login'
  } else if (to.meta.roles && !to.meta.roles.includes(role)) {
    // 如果没有权限,则进入403
    return '/403'
  } else {
    return true
  }
})

next死循环 已解

清早继续,终于明白了产生死循环的原因,果然早晨脑袋比较好使。

因为上面的代码,一直使用return { ...to, replace: true },没有在合适的时候放行return true,相当于无穷无尽地执行路由跳转

    return { ...to, replace: true }
        return { ...to, replace: true }
            return { ...to, replace: true }
                return { ...to, replace: true }
                    return { ...to, replace: true }
                            ...

解决死循环的关键:在动态路由完成挂载后,使用return true正常进入导航。

参考官方文档:router.vuejs.org/zh/guide/ad…

每个守卫方法接收两个参数:

to: 即将要进入的目标 用一种标准化的方式

from: 当前导航正要离开的路由 用一种标准化的方式

可以返回的值如下:

  • false: 取消当前的导航。如果浏览器的 URL 改变了(可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。

  • 一个路由地址: 通过一个路由地址跳转到一个不同的地址,就像你调用 router.push() 一样,你可以设置诸如 replace: true 或 name: 'home' 之类的配置。当前的导航被中断,然后进行一个新的导航,就和 from 一样。

如果遇到了意料之外的情况,可能会抛出一个 Error。这会取消导航并且调用 router.onError() 注册过的回调。

如果什么都没有,undefined 或返回 true则导航是有效的,并调用下一个导航守卫

如果刷新时,动态路由没有完成挂载,观察前置守卫中 to的属性值,其中很多属性都和正常情况不同,比如name、matched、redirectedFrom...

image.png

此处使用to.redirectedFrom作为条件,修改导航守卫中的代码。

如果用to.name,在输入错误路由时,不会正常跳转到404页面。

    // 如果没有匹配到路由,则执行addRoute,否则正常导航
    if (!to.redirectedFrom) {
      filteredRoutes.forEach((val) => router.addRoute(val))
      useUser.getAsyncRoutes(filteredRoutes)

      return { ...to, replace: true }
    } else return true

死循环解决了,可以把放在导航守卫外面的代码挪进来了。这样可以实现在每次跳转路由时,实时判断用户权限,这样才比较完美呀。