vue3 KeepAlive 组件缓存失效 bug 分析 BUG

#前端>Vue

vue3 组合式 API 风格中,使用 setup 语法糖创建的组件出现缓存失效的 bug

分析

路由设置了 name 属性,KeepAlive 组件中设置了排除项(exclude),核验无误,但排除失效了

经网络方案搜寻,根源在于 KeepAlive 组件的 include/exclude 指的并不是路由名称,而是组件名称

官网说明

keep-alive

组件 name 选项

TIP

在 3.2.34 或以上的版本中,使用 setup 语法糖的单文件组件会自动根据文件名生成对应的 name 选项,无需再手动声明。

使用 vue devtools 调试可以很清楚的看到哪些组件被缓存了:

以排除的“Home”组件为例,路由及对应文件如下:

js
{
  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

解决方案

参考

vue keep-alive

vue3 中使用 keepAlive 缓存路由组件不生效的情况记录

vue3 中使用 keepAlive 缓存路由组件不生效的问题解决

不要相信组件名称自动推导机制

1. 唯一命名单文件组件

单文件组件命名时就考虑其唯一性

text
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 选项即可

js
export default {
  name: 'Home',
  // ...
}

• vue3 setup 语法糖

既然 vue 3.2.34 及以上的版本中 setup 语法糖创建的单文件组件会自动生成的 name 选项

如果单文件组件命名无法保证其唯一性

要么,手动附加一个 script 声明 name 选项

或者,使用宏defineOptions在组件内声明 name 选项

vue
<script setup>
import { defineOptions } from 'vue'

defineOptions({ name: 'Home' })
</script>
<template>
  <!-- ... -->
</template>
vue
<script>
export default { name: 'Home' }
</script>
<script setup>
// ...
</script>
<template>
  <!-- ... -->
</template>

script 的 lang 属性如有需保持一致

• jsx/tsx

可使用 defineComponent,例如:

jsx
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' }
)
jsx
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 组件中渲染包裹后的组件

vue
<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 列表是比较繁琐且易出错的,在全局路由守卫中可以很方便的进行动态收集:

js
// 全局路由拦截
router.afterEach((to) => {
  // 当进入一个新页面时,把它的名字加入缓存池
  if (to.meta.keepAlive) {
    useCacheStore().addCachedView(to.name)
    // 👆 注意这里!我们能最轻易拿到的标识,是路由对象的 to.name
  }
})

如果组件名称与路由名称不一致,那么动态收集时需要再去查找组件名称。要么将组件名存储在路由meta信息中,要么维护一套映射关系。

其他

Vue3 中缓存失效还有一种场景,那就是项目中使用了嵌套/多级路由,详见文章 Vue 进阶:破解嵌套路由 KeepAlive 缓存失效的架构级指南

Cornor Blog