Code前端首页关于Code前端联系我们

Vue3使用keep-alive后路由缓存失效怎么彻底解决?

terry 1天前 阅读数 339 #Vue

先搞懂Vue3路由缓存的基础逻辑

很多刚上手Vue3组合式API的开发者,容易直接把Vue2的keep-alive经验搬过来,结果踩一堆缓存失效的坑,其实先捋清楚它的底层逻辑,解决问题才不会瞎试错。 keep-alive本质是Vue提供的抽象组件,它本身不会渲染DOM节点,只会包裹子组件并控制它们的激活/失活,保留组件实例的状态和DOM结构,Vue3组合式API虽然重构了组件生命周期,但keep-alive的核心依赖没变:需要被缓存的组件必须有唯一的标识。 以前Vue2里,唯一标识要么是组件的name属性(推荐),要么是keep-alive的include/exclude数组里的字符串;但Vue3 setup语法糖刚出来的时候,很多人忘了加<script setup name="xxx">——因为默认setup语法糖不会暴露组件名,include/exclude找不到匹配项,自然不会缓存,这是新手最常犯的第一个低级错误。 keep-alive包裹的必须是动态的组件占位,在Vue Router里就是<router-view>,而且Vue3 Router 4.x开始,<router-view>必须通过v-slot拿到Componentroute对象,再把Component传给keep-alive的component动态组件属性——哦对了,Vue3.3.0之前,v-slot写法还有几个坑点,但现在用最新版本的话基本没问题,但如果你用的是旧版Router 4.x,可能得注意<router-view>不能直接加key属性在外面,得加在里面的动态组件上,不然切换路由时不管是不是同一个匹配项,都会强制销毁重建实例,等于白加keep-alive。

常见的Vue3路由缓存失效场景及对应修复方法

场景1:setup语法糖里忘了加name属性

刚说过这个是新手最多的,但哪怕是熟手,有时候重构组件或者拆分组件块,也容易漏写,Vue3里普通组件(非setup语法糖)可以直接在export default里加name,setup语法糖的话,要么用<script setup name="UserDetail">这种写法(需要Vue版本≥3.2.34,现在绝大多数项目都满足),要么在同一个文件里再加一个普通的<script>标签专门导出name,不过后者比较冗余,推荐前者。 举个正确的例子:

<!-- UserDetail.vue -->
<script setup name="UserDetail">
import { ref, onActivated } from 'vue'
const inputVal = ref('')
const scrollTop = ref(0)
onActivated(() => {
  // 组件重新激活时恢复滚动位置
  document.querySelector('.detail-scroll').scrollTop = scrollTop.value
})
</script>

然后在路由配置或者根组件/App.vue的keep-alive里,include数组里加上'UserDetail'就行,这里要注意大小写!组件名、include里的字符串,必须完全一致,Vue对字母大小写是敏感的。

场景2:<router-view>的key属性加错位置

这个场景分两种情况:一种是key加在<router-view>标签本身,另一种是key的取值有问题。 先说加错位置:比如你这样写:

<!-- App.vue(错误写法) -->
<template>
  <nav>...</nav>
  <keep-alive :include="cachedViews">
    <router-view :key="$route.fullPath" />
  </keep-alive>
</template>

这时候不管你加不加keep-alive,只要路由的fullPath变了,Vue就会销毁旧的组件实例,重建新的,滚动位置、表单输入值这些状态全没了——Router 4.x的<router-view>本质是个插槽容器,外层加key相当于给容器换了身份,里面的动态组件自然也会被强制销毁。 正确的写法是用v-slot拿到Component和route,把key加在动态组件上,而且key的取值要只在需要重建组件的时候才变

<!-- App.vue(推荐写法) -->
<template>
  <nav>...</nav>
  <router-view v-slot="{ Component, route }">
    <keep-alive :include="cachedViews">
      <component :is="Component" :key="route.meta.needKey ? route.fullPath : route.name" />
    </keep-alive>
  </router-view>
</template>

这里解释下key的取值逻辑:

  • 如果某个页面不管参数怎么变都需要缓存(比如用户详情页,不管看哪个用户的,缓存上一个用户的输入草稿或者滚动位置是有意义的),就用route.name当key——同一个路由名称的页面,Vue会认为是同一个组件实例,只要在include里就会保留。
  • 如果某个页面参数一变就必须重建(比如商品列表页,分类、页码变了就要重新请求接口,不能用上一个分类的缓存),就在路由配置的meta里加个needKey: true,这时候用route.fullPath当key,参数一变key就变,组件实例就会销毁重建。

场景3:路由配置里的path参数有歧义或者动态路由嵌套层级不对

这个场景虽然不那么常见,但踩坑后非常难排查,比如你有两个动态路由:

// router/index.js(有歧义的写法)
const routes = [
  { path: '/user/:id', name: 'UserDetail', component: () => import('@/views/UserDetail.vue') },
  { path: '/user/me', name: 'MyProfile', component: () => import('@/views/MyProfile.vue') }
]

这里注意路由的顺序!Vue Router是从上到下匹配路由规则的,如果你把/user/:id放在前面,访问/user/me的时候,Router会把'me'当成id参数,匹配到UserDetail组件,而不是MyProfile组件——如果这时候你只在include里加了'MyProfile',那UserDetail组件(虽然你以为访问的是MyProfile)自然不会被缓存,而且接口请求的也是id为'me'的用户数据,状态完全不对。 修复方法很简单:把静态路由放在动态路由前面,这样Router会优先匹配静态路径。 另外还有一种情况:嵌套路由的父组件没有用<router-view>,或者父组件的<router-view>没有被keep-alive包裹,但子组件又需要缓存——这时候哪怕子组件在include里,状态也会丢失,因为父组件销毁了,子组件实例也会跟着被销毁。

场景4:使用了第三方库或者插件破坏了组件实例的激活/失活状态

这种情况比较少见,但随着项目复杂度的增加,可能会遇到,比如有些第三方弹窗库,会在组件卸载前强制移除某些DOM节点,或者修改组件的内部状态;有些第三方状态管理库的旧版本,对keep-alive的onActivated/onDeactivated生命周期支持不好。 排查这种问题的方法是:先把第三方库或者插件去掉,看看缓存能不能正常工作,如果可以,就逐个加回来,定位是哪个库的问题,然后去查这个库的官方文档,看看有没有关于keep-alive的配置项或者兼容版本。

场景5:手动操作了组件的$destroy或者v-if销毁了被keep-alive包裹的组件

keep-alive只能控制自己直接包裹的动态组件的激活/失活,如果你在组件内部用this.$destroy()(组合式API里没有直接的$destroy方法,要用getCurrentInstance()?.proxy?.$destroy(),但强烈不推荐),或者在父组件里用v-if控制keep-alive的显示隐藏,或者用v-if控制被keep-alive包裹的外层容器的显示隐藏,组件实例都会被直接销毁,状态自然丢失。 Vue3里如果需要手动销毁某个被缓存的组件,不能直接用v-if,应该用keep-alive的include/exclude数组的动态变化来实现——比如把某个组件名从include数组里移除,Vue会自动销毁该组件的实例;如果之后又需要缓存,再把组件名加回来就行。 举个手动清除缓存的例子(比如用户退出登录后,要清除所有已缓存的页面):

<!-- App.vue -->
<script setup>
import { ref, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
const cachedViews = ref(['Home', 'UserList'])
// 比如在登录成功或者退出登录的逻辑里
const clearAllCache = () => {
  cachedViews.value = []
  // 等DOM更新后再把需要的组件加回来,比如Home
  setTimeout(() => {
    cachedViews.value = ['Home']
  }, 0)
}
</script>

这里用setTimeout是因为Vue的DOM更新是异步的,移除组件名后,需要等Vue处理完销毁逻辑,再把需要的组件加回来,否则可能会出现缓存不生效的情况。

进阶:如何实现Vue3路由的部分缓存?

有时候我们不是需要缓存整个页面,而是需要缓存页面的某一部分——比如商品列表页,顶部的筛选栏状态需要保留,但中间的商品列表需要根据参数变化重新请求,这时候keep-alive包裹整个路由组件就不太合适了,因为商品列表会被缓存,不会重新请求接口。 实现部分缓存的方法有两种:

方法1:把不需要缓存的部分放在onActivated生命周期里重新初始化

比如商品列表页,筛选栏的状态用ref保存,会被keep-alive缓存;商品列表的数据用ref保存,但在onActivated生命周期里,根据当前路由的参数重新请求接口,覆盖掉旧的数据:

<script setup name="ProductList">
import { ref, onActivated } from 'vue'
import { useRoute } from 'vue-router'
import { getProductList } from '@/api/product'
const route = useRoute()
// 筛选栏状态,会被缓存
const category = ref('all')
const priceRange = ref([0, 9999])
// 商品列表数据,需要重新请求
const productList = ref([])
const loading = ref(false)
// 初始化或者重新激活时请求商品列表
const fetchProductList = async () => {
  loading.value = true
  try {
    const res = await getProductList({
      category: category.value,
      priceMin: priceRange.value[0],
      priceMax: priceRange.value[1],
      page: route.query.page || 1
    })
    productList.value = res.data
  } catch (err) {
    console.error(err)
  } finally {
    loading.value = false
  }
}
onActivated(() => {
  fetchProductList()
})
</script>

这种方法比较简单,适合不需要缓存的部分只有数据的场景。

方法2:把需要缓存的部分单独抽成一个子组件,用keep-alive包裹该子组件

比如商品列表页,把筛选栏单独抽成ProductFilter.vue组件,加上name属性,然后在父组件里用keep-alive包裹它;商品列表放在父组件的正常位置,或者抽成另一个不需要缓存的子组件:

<!-- ProductList.vue -->
<template>
  <keep-alive include="ProductFilter">
    <ProductFilter v-model:category="category" v-model:priceRange="priceRange" />
  </keep-alive>
  <ProductListCore :category="category" :priceRange="priceRange" :key="route.fullPath" />
</template>

这种方法更灵活,适合需要缓存的部分有复杂的DOM结构或者交互逻辑的场景——比如筛选栏有一个展开/收起的动画,抽成单独的组件用keep-alive包裹,动画的展开/收起状态也会被保留。

Vue3路由缓存的最佳实践

为了避免路由缓存失效的问题,建议大家遵循以下最佳实践:

  1. 所有需要被缓存的组件都必须加唯一的name属性,setup语法糖用<script setup name="xxx">,普通组件用export default里的name。
  2. Router 4.x必须用v-slot写法,把keep-alive包裹在<router-view>的插槽里,把key加在里面的动态组件上。
  3. key的取值要合理,根据页面需求选择route.name或者route.fullPath,也可以用meta里的自定义变量控制。
  4. 静态路由放在动态路由前面,嵌套路由的父组件必须正确使用<router-view>,如果子组件需要缓存,父组件的<router-view>最好也被keep-alive包裹(或者父组件本身在include里)。
  5. 不要手动销毁组件实例,如果需要清除缓存,用include/exclude数组的动态变化来实现。
  6. 实现部分缓存时,根据场景选择合适的方法

再提醒大家一句:Vue的版本和Vue Router的版本要尽量保持最新,旧版本可能会有一些已知的keep-alive兼容问题,升级到最新版本通常可以解决很多莫名其妙的bug。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门