vue-element-admin解决三级目录的KeepAlive缓存问题(配置版)
本文章只是列出三种解决方法具体怎么改。本文章比较粗略,建议改完后,看看详情版的,里面有更多细节,请前往详情版。
一、解决方法
①添加<RouterViewKeepAlive>解决
本文章的该解决方法,参考该文章——见该文章。
-
首先,添加<RouterViewKeepAlive>组件
新目录:src\layout\components\RouterViewKeepAlive\RouterViewKeepAlive.vue<!-- 父级路由组件,用于二级路由上, 该二级可以被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<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/** 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配置解决
现在方法一,存在“关闭面包屑却不会关闭该缓存,会一直占用内存”的弊端,那怎么解决呢?
我们可以换一种思路——“注册路由时,将三级或以上的路由配置转换为一级和二级的那样”,如下图:
由于vue-element-admin项目在左侧菜单栏等地方用到了@/store的permission.js的“routes”。所以,现在的思路是“只改变Router的挂载,其他保持不改”,步骤如下。
-
首先,对permission.js,添加flattenRoutes方法和修改generateRoutes
目录:src\store\modules\permission.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) }) } } // ...
完整代码如下:
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 }
-
测试,见详情版
-
结果
可以看到“三级或以上的目录”被成功缓存了
注意事项:
①与方法一“添加<RouterViewKeepAlive>解决”的区别:
方法一是“使用了<RouterViewKeepAlive>作为二级组件,替换了’@/views/nested/menu1/index’的<Menu1>组件。”;
而方法二是“扁平化路由”,就如上方测试改nested.js那样,一些“component”的配置是没意义的,所以注释掉了。
②这种实现方式同样存在弊端,就是Breadcrumb 面包屑多级关系不见了。因为,由于vue-element-admin项目Breadcrumb 面包屑是通过$route来实现的,而我们恰好改的就是路由配置。区别如下:
③上面,添加flattenRoutes方法只是对“accessedRoutes”做了处理,还未对“constantRoutes”处理,比如同时对“constantRoutes”处理。处理代码如下:
目录:src\router\index.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
<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>不缓存的原因,也欢迎看看个人的另一篇文章。
如果大家有其他更完美的解决方案或者本文章方法的不足之处,欢迎在评论区讨论!