Vue Router 4.x 动态路由解决刷新空白
问题描述:
基于对 Vue Router 3.x
没有改变前,我们常规的实现一定,在 store
中根据获取的用户权限,对路由进行过滤并返回,然后到路由守卫的地方,使用 addRoutes
动态添加路由。但是在 Vue Router 4.x
以后对这部分进行了修改。
修改点:
- 删除API
addRoutes
- 改用API
addRoute
,新增APIremoveRoute
,下附官方该 API 的说明:
![](https://img2023.cnblogs.com/blog/995687/202212/995687-20221202163145638-1565838612.png)
也就是说这两个API是我们实现动态路由的关键,但是按照官方的说明及以往的开发经验,最终我出错了,动态路由页面刷新空白。分析下问题原因:
动态路由实现常规思路
路由
- 创建
./src/router
目录 - 新建
index.ts
文件用于书写通用的路由routes
和createRouter
对象
// ./src/router/index.ts
import { createWebHistory, createRouter } from 'vue-router'
import GuardEach from './guard'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{ path: '/*', redirect: '/'},
{ path: '/404', name: '404', component: () => import('@/views/common/404.vue') },
{ path: '/login', name: 'Login', component: () => import('@/views/common/login.vue') },
{ path: '/', name: 'Home', redirect: '/home', component: () => import('@/views/layout/index.vue'),
children: [
{ path: '/home', name: 'Home-Index', component: () => import('@/views/home/index.vue') }
]
}
]
})
GuardEach(router)
export default router
- 新建
async
目录,用于按业务模块存放你的异步路由routes
,当然了如果的的动态路由实在少,你可以可以写在index.ts
中导出,或者一个文件搞定,这里演示我们就比如他是一个ts
文件
// ./src/router/async.ts
import type { RouteRecordRaw } from 'vue-router'
export const Member: Readonly<RouteRecordRaw[]> = [
{ path: '/member', name: 'Member', redirect: '/member/list', meta: { code: 1 },
component: () => import('@/views/layout/index.vue'),
children: [
{ path: 'list', name: 'Member-List', meta: { code: 2 } component: () => import('@/views/member/list.vue') },
{ path: 'item', name: 'Member-Item', meta: { code: 3 } component: () => import('@/views/member/item.vue') }
]
}
]
- 新建
guard.ts
文件,用于书写路由守卫逻辑(大部分程序员习惯将其放至在./src/permission.ts
中,这个看个人喜好,无关痛痒)
// ./src/router/guard.ts
import { userStore } from '@/stores'
import type { Router } from 'vue-router'
let hasInitAuth = true
export default function (router: Router) {
router.beforeEach((to, from, next) => {
const user = userStore()
const authRoutes = user.getAuthRoutes(router.options.routes) || []
if (hasInitAuth) {
authRoutes.forEach((item: any) => router.addRoute(item))
hasInitAuth = false
router.push({ ...to, replace: true })
}
next()
})
}
- 这里为了处理路由和菜单,我新增了一个文件
extend.ts
,用于存放一些扩展的函数(仅供参考)
// ./src/router/extend.ts
import type { RouteRecordRaw } from 'vue-router'
// 生成有权限的路由表
export function createAuthRoutes(asyncRoutes: Readonly<RouteRecordRaw[]>, authCode: Number) {
return asyncRoutes.filter((s) => {
const code = s.meta?.code
const child = s.children
if (child && child.length > 0) createAuthRoutes(child, authCode)
return !code || (code && authCode)
})
}
// 生成有权限的导航菜单
export function createNavMenus(allRoutes: Readonly<RouteRecordRaw[]>) {
function mapItem(data: Readonly<RouteRecordRaw[]>): any[] {
return data.map((s) => {
let children = []
if (s.children && s.children.length > 0) {
children = mapItem(s.children) || []
}
return {
title: s.meta?.name as string,
children: children.length === 0 ? undefined : children
}
})
}
function filterItem(data: Readonly<RouteRecordRaw[]>): any[] {
return data.filter((s) => {
if (s.children && s.children.length > 0) {
s.children = filterItem(s.children)
}
return true
})
}
return filterItem(mapItem(allRoutes))
}
stores
- 创建一个模块或一个文件,用来实现对
./src/router/async/**/*.ts
的所有routes
们进行权限的过滤,以及右侧菜单的生成,最终你需要在你的state
中存储两个值authAsyncRoutes
(有权限的动态路由列表),authNavMenus
(有权限的导航菜单列表) - 根据你的业务场景需要,对这两个值做相关的持久化存储
// ./src/stores/index.ts
import { defineStore } from 'pinia'
import type { RouteRecordRaw } from 'vue-router'
import { createAuthRoutes, createNavMenus } from '@/router/extend'
import asyncRoutes from '@/router/async'
export const userStore = defineStore('USER_STORES', {
state: () => ({
authNavMenus: [] as any[]
}),
actions: {
getAuthRoutes(syncRoutes: Readonly<RouteRecordRaw[]>) {
/**
* 1. 获取缓存用户权限路由
* 2. 获取缓存用户权限菜单
* 3. 如果存在缓存,则给导航菜单赋值并返回有权限的路由
*/
const authAsyncRoutes = sessionStorage.get('UserRoutes')
const authNavMenus = sessionStorage.get('UserMenus')
if (authAsyncRoutes && authNavMenus) {
this.authNavMenus = authNavMenus
return authAsyncRoutes
}
/**
* 4. 如果不存在缓存,则获取当前用户的权限配置(我的业务场景是二进制权限配置,因此是一个最大权限值)
* 5. 根据权限配置,生成有权限的路由
* 6. 根据有权限的路由,生成导航菜单,并赋值
* 7. 将有权限的路由和导航菜单进行缓存
* 9. 返回有权限的路由
*/
const authCode = sessionStorage.get('UserAuthCode')
if (authCode) {
const authAsyncRoutes = createAuthRoutes(asyncRoutes, authCode)
const allRoutes = syncRoutes.concat(authAsyncRoutes)
this.authNavMenus = createNavMenus(allRoutes)
sessionStorage.set('UserRoutes', authAsyncRoutes)
sessionStorage.set('UserMenus', this.authNavMenus)
return authAsyncRoutes
}
}
}
})
main.ts
- 引入路由和
stores
,并use
,ok!感受报错和刷新白屏的洗礼!
// ./src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.mount('#app')
是不是发现没有没有看出什么问题,其实这其中有两个问题
- 控制台报错问题,具体是因为 next() 这个发生了一些修改,看了官方文档其实还是一知半解,经过不断实验,不断探索,终于我发现了问题:
// ./src/router/guard.ts 中以下代码错误,这段代码等同于执行了一个 next()
router.push({ ...to, replace: true })
// ,,因此这应该修改为:
if (hasInitAuth) {
authRoutes.forEach((item: any) => router.addRoute(item))
hasInitAuth = false
router.push({ ...to, replace: true })
+ } else {
+ next()
+ }
- next()
![](https://img2023.cnblogs.com/blog/995687/202212/995687-20221202173155200-1380984575.png)
- 解决报错问题,发现刷新空白,经查看报错提示及查看了缓存的有权限的路由发现,缓存中是不存在路由组件关联的,因此这里我们需要手动将视图组件导入并关联,需要在添加如下代码:
// ./src/router/extend.ts 新增如下代码:
/**
* 动态添加路由当缓存时只会存储其路由清单树,不会存储其关联的视图组件
* 故而当重新刷新或进入页面时,需要重新将视图组件与路由清单树关联
* 否则会导致页面空白,无法正常显示
*/
+ export function authRouteTreePlug(
+ authRoutesTree: Readonly<RouteRecordRaw[]>,
+ parentPath: string = ''
+ ) {
+ // 查阅资料得出,在这里不支持 () => import() 的写法,需要使用 import.meta.glob导入组件,而后在使用,这里不要使用 @ 哦,否则会找不到,具体原因不明
+ const modules = import.meta.glob('../views/**/*.vue')
+
+ return authRoutesTree.map((item) => {
+ const itemPath = item.path.slice(0, 1) === '/' ? item.path : `/${item.path}`
+ const hasChild = item.children && item.children.length > 0
+
+ // 由于这里是管理后台,因此一级路由需要使用layout布局组件,故增加判断,各位看官可以根据需求修改
+ const compPath = item.redirect && hasChild ? '/layout/index' : parentPath + itemPath
+ item.component = modules[`../views${compPath}.vue`]
+
+ if (hasChild) {
+ item.children = authRouteTreePlug(item.children as Readonly<RouteRecordRaw[]>, itemPath)
+ }
+ return item
+ })
+ }
// ./src/stores/index.ts 修改如下这段代码:
/**
* 1. 获取缓存用户权限路由
* 2. 获取缓存用户权限菜单
* 3. 如果存在缓存,则给导航菜单赋值并返回有权限的路由
*/
+ const authAsyncRoutesTree = sessionStorage.get('UserRoutes')
const authNavMenus = sessionStorage.get('UserMenus')
+ if (authAsyncRoutesTree && authNavMenus) {
+ const authAsyncRoutes = authRouteTreePlug(authRoutesTree)
this.authNavMenus = authNavMenus
return authAsyncRoutes
}
这下终于解决了,完事!