Vue Router 4.x 动态路由解决刷新空白

问题描述:

基于对 Vue Router 3.x 没有改变前,我们常规的实现一定,在 store 中根据获取的用户权限,对路由进行过滤并返回,然后到路由守卫的地方,使用 addRoutes 动态添加路由。但是在 Vue Router 4.x 以后对这部分进行了修改。
修改点:

  1. 删除API addRoutes
  2. 改用API addRoute,新增API removeRoute,下附官方该 API 的说明:

也就是说这两个API是我们实现动态路由的关键,但是按照官方的说明及以往的开发经验,最终我出错了,动态路由页面刷新空白。分析下问题原因:

动态路由实现常规思路

路由

  1. 创建 ./src/router 目录
  2. 新建 index.ts 文件用于书写通用的路由 routescreateRouter 对象
// ./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
  1. 新建 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') }
    ]
  }
]
  1. 新建 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()
  })
}
  1. 这里为了处理路由和菜单,我新增了一个文件 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

  1. 创建一个模块或一个文件,用来实现对 ./src/router/async/**/*.ts 的所有 routes 们进行权限的过滤,以及右侧菜单的生成,最终你需要在你的 state 中存储两个值 authAsyncRoutes(有权限的动态路由列表),authNavMenus(有权限的导航菜单列表)
  2. 根据你的业务场景需要,对这两个值做相关的持久化存储
// ./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

  1. 引入路由和 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')

是不是发现没有没有看出什么问题,其实这其中有两个问题

  1. 控制台报错问题,具体是因为 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()
  1. 解决报错问题,发现刷新空白,经查看报错提示及查看了缓存的有权限的路由发现,缓存中是不存在路由组件关联的,因此这里我们需要手动将视图组件导入并关联,需要在添加如下代码:
// ./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
  }

这下终于解决了,完事!

posted @ 2022-12-02 17:51  YanEr、  阅读(2934)  评论(0编辑  收藏  举报