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) }) } } // ...
完整代码如下:
jsimport { 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>不缓存的原因,也欢迎看看个人的另一篇文章。
如果大家有其他更完美的解决方案或者本文章方法的不足之处,欢迎在评论区讨论!