动态路由的思路很简单:
- 登录成功后,请求接口接口获取路由数据
- 拼装成前端可用的路由结构
- 使用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失效了。。。
这个问题有两个解决办法:
- 每次刷新都去接口获取用户数据,得到role
- 获取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...
此处使用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
死循环解决了,可以把放在导航守卫外面的代码挪进来了。这样可以实现在每次跳转路由时,实时判断用户权限,这样才比较完美呀。