我正在参与掘金创作者训练营第4期,点击了解活动详情,一起学习吧!
起因
我们公司是主做toB的业务,常开发的是后台管理系统。从19年开始,大部分新开项目前端技术栈都从原jsp + layui +jquery
变成vue全家桶 + elementui
,新框架基于vue-element-admin-master
二次开发。稳定上线一年多后,前一段客户突然反应,右键删除一个标签页后再打开还是缓存的上次的值,没有重新初始化,我测试了好多遍,发现问题并不总是重现
这是什么问题,黑人问号
发现问题
通过调试工具和vue devtools
一点一点调试终于发现了这个bug,二级路由是正常缓存的,问题出在三级以上路由。
缓存的逻辑主要在layout
下面的AppMain
组件中
里面的cachedViews
是在store中维护,记录路由的name值。
假设有两个三级路由xxx/#/a/b/c
、xxx/#/a/b/d
如果想要第三级c和d缓存,常用的方案就是把b也加入到keep-alive组件的include
参数中,也就是include
的值为['b', 'c', 'd']
。
上面两个路由顺序点开后,通过Vue Devtools中的Computent里面看到的结构应该是
-Root
-App
-Layout
-Navbar
-Sidebar
-TagsView
-AppMain
-b [inactive]
-c [inactive]
-b
-d
-keep-alive
复制代码
现在点击路由xxx/#/a/b/d
标签的删除是删除不了d的缓存。
从keep-alive源码的逻辑看(位置在vue源码中的src/core/components/keep-alive.js)
function matches (pattern: string | RegExp | Array<string>, name: string): boolean {
if (Array.isArray(pattern)) {
return pattern.indexOf(name) > -1
} else if (typeof pattern === 'string') {
return pattern.split(',').indexOf(name) > -1
} else if (isRegExp(pattern)) {
return pattern.test(name)
}
/* istanbul ignore next */
return false
}
// 删除缓存
function pruneCache (keepAliveInstance: any, filter: Function) {
const { cache, keys, _vnode } = keepAliveInstance
for (const key in cache) {
const cachedNode: ?VNode = cache[key]
if (cachedNode) {
// 主要问题在这个地方 name总是取的二级路由组件的名称,导致删除不掉
const name: ?string = getComponentName(cachedNode. componentOptions)
// 本例子name获取的是b 因为filter(name)为true 所以就没有走下面的删除缓存逻辑
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
mounted () {
// 在初始化后监听include的变化,决定是否删除缓存
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
}
复制代码
当点击标签上的刷新时,会把include数组中的d删除,但是inactive状态缓存d并没有被删掉,原因就在于触发include监听后,pruneCache中 const name: ?string = getComponentName(cachedNode.componentOptions)
获取到的name
为b,因为b还存在,就无法删除对应的缓存d。
解决问题
问题只要找到,再寻找解决方案就容易许多。所以最终选择的解决方案
- 方法一:参考一位大佬的方法(看了之后忘了保存网址)。因为二级路由是没有这个问题的,所以把数据整体改为二级路由,菜单使用原多级数据,addRoute数据改为二级路由(亲测可以解决)
// 原始数据用于左侧菜单展示使用
const nestedRouter = {
path: '/test',
component: Layout,
redirect: '/test/menu1/menu1-1',
name: 'test',
children: [
{
path: 'menu1',
component: () => import('@/views/test/menu1/index'), // Parent router-view
name: 'Menu1',
redirect: '/test/menu1/menu1-1',
children: [
{
path: 'menu1-1',
component: () => import('@/views/test/menu1/menu1-1'),
name: 'Menu1-1'
},
{
path: 'menu1-2',
component: () => import('@/views/test/menu1/menu1-2'),
name: 'Menu1-2'
}
]
}
]
}
// 通过递归处理后的数据 可以用于addRoute使用
const nestedRouter = {
path: '/test',
component: Layout,
redirect: '/test/menu1/menu1-1',
name: 'test',
children: [
{
path: 'menu1-1',
component: () => import('@/views/test/menu1/menu1-1'),
name: 'Menu1-1'
},
{
path: 'menu1-2',
component: () => import('@/views/test/menu1/menu1-2'),
name: 'Menu1-2'
}
]
}
复制代码
- 方法二:由源码入手
关键点在于如何匹配到想要删除的Cache,从现在看来想要从源码上实现可以修改匹配条件,只能临时解决问题,但是想要所有的情况都适用这条路不好实现,可能尤大设计的就是很简单的模式,没有考虑这么复杂的场景。如果觉得有必要可以自己写一个keep-alive组件。
有哪些写的不好,欢迎各位大佬指正