改进的Vue缓存策略:进入时重置模式

489 阅读4分钟

Vue 的 KeepAlive 组件是一个内置的抽象组件,主要用于缓存组件实例,避免重复渲染,提高性能和用户体验。但在某些场景下,我们希望在进入页面时清除之前的缓存。

使用场景举例

场景一:表单页面需初始化

假如你有一个表单页面,有大量的字段需要填写,其中某个字段需要跳转新的页面进行选择。这个时候表单页面肯定需要被keep-alive包裹,否则从选择页面返回后,表单页面已填写的数据会丢失。在此基础之上又希望每次进入表单页面(准备添加新数据)的时候,表单是个干净的表单,没有任何历史数据,这个时候就需要把之前的缓存清除掉

场景二:列表页面需刷新数据

有一个列表页面(分页或者垂直滚动),当点击了列表中某一条数据进入详情后再返回希望保留滚动位置或者页码,这个时候列表页面也需要被keep-alive包裹,但是当每次进入列表页面时候都希望看到最新的数据,也需要把之前的缓存清楚掉

官方未支持,但社区方案丰富

关于清楚缓存的这个需求,不管是vue2还是vue3,谈论一直没断过,参开仓库issue

官方讨论链接

然而官方一直没有支持这个需求,其实民间也有不少方案来实现这个需求,比如

  1. 动态调整component组件的key,参考juejin.cn/post/711207…
  2. 动态调整keep-aliveinclude

之前写过一篇文章:介绍的是利用动态调整component组件的key来实现需求,这个方案不够灵活,侵入性太强。下面介绍另外一种方案:动态调整include

实现方案

1. 路由配置增强

在路由配置中添加新的元信息:

const routes = [
  {
    path: '/a',
    name: 'PageA',
    component: PageA,
    meta: {
      keepAlive: true,
      resetOnEnter: false // 不需要重置的页面
    }
  },
  {
    path: '/b',
    name: 'FormB',
    component: FormB,
    meta: {
      keepAlive: true,
      resetOnEnter: true, // 进入时重置
      exceptFrom: ['select'] // 例外:从select页面返回时不重置
    }
  },
  {
    path: '/c',
    name: 'Select',
    component: SelectC,
    meta: {
      keepAlive: true,
      resetOnEnter: false
    }
  }
]

2. 导航守卫或者在使用keep-alive页面使用watch实现实现

在路由导航守卫中实现缓存控制逻辑:

router.beforeEach((to, from, next) => {
  const navigationStore = useNavigationStore();
  
  // 记录导航历史(可选,用于调试或其他目的)
  navigationStore.addToHistory(to);
  
  // 如果目标页面需要在进入时重置
  if (to.meta.resetOnEnter) {
    // 检查是否有例外情况
    const exceptFromRoutes = to.meta.exceptFrom || [];
    if (!exceptFromRoutes.includes(from.name)) {
      // 不在例外列表中,移除缓存
      navigationStore.removeCache(to.name);
    }
  }
  
  // 如果目标页面需要缓存,添加到缓存列表
  if (to.meta.keepAlive) {
    // 确保在下一个tick添加缓存,避免移除后立即添加的问题
    nextTick(() => {
      navigationStore.addCache(to.name);
    });
  }
  
  next();
});

watch实现

<script setup>
import { RouterView } from 'vue-router'
import {useNavigation} from "@/hooks/useNavigation"
import { useRouter } from 'vue-router'
import { watch } from 'vue'

const { keepAliveComponents, handleRouteChange } = useNavigation()
const router = useRouter()

// 监听路由变化
watch(
  () => router.currentRoute.value,
  (to, from) => {
    handleRouteChange(to, from)
  }
)
</script>

<template>
 <router-view v-slot="{ Component }">
    <keep-alive :include="keepAliveComponents">
      <component :is="Component" />
    </keep-alive>
  </router-view>
</template>

3. 状态管理或者hooks实现

缓存状态管理保持不变

// stores/navigation.js
import { defineStore } from 'pinia';

export const useNavigationStore = defineStore('navigation', {
  state: () => ({
    history: [], // 导航历史记录(可选)
    cachedViews: [] // 当前缓存的页面
  }),
  actions: {
    addToHistory(route) {
      this.history.push({
        path: route.path,
        name: route.name,
        fullPath: route.fullPath
      });
      // 保持历史记录在合理长度
      if (this.history.length > 20) {
        this.history.shift();
      }
    },
    addCache(name) {
      if (name && !this.cachedViews.includes(name)) {
        this.cachedViews.push(name);
      }
    },
    removeCache(name) {
      const index = this.cachedViews.indexOf(name);
      if (index > -1) {
        this.cachedViews.splice(index, 1);
      }
    }
  }
});

hooks实现

import { ref, computed, nextTick } from 'vue'

const cacheViews = ref(new Set())

export function useNavigation() {
  // 计算当前需要缓存的组件列表
  const keepAliveComponents = computed(() => {
    return Array.from(cacheViews.value)
  })

  // 如果目标页面需要在进入时重置
  const handleRouteChange = (to, from) => {
    if (to.meta?.resetOnEnter) {
      const exceptFromRoutes = to.meta.exceptFrom || []
      const customResetCondition =
        typeof to.meta.resetWhen === 'function' ? to.meta.resetWhen(to, from) : false

      if (!exceptFromRoutes.includes(from.name) || customResetCondition) {
        cacheViews.value.delete(to.name)
      }
    }

    // 如果目标页面需要缓存,添加到缓存列表
    if (to.meta.keepAlive) {
      // 确保在下一个tick添加缓存,避免移除后立即添加的问题
      nextTick(() => {
        cacheViews.value.add(to.name)
      })
    }
  }
  return {
    keepAliveComponents,
    handleRouteChange
  }
}

高级配置选项

可以进一步扩展配置选项,提供更灵活的控制:

const routes = [
  {
    path: '/form',
    name: 'Form',
    component: FormPage,
    meta: {
      keepAlive: true,
      resetOnEnter: true,
      exceptFrom: ['select'], // 从哪些页面返回时不重置
      resetWhen: (to, from) => {
        // 自定义重置条件
        return from.query.reset === 'true';
      }
    }
  }
];

导航守卫或hooks中处理自定义条件:参考hooks方案