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>不缓存的原因,也欢迎看看个人的另一篇文章

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

四、参考文献

相关推荐
WeiShuai10 分钟前
vue-cli3使用DllPlugin优化webpack打包性能
前端·javascript
攻城狮的梦11 分钟前
redis集群模式连接
数据库·redis·缓存
forwardMyLife15 分钟前
element-plus的面包屑组件el-breadcrumb
javascript·vue.js·ecmascript
ice___Cpu16 分钟前
Linux 基本使用和 web 程序部署 ( 8000 字 Linux 入门 )
linux·运维·前端
JYbill18 分钟前
nestjs使用ESM模块化
前端
加油吧x青年37 分钟前
Web端开启直播技术方案分享
前端·webrtc·直播
计算机学姐1 小时前
基于python+django+vue的影视推荐系统
开发语言·vue.js·后端·python·mysql·django·intellij-idea
luoluoal1 小时前
java项目之基于Spring Boot智能无人仓库管理源码(springboot+vue)
java·vue.js·spring boot
吕彬-前端1 小时前
使用vite+react+ts+Ant Design开发后台管理项目(二)
前端·react.js·前端框架
小白小白从不日白1 小时前
react hooks--useCallback
前端·react.js·前端框架