vue-element-admin解决三级目录的KeepAlive缓存问题(详情版)

vue-element-admin解决三级目录的KeepAlive缓存问题(详情版)

本文章将从问题出现的角度看看KeepAlive的缓存问题,然后提出两种解决方法。本文章比较详细,如果只是看怎么解决,代码怎么改,请前往配置版

一、解决问题之间,先理清问题出现的原因

①首先,观察一下"一级目录"和"二级目录"

通过vue devtools工具,截图如下。

可以看到,"一级目录"------Tab,可以缓存:

再看看,"二级目录"------DirectivePermission,也可以缓存:

可以发现,他们都在<App>------<Layout>------<AppMain>下,同时<KeepAlive>的include属性包含组件配置的"name"(如上面两图所示)。

②再看看,"三级目录"的情况

通过vue devtools工具,截图如下。

可以看到,"三级目录"------Menu1-1,不可以缓存:

看发现,其明细的不同,他在<App>------<Layout>------<AppMain>------<Menu1>下,比"一级目录"和"二级目录"多了<Menu1>。由于<KeepAlive>的include属性并不包含<Menu1>组件配置的name------"Menu1",而是组件配置的name------"Menu1-1"(如下图),所有不缓存。更多<keep-alive>不缓存的原因,可以看个人的另一篇文章


二、解决问题

①添加<RouterViewKeepAlive>解决

这里,你可能想到。既然,"三级目录"不缓存的原因是"由于<KeepAlive>的include属性并不包含<Menu1>组件配置的name------'Menu1'"。那我在的include属性上,始终包含"Menu1"就行了(网上已经早有人写过类似解决方法,本文章的该解决方法也是参考该文章------见该文章)。

  • 首先,添加<RouterViewKeepAlive>组件

    新目录:src\layout\components\RouterViewKeepAlive\RouterViewKeepAlive.vue

    html 复制代码
    <!-- 父级路由组件,用于二级路由上, 该二级可以被keep-alive缓存 -->
    <!-- 由于该二级可以被keep-alive缓存,所以其三级的内容将保存 -->
    <!-- 注意:面包屑关闭后,不会从KeepAlive的include属性清除 -->
    <template>
      <div class="app-main">
        <router-view />
      </div>
    </template>
    <script>
    export default {
      name: 'RouterViewKeepAlive'
    }
    </script>
    <style lang="scss" scoped>
      .app-main {
        
      }
    </style>
  • 然后,在<AppMain>添加"cachedViews"计算属性上添加"RouterViewKeepAlive"

    目录:src\layout\components\AppMain.vue

    javascript 复制代码
    <script>
    export default {
      name: 'AppMain',
      computed: {
        cachedViews() {
          // return this.$store.state.tagsView.cachedViews
          // 加入RouterViewKeepAlive组件,总是缓存二级目录路由配置为"RouterViewKeepAlive"的
          return ['RouterViewKeepAlive', ...this.$store.state.tagsView.cachedViews]
        },
        key() {
          return this.$route.path
        }
      }
    }
    </script>
  • 最后,修改路由配置(以原项目Nested Routes路由配置nested.js为例)

    目录:src\router\modules\nested.js

    javascript 复制代码
    /** When your routing table is too long, you can split it into small modules **/
    
    import Layout from '@/layout'
    // 导入RouterViewKeepAlive
    import RouterViewKeepAlive from '@/layout/components/RouterViewKeepAlive/RouterViewKeepAlive.vue'
    
    const nestedRouter = {
      path: '/nested',
      component: Layout,
      redirect: '/nested/menu1/menu1-1',
      name: 'Nested',
      meta: {
        title: 'Nested Routes',
        icon: 'nested'
      },
      children: [
        {
          path: 'menu1',
          // component: () => import('@/views/nested/menu1/index'), // Parent router-view
          component: RouterViewKeepAlive, // 使用RouterViewKeepAlive作为二级组件
          // name: 'Menu1',
          name: 'RouterViewKeepAlive', // 名字改为"RouterViewKeepAlive",虽然没必要,但为了维护性
          meta: { title: 'Menu 1' },
          redirect: '/nested/menu1/menu1-1',
          children: [
            {
              path: 'menu1-1',
              component: () => import('@/views/nested/menu1/menu1-1'),
              name: 'Menu1-1',
              meta: { title: 'Menu 1-1' }
            },
            {
              path: 'menu1-2',
              component: () => import('@/views/nested/menu1/menu1-2'),
              name: 'Menu1-2',
              redirect: '/nested/menu1/menu1-2/menu1-2-1',
              meta: { title: 'Menu 1-2' },
              children: [
                {
                  path: 'menu1-2-1',
                  component: () => import('@/views/nested/menu1/menu1-2/menu1-2-1'),
                  name: 'Menu1-2-1',
                  meta: { title: 'Menu 1-2-1' }
                },
                {
                  path: 'menu1-2-2',
                  component: () => import('@/views/nested/menu1/menu1-2/menu1-2-2'),
                  name: 'Menu1-2-2',
                  meta: { title: 'Menu 1-2-2' }
                }
              ]
            },
            {
              path: 'menu1-3',
              component: () => import('@/views/nested/menu1/menu1-3'),
              name: 'Menu1-3',
              meta: { title: 'Menu 1-3' }
            }
          ]
        },
        {
          path: 'menu2',
          name: 'Menu2',
          component: () => import('@/views/nested/menu2/index'),
          meta: { title: 'Menu 2' }
        }
      ]
    }
    
    export default nestedRouter

    注意:由于配置了"component: RouterViewKeepAlive",使用了<RouterViewKeepAlive>作为二级组件,替换了'@/views/nested/menu1/index'的<Menu1>组件。

  • 结果

    可以看到"三级或以上的目录"被成功缓存了

注意:这种实现方式存在弊端,就是永远关不掉(TagsView上的关闭),会一直占用内存

②转变Router配置解决

现在方法一,存在"关闭面包屑却不会关闭该缓存,会一直占用内存 "的弊端,那怎么解决呢?

那找一下关闭<keep-alive>缓存的方法,不就解决了吗?

由于vue-element-admin项目是通过<keep-alive>的include来完成的,include如果没有加上"RouterViewKeepAlive",就会将所有的<RouterViewKeepAlive>没缓存,这不是我们想要的。我们想要的是"缓存对应key的<RouterViewKeepAlive>,然后移除对应key的<RouterViewKeepAlive>"。

但是,个人看了官网并未提供或暴露这种特殊的方法接口。

那现在我们换一种思路------"注册路由时,将三级或以上的路由配置转换为一级和二级的那样",如下图:

由于vue-element-admin项目在左侧菜单栏等地方用到了@/store的permission.js的"routes"。所以,现在的思路是"只改变Router的挂载,其他保持不改",步骤如下。

  • 首先,对permission.js,添加flattenRoutes方法和修改generateRoutes

    目录:src\store\modules\permission.js

    js 复制代码
    // ...
    /**
     * 将单个路由,假如有三级或三级目录,则转为二级目录格式
     * @param {Object} router 要处理的路由
     * @returns {Object} 处理后的路由
     */
    function flattenRouter(router) {
      // 创建一个新的对象来存储转换后的路由
      const newRouter = {
        ...router,
        children: []
      }
      const routerChildren = router.children
      // 从根路由开始扁平化
      if (routerChildren && routerChildren.length > 0) {
        flatten('', routerChildren)
      }
      /**
       * 递归函数来遍历和扁平化路由
       * @param {String} parentPath 父路由路径
       * @param {Array} routes 路由
       */
      function flatten(parentPath, routes) {
        routes.forEach(route => {
          const { path, children } = route
          // 构建完整的路径
          const fullPath = `${parentPath}${path.startsWith('/') ? path.slice(1) : path}`
          // 如果当前路由有子路由,则递归处理
          if (children && children.length > 0) {
            flatten(`${fullPath}/`, children)
          } else {
            // 否则,将当前路由添加到新的children数组中
            newRouter.children.push({
              ...route,
              path: fullPath
            })
          }
        })
      }
      return newRouter
    }
    
    /**
     * 处理路由,将三级或三级以上目录的转为二级目录格式
     * @param {Array} routes routes
     * @param {Array} 处理后的路由
     */
    export function flattenRoutes(routes) {
      const res = []
      routes.forEach(route => {
        const newRouter = flattenRouter(route)
        res.push(newRouter)
      })
      return res
    }
    
    // ...
    const actions = {
      generateRoutes({ commit }, roles) {
        return new Promise(resolve => {
          let accessedRoutes
          if (roles.includes('admin')) {
            accessedRoutes = asyncRoutes || []
          } else {
            accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
          }
          // 处理路由,将三级或三级以上目录的转为二级目录格式
          const flattenAccessedRoutes = flattenRoutes(accessedRoutes)
          // store存的是accessedRoutes,用于左侧边导航栏,多级目录(包含三级或以上)
          commit('SET_ROUTES', accessedRoutes)
          // resolve(accessedRoutes)
          // Promise的resolve出去的是flattenAccessedRoutes,用于路由,多级目录(已转换为二级目录格式,不包含三级或以上)
          resolve(flattenAccessedRoutes)
        })
      }
    }
    // ...

    完整代码如下:

    js 复制代码
    import { asyncRoutes, constantRoutes } from '@/router'
    
    /**
     * Use meta.role to determine if the current user has permission
     * @param roles
     * @param route
     */
    function hasPermission(roles, route) {
      if (route.meta && route.meta.roles) {
        return roles.some(role => route.meta.roles.includes(role))
      } else {
        return true
      }
    }
    
    /**
     * Filter asynchronous routing tables by recursion
     * @param routes asyncRoutes
     * @param roles
     */
    export function filterAsyncRoutes(routes, roles) {
      const res = []
    
      routes.forEach(route => {
        const tmp = { ...route }
        if (hasPermission(roles, tmp)) {
          if (tmp.children) {
            tmp.children = filterAsyncRoutes(tmp.children, roles)
          }
          res.push(tmp)
        }
      })
    
      return res
    }
    
    /**
     * 将单个路由,假如有三级或三级目录,则转为二级目录格式
     * @param {Object} router 要处理的路由
     * @returns {Object} 处理后的路由
     */
    function flattenRouter(router) {
      // 创建一个新的对象来存储转换后的路由
      const newRouter = {
        ...router,
        children: []
      }
    
      const routerChildren = router.children
      // 从根路由开始扁平化
      if (routerChildren && routerChildren.length > 0) {
        flatten('', routerChildren)
      }
    
      /**
       * 递归函数来遍历和扁平化路由
       * @param {String} parentPath 父路由路径
       * @param {Array} routes 路由
       */
      function flatten(parentPath, routes) {
        routes.forEach(route => {
          const { path, children } = route
          // 构建完整的路径
          const fullPath = `${parentPath}${path.startsWith('/') ? path.slice(1) : path}`
          // 如果当前路由有子路由,则递归处理
          if (children && children.length > 0) {
            flatten(`${fullPath}/`, children)
          } else {
            // 否则,将当前路由添加到新的children数组中
            newRouter.children.push({
              ...route,
              path: fullPath
            })
          }
        })
      }
    
      return newRouter
    }
    
    /**
     * 处理路由,将三级或三级以上目录的转为二级目录格式
     * @param {Array} routes routes
     * @param {Array} 处理后的路由
     */
    export function flattenRoutes(routes) {
      const res = []
      routes.forEach(route => {
        const newRouter = flattenRouter(route)
        res.push(newRouter)
      })
      return res
    }
    
    const state = {
      routes: [],
      addRoutes: []
    }
    
    const mutations = {
      SET_ROUTES: (state, routes) => {
        state.addRoutes = routes
        state.routes = constantRoutes.concat(routes)
      }
    }
    
    const actions = {
      generateRoutes({ commit }, roles) {
        return new Promise(resolve => {
          let accessedRoutes
          if (roles.includes('admin')) {
            accessedRoutes = asyncRoutes || []
          } else {
            accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
          }
          // 处理路由,将三级或三级以上目录的转为二级目录格式
          const flattenAccessedRoutes = flattenRoutes(accessedRoutes)
          // store存的是accessedRoutes,用于左侧边导航栏,多级目录(包含三级或以上)
          commit('SET_ROUTES', accessedRoutes)
          // resolve(accessedRoutes)
          // Promise的resolve出去的是flattenAccessedRoutes,用于路由,多级目录(已转换为二级目录,不包含三级或以上)
          resolve(flattenAccessedRoutes)
        })
      }
    }
    
    export default {
      namespaced: true,
      state,
      mutations,
      actions
    }
  • 然后,进行测试

    更改文件如下:

    修改nested.js的name:'Menu1-1'→'Menu11'。同理,'Menu12'、'Menu121'、'Menu122'、'Menu13'。

    目录:src\router\modules\nested.js

    javascript 复制代码
    /** When your routing table is too long, you can split it into small modules **/
    
    import Layout from '@/layout'
    
    const nestedRouter = {
      path: '/nested',
      component: Layout,
      redirect: '/nested/menu1/menu1-1',
      name: 'Nested',
      meta: {
        title: 'Nested Routes',
        icon: 'nested'
      },
      children: [
        {
          path: 'menu1',
          // component: () => import('@/views/nested/menu1/index'), // Parent router-view
          name: 'Menu1',
          meta: { title: 'Menu 1' },
          redirect: '/nested/menu1/menu1-1',
          children: [
            {
              path: 'menu1-1',
              component: () => import('@/views/nested/menu1/menu1-1'),
              name: 'Menu11', // 已更改,便于测试
              meta: { title: 'Menu 1-1' }
            },
            {
              path: 'menu1-2',
              // component: () => import('@/views/nested/menu1/menu1-2'),
              name: 'Menu12', // 已更改,便于测试
              redirect: '/nested/menu1/menu1-2/menu1-2-1',
              meta: { title: 'Menu 1-2' },
              children: [
                {
                  path: 'menu1-2-1',
                  component: () => import('@/views/nested/menu1/menu1-2/menu1-2-1'),
                  name: 'Menu121', // 已更改,便于测试
                  meta: { title: 'Menu 1-2-1' }
                },
                {
                  path: 'menu1-2-2',
                  component: () => import('@/views/nested/menu1/menu1-2/menu1-2-2'),
                  name: 'Menu122', // 已更改,便于测试
                  meta: { title: 'Menu 1-2-2' }
                }
              ]
            },
            {
              path: 'menu1-3',
              component: () => import('@/views/nested/menu1/menu1-3'),
              name: 'Menu13', // 已更改,便于测试
              meta: { title: 'Menu 1-3' }
            }
          ]
        },
        {
          path: 'menu2',
          name: 'Menu2',
          component: () => import('@/views/nested/menu2/index'),
          meta: { title: 'Menu 2' }
        }
      ]
    }
    
    export default nestedRouter

    修改'menu1-1\index.vue',添加和nested.js配置一样的name属性。同理,'menu1\menu1-2\menu1-2-1\index.vue'、'menu1\menu1-2\menu1-2-2\index.vue'、'menu1\menu1-3\index.vue'(注意:同时,将开头的"<template functional> "改为"<template> ")。

    以menu1-1为例,代码如下,其他的同理。

    目录:src\views\nested\menu1\menu1-1\index.vue

    javascript 复制代码
    <template>
      <div style="padding: 30px">
        <el-alert :closable="false" title="menu 1-1" type="success">
          <router-view />
        </el-alert>
      </div>
    </template>
    <!-- 修改后 -->
    <script>
    export default {
      // 添加name属性,让keep-alive进行缓存
      name: 'Menu11'
    }
    </script>
  • 结果

    可以看到"三级或以上的目录"被成功缓存了

注意事项:

①与方法一"添加<RouterViewKeepAlive>解决"的区别:

方法一是"使用了<RouterViewKeepAlive>作为二级组件,替换了'@/views/nested/menu1/index'的<Menu1>组件。 ";

而方法二是"扁平化路由",就如上方测试改nested.js那样,一些"component"的配置是没意义的,所以注释掉了

这种实现方式同样存在弊端,就是Breadcrumb 面包屑多级关系不见了 。因为,由于vue-element-admin项目Breadcrumb 面包屑是通过$route来实现的,而我们恰好改的就是路由配置。区别如下:

③上面,添加flattenRoutes方法只是对"accessedRoutes"做了处理,还未对"constantRoutes"处理,比如同时对"constantRoutes"处理。处理代码如下:

目录:src\router\index.js

js 复制代码
// ...
/* 处理路由 */
import { flattenRoutes } from '@/store/modules/permission'

// ...
const createRouter = () => new Router({
  // mode: 'history', // require service support
  scrollBehavior: () => ({ y: 0 }),
  // routes: constantRoutes
  // 处理路由,将三级或三级以上目录的转为二级目录格式
  routes: flattenRoutes(constantRoutes)
})
// ...

③直接移除include解决

此方法官方文档此次提到"前往@/layout/components/AppMain.vue文件下,移除include相关代码即可。当然直接使用 keep-alive 也是有弊端的,他并不能动态的删除缓存,你最多只能帮它设置一个最大缓存实例的个数 limit。"

更改如下:

目录:src\layout\components\AppMain.vue

js 复制代码
<template>
  <section class="app-main">
    <transition name="fade-transform" mode="out-in">
      <!-- 移除include -->
      <!-- <keep-alive :include="cachedViews"> -->
      <keep-alive>
        <router-view :key="key" />
      </keep-alive>
    </transition>
  </section>
</template>
// ...

如果,想要设置最大缓存个数,比如设置最大10个。只需将"<keep-alive>"改为"<keep-alive :max="10">"

注意事项:

①该方法的弊端:官方文档说的也很清楚了------"他并不能动态的删除缓存,只能帮它设置一个最大缓存实例的个数 "。

②与方法一的对比:没使用了<RouterViewKeepAlive>作为二级组件,替换了'@/views/nested/menu1/index'的<Menu1>组件;该方法的"他并不能动态的删除缓存"的范围比方法一的范围大 ,该方法所有的目录都不能动态删除缓存,而方法一是三级或以上的目录不能移除。

③与方法二的对比:没"扁平化路由"无方法二的弊端------Breadcrumb 面包屑多级关系不见了

三、总结

vue-element-admin解决三级目录的KeepAlive缓存问题:

①添加<RouterViewKeepAlive>解决

弊端:永远关不掉(TagsView上的关闭),会一直占用内存

(可以给keep-alive设置一个最大缓存实例的个数,但不一定满足项目需求;如果该项目三级或以上的目录不多,就几个,那还能接受内存的占用)

②转变Router配置解决

弊端:Breadcrumb 面包屑多级关系不见了

(如果真实项目,无需"Breadcrumb 面包屑"同时最多三级(见方法二的"注意事项"),还可以接受。)

③直接移除include解决

弊端:他并不能动态的删除缓存,只能帮它设置一个最大缓存实例的个数

(如果真实项目,可以接受"不能动态的删除缓存"和"设置最大缓存实例的个数"的弊端,那该方法是最简单的解决方法。)

这里强调一下 :由于上述方法是对原本vue-element-admin项目构建上的修复,一旦按照文章修复了,一定要记得项目的可维护性,不然,下一个接手该项目的码农将会很疑惑。比如,在真实项目的"README.md"上添加修改的文字描述和路由配置注意事项,同时在src\router\index.js的路由配置上注释好。

最终,似乎都没有十全十美的解决方案,每一种方案总是存在一些"舍去" 。就vue-element-admin的作者在文档提过"如果没有标签导航栏需求的用户,建议移除此功能"

网上也有更多的解决方法,比如:

如果想了解更多关于vue-element-admin项目<keep-alive>不缓存的原因,也欢迎看看个人的另一篇文章

如果大家有其他更完美的解决方案或者本文章方法的不足之处,欢迎在评论区讨论!

四、参考文献

相关推荐
日升_rs2 分钟前
Chrome 134 版本新特性
前端·chrome·浏览器
前端卧龙人3 分钟前
前端如何最小化重绘和回流
前端
南蓝4 分钟前
【React】React 19 新特性
前端
银之夏雪4 分钟前
深入理解 GPU 渲染加速与合成层(Composite Layers)
前端·javascript·浏览器
机巧咸鱼不会受伤5 分钟前
为什么浏览器的渲染进程不适用于多个线程来处理
前端·浏览器
前端日常开发6 分钟前
轻松合并远端两个Commit,代码历史秒变整洁!
前端
Au_ust6 分钟前
React:类组件(上)
前端·javascript·react.js
前端日常开发6 分钟前
前端并发请求太多?教你几招轻松搞定!
前端
myyyl7 分钟前
Javascript中哈希表的实现
前端·算法·面试
the_flash7 分钟前
手写lodash的get函数
前端