vue动态路由实现及多级路由缓存失效解决(v3+vite方式)

207 阅读3分钟

实现目标

开发后台管理系统做动态路由时,遇到路由超过2级缓存不生效的问题,借此记录下(全是代码没有技巧 滑稽)
示例代码地址:github.com/vela666/vit…

动态路由常见后端返回方式:

  1. 数据格式类似这样
[
  {
    menu_id: '0da4b12d84e64764ad3a34d297f799063',
    type: 0,
    menu_name: '首页',
    button_name: '',
    menu_sort: 10,
    menu_icon: '',
    menu_path: '/home',
    is_cache: true,
    menu_type: 0,
    is_enable: 0,
    create_time: '2021-08-06 15:33:03',
    children: [
      {
        menu_id: '7e79440386a0416d8e80d03c794b3a355',
        type: 0,
        menu_name: '引导',
        button_name: '',
        menu_sort: 3,
        menu_icon: '',
        menu_path: '/home/user-guide/index',
        is_cache: true,
        menu_type: 0,
        is_enable: 0,
        create_time: '2022-08-25 21:22:48',
        children: [
          {
            menu_id: 'a24a3002d3e74357ade602eac897fb317',
            type: 0,
            menu_name: '概览',
            button_name: '',
            menu_sort: 1,
            menu_icon: '',
            menu_path: '/home/overview/index',
            is_cache: true,
            menu_type: 1,
            is_enable: 0,
            create_time: '2022-08-08 14:57:51',
            children: [],
          },
        ],
      },
    ],
  },
]
  1. 前端写好所有的路由,后端返回对应路由name等类似的标识, 前端根据标识去过滤
    如:juejin.cn/post/695231…
    路由格式类似这样
    // 默认路由
    const constantRoutes = [
      {
        path: '/login',
        name: 'Login',
        hidden: true,
        component: () => import('@/views/login/index.vue'),
        meta: {
          hidden: true,
          title: '登录',
        },
      }
    ]

    // 根据后端返回的name(标识)过滤展示
    const asyncRoutes = [
      {
        path: '/',
        component: Layout,
        redirect: '/dashboard',
        name: 'Home',
        meta: { title: '主页', icon: 'dashboard' },
        children: [
          {
            path: '/dashboard',
            component: () => import('@/views/home'),
            name: 'Dashboard',
            meta: { title: '主页', icon: 'dashboard', affix: true },
          },
        ],
      },
      {
        path: '/nested',
        name: 'Nested',
        component: Layout,
        redirect: '/nested/menu1/menu1-1',
        meta: {
          title: '嵌套路由',
          icon: 'nested',
          noCache: false,
          roles: ['admin'],
        },
        children: [
          {
            path: '/nested/menu1',
            name: 'Menu1',
            component: () => import('@/views/nested/menu1'),
            meta: {
              title: 'menu1',
              noCache: false,
            },
            redirect: '/nested/menu1/menu1-1',
            children: [
              {
                path: '/nested/menu1/menu1-1',
                component: () => import('@/views/nested/menu1/menu1-1'),
                name: 'Menu1-1',
                meta: {
                  title: 'menu1-1',
                  noCache: false,
                },
              },
              {
                path: '/nested/menu1/menu1-2',
                name: 'Menu1-2',
                redirect: '/nested/menu1/menu1-2/menu1-2-1',
                component: () => import('@/views/nested/menu1/menu1-2'),
                meta: {
                  title: 'menu1-2',
                  noCache: false,
                },
                children: [
                  {
                    path: '/nested/menu1/menu1-2/menu1-2-1',
                    component: () =>
                      import('@/views/nested/menu1/menu1-2/menu1-2-1'),
                    name: 'Menu1-2-1',
                    meta: {
                      title: 'menu1-2-1',
                      noCache: false,
                    },
                  },
                  {
                    path: '/nested/menu1/menu1-2/menu1-2-2',
                    component: () =>
                      import('@/views/nested/menu1/menu1-2/menu1-2-2'),
                    name: 'Menu1-2-2',
                    meta: {
                      title: 'menu1-2-2',
                      noCache: false,
                    },
                  },
                ],
              },
            ],
          },
          {
            path: '/nested/menu2',
            name: 'Menu2',
            component: () => import('@/views/nested/menu2'),
            meta: {
              title: 'menu2',
              noCache: false,
              // icon : 'devices',
              roles: ['admin'],
            },
          },
          {
            path: '/nested/menu3',
            name: 'Menu3',
            component: () => import('@/views/nested/menu3'),
            meta: {
              title: 'menu3',
              noCache: false,
              // icon : 'devices',
              roles: ['admin'],
            },
          },
        ],
      }
    ]

    const notFoundRoute = {
      path: '/:pathMatch(.*)*',
      // 不要写name不然动态路由 页面刷新就在404页面
      // name: 'Redirect404',
      hidden: true,
      meta: {
        title: '404',
      },
      component: () => import('@/views/error/404'),
    }

以第一种方式实现(第二种也差不多) vite方式 (webpack也差不多)

    import Layout from '@/layout'

    const defaultPath = '../../../views/'  
    // 过滤4|5开头的文件  
    const dynamicRoutesModules = import.meta.glob('../../../views/**/!(4|5).vue')

    // bool = true creative-label  false === CreativeLabel
    function toPascalCase(str, bool = true) {
      const tmp = str.replace(/\/index$/, '').match(/\/([^/]+)\/?$/g)?.[0] ?? str
      // const words = tmp.replace('/', '').split('-');
      const words = tmp.replace(/^\/+|\/+$/g, '').split('-')
      const capitalizedWords = words.map(
        (word) => word.charAt(0).toUpperCase() + word.slice(1),
      )
      // return capitalizedWords.join('-') || tmp; // creative-label === Creative-Label
      return capitalizedWords.join(bool ? '' : '-') || tmp // creative-label === CreativeLabel
    }

    // 父级记录N层子级标识
    function getParentPath1(parent, childrenPath, key = 'path') {
      parent?.children?.forEach((child) => {
        childrenPath.push(child[key])
        child?.children && getParentPath1(child, childrenPath, key)
      })
    }

    /**  
    * @description 映射动态路由(超过二级路由平铺为二级路由,解决keep-alive不缓存问题)  
    * @param {Array} menus 后端接口返回的菜单列表  
    * @param {Boolean} level 路由嵌套级别 false 不平铺 true 平铺  
    * @param {Boolean} needTiling 需不需平铺(渲染左侧或顶部菜单不平铺)  
    */
    // let firstLoop = true
    export function generatorDynamicRoutes1(
      menus = [],
      isOneLevel = false,
      parentPath = [],
      needTiling = true,
    ) {
      const routes = []
      menus.forEach((item) => {
        const route = {
          path: item.menu_path,
          // 缓存路由时用
          name: toPascalCase(item.menu_path),
          meta: {
            title: item.menu_name,
            noCache: !item.is_cache,
            id: item.menu_id,
            icon: 'menu',
            // 添加父级记录所有子级 标识
            hasSubs: [],
            // affix: false,
            // 子级记录所有父级 标识 
            hasParents: [...parentPath],
          },
          component: null,
        }
        getParentPath1(item, route.meta.hasSubs, 'menu_path')

        // 去除第一个 /  /home/test2/ = home/test2/
        // const comp = item.menu_path.replace(/\//, '')
        // 去除路径前面和后面的 /
        const comp = item.menu_path.replace(/^\/+|\/+$/g, '')
        if (item.children && item.children.length > 0) {
          // route.component =  Layout
          // 只有第一层应为Layout  防止子级包含children 时 component 也设置为 Layout
          route.component = !isOneLevel
            ? Layout
            : dynamicRoutesModules[`${defaultPath}${comp}.vue`]
          route.redirect = item.children[0].menu_path
          const parentMark = [...route.meta.hasParents, item.menu_path]
          if (isOneLevel && needTiling) {
            routes.push(
              ...generatorDynamicRoutes1(
                item.children,
                true,
                parentMark,
                needTiling,
              ),
            )
          } else {
            route.children = generatorDynamicRoutes1(
              item.children,
              true,
              parentMark,
              needTiling,
            )
            // 第一个路由添加 affix 标识 固定
            /*if (firstLoop) {
                      route.children[0].meta.affix = true
            }*/
          }
          // firstLoop = false
        } else {
          // comp 路由文件目录位置
          // '../../views/b/index.vue'.match(/[^/]*\.vue$/) 获取文件名index.vue
          route.component = dynamicRoutesModules[`${defaultPath}${comp}.vue`]
          // 打包后运行不了 开发环境可以 之前测试的
          // route.component = () => import(/* @vite-ignore */ `${defaultPath}${comp}`)
          // route.component = () => import(/* @vite-ignore */ '../../views/' + comp)
          // route.component = () => import(/* @vite-ignore */ '/src/views/' + comp)
          // webpack方式
          // const comp = item.menu_path.replace(/\//, '');
          // webpackChunkName: request 占位符 会以文件名来填写名字 index 使用数字
          // route.component = import(/* webpackChunkName: "[index]" */ `@/views/${comp}`)
          // route.component = () => import(/* webpackChunkName: "[index]" */`@/views/${comp}`)
        }

        routes.push(route)
      })

      return routes
    }

然后再router.beforeEach里添加即可

router.beforeEach(async (to, from, next) => {
      NProgress.start()
      const userStore = useUserStore()
      const permissionStore = usePermissionStore()
      if (getToken()) {
        if (to.path === '/login') {
          next({ path: '/' })
        } else {
          // 请求过菜单
          if (permissionStore.asyncRoutesRequested) {
            next()
          } else {
            try {
              await userStore.getUserInfo()
              // 获取菜单
              await permissionStore.getMenus()
              // permissionStore.generateRoutes = 使用generatorDynamicRoutes1方法处理好的路由
              if (permissionStore.generateRoutes.length > 0) {
                permissionStore.generateRoutes.forEach((route) => {
                  router.addRoute(route.name, route)
                })
              }
              next({ ...to, replace: true })
            } catch (error) {
              await userStore.resetInfo()
              next('/login')
            }
          }
        }
      } else {
        if (whiteList.includes(to.path)) {
          next()
        } else {
          next(`/login?redirect=${to.fullPath}`)
        }
      }
})

添加动态路由时 可能会遇到 添加完成 一直在404页面 或者在动态路由添加完成后再添加404

    const notFoundRoute = {
      path: '/:pathMatch(.*)*',
      // 不要写name不然动态路由 页面刷新就在404页面
      // name: 'Redirect404',
      hidden: true,
      meta: {
        title: '404',
      },
      component: () => import('@/views/error/404'),
    }

多级路由缓存效果展示

动画.gif End~