vue3 KeepAlive 组件缓存失效 bug 分析 BUG
vue3 组合式 API 风格中,使用 setup 语法糖创建的组件出现缓存失效的 bug
分析
路由设置了 name 属性,KeepAlive 组件中设置了排除项(exclude),核验无误,但排除失效了
经网络方案搜寻,根源在于 KeepAlive 组件的 include/exclude 指的并不是路由名称,而是组件名称
官网说明
TIP
在 3.2.34 或以上的版本中,使用 setup 语法糖的单文件组件会自动根据文件名生成对应的 name 选项,无需再手动声明。
使用 vue devtools 调试可以很清楚的看到哪些组件被缓存了:
以排除的“Home”组件为例,路由及对应文件如下:
{
path: 'home',
component: () => import('@/views/Dashboard/Home/index.vue'),
name: 'Home'
}KeepAlive 组件中缓存的组件 name 是 Index
TIP
有些脚手架下,index.vue的name推导会取上级文件夹名。
@/views/Dashboard/Home/index.vue: Home@/views/release-history/index.vue: ReleaseHistory
解决方案
参考
不要相信组件名称自动推导机制
1. 唯一命名单文件组件
单文件组件命名时就考虑其唯一性
src/xxx/index.vue -> src/xxx/xxxIndex.vue
src/xxx/list.vue -> src/xxx/xxxList.vue
src/yyy/index.vue -> src/xxx/yyyIndex.vue
src/yyy/list.vue -> src/xxx/yyyList.vue有人愿意为此更改代码规范吗?🤣
2. 设置组件 name 选项
• vue2 & vue3 非 setup 语法糖
显示声明 name 选项即可
export default {
name: 'Home',
// ...
}• vue3 setup 语法糖
既然 vue 3.2.34 及以上的版本中 setup 语法糖创建的单文件组件会自动生成的 name 选项
如果单文件组件命名无法保证其唯一性
要么,手动附加一个 script 声明 name 选项
或者,使用宏defineOptions在组件内声明 name 选项
<script setup>
import { defineOptions } from 'vue'
defineOptions({ name: 'Home' })
</script>
<template>
<!-- ... -->
</template><script>
export default { name: 'Home' }
</script>
<script setup>
// ...
</script>
<template>
<!-- ... -->
</template>script 的 lang 属性如有需保持一致
• jsx/tsx
可使用 defineComponent,例如:
import { ref } from 'vue'
export default defineComponent(
props => {
const count = ref(0)
const handleAdd = () => count.value++
return () => (
<div>
<p>count: {count.value}</p>
<button onClick={handleAdd}>Add</button>
</div>
)
},
{ name: 'TestJsxComp' }
)import { ref } from 'vue'
export default defineComponent({
name: 'TestJsxComp',
setup() {
const count = ref(0)
const handleAdd = () => count.value++
return () => (
<div>
<p>count: {count.value}</p>
<button onClick={handleAdd}>Add</button>
</div>
)
},
})借助社区插件
借助社区插件拓展 <script setup> 语法糖,实现 name 选项的快速声明。例如:<script setup name="Home">
3. 外包一层组件声明 name 选项
在 Component 组件中渲染包裹后的组件
<script setup>
import { h, computed } from 'vue'
const excludeKeepAlive = computed(() => /* ... */)
function formatComponentInstance(comp, route) {
if (!route.name || !excludeKeepAlive.value.includes(route.name)) return comp
return {
name: route.name,
render() {
return h(comp)
}
}
}
</script>
<template>
<router-view>
<template #default="{ Component, route }">
<keep-alive :exclude="excludeKeepAlive">
<component :is="formatComponentInstance(Component, route)" :key="route.fullPath" />
</keep-alive>
</template>
</router-view>
</template>小结
第一种方案最简单,但对组件命名有要求。
第二种方案更标准,需要一一声明好组件 name 选项。
第三种方法统一外包一层,根据路由名称声明包裹组件 name 选项。
个人推荐
个人推荐使用第二种方法,且组件名称与路由名称保持一致,这样有利于进行动态缓存管理
固定 KeepAlive 的 include 列表是比较繁琐且易出错的,在全局路由守卫中可以很方便的进行动态收集:
// 全局路由拦截
router.afterEach((to) => {
// 当进入一个新页面时,把它的名字加入缓存池
if (to.meta.keepAlive) {
useCacheStore().addCachedView(to.name)
// 👆 注意这里!我们能最轻易拿到的标识,是路由对象的 to.name
}
})如果组件名称与路由名称不一致,那么动态收集时需要再去查找组件名称。要么将组件名存储在路由meta信息中,要么维护一套映射关系。
其他
Vue3 中缓存失效还有一种场景,那就是项目中使用了嵌套/多级路由,详见文章 Vue 进阶:破解嵌套路由 KeepAlive 缓存失效的架构级指南