vue3 keep-alive 页面缓存最佳实践方案

2,051 阅读3分钟

使用场景

在做项目时遇到需要缓存页面,比如 A页面 -> B列表页面 —> C详情页面

这时 C详情页面返回到B列表页面时需要缓存,B列表页面到A页面时不需要缓存。

踩过的坑

问题: 动态设置 路由的 meta 属性 keepAlive 时,发现总是第一次不生效,需要进行第二次跳转才能生效

原因: 大概率是你采用了以下的设置方法

// app.vue
<template>
    <router-view v-slot="{ Component }">
      <KeepAlive>
        <component :is="Component" :key="$route.fullPath" v-if="$route.meta.keepAlive" />
      </KeepAlive>
       <component :is="Component" :key="$route.fullPath" v-if="!$route.meta.keepAlive" />
    </router-view>
</template>

因为使用v-if $route.meta.keepAlive 来判断是否缓存,当第一次进入页面时其实是并没有设置成功的。

最好看下 vue 的文档,有章节专门介绍 includeexclude 。 利用这俩个参数就可以完美实现动态设置缓存页面

实践方案:

  1. 设置 KeepAlive 路由
// app.vue
<template>
    <router-view v-slot="{ Component }">
      <KeepAlive :include="includePages">
        <component :is="Component" :key="$route.fullPath" />
      </KeepAlive>
    </router-view>
</template>
<script lang="ts" setup>
/**
 * @description: 设置缓存页面逻辑
 */
const keepPageStore = useKeepPageStoreWithOut()
const { includePages } = storeToRefs(keepPageStore)
</script>
  1. 使用 状态管理工具 来进行 动态设置 include
// store
import { defineStore } from 'pinia'
import { store } from '../index'export const useKeepPageStore = defineStore({
  id: 'KeepPage',
  state: () => ({
    includePages: [] as string[],
    scrollTop: 0
  }),
  actions: {
    addIncludePage(pageName: string) {
      if (!this.includePages.includes(pageName)) {
        this.includePages.push(pageName)
      }
      // 记住当前的滚动距离
      this.scrollTop = window.scrollY
    },
    removeIncludePage(pageName: string) {
      this.includePages = this.includePages.filter((item) => item !== pageName)
      this.scrollTop = 0
    }
  }
})
​
export function useKeepPageStoreWithOut() {
  return useKeepPageStore(store)
}
  1. 写一个通用的 hooks 用来更加方便的复用代码逻辑
// hooks
import { onBeforeRouteLeave } from 'vue-router'
import { useKeepPageStoreWithOut } from '~/store/modules/keepPage'
const keepPageStore = useKeepPageStoreWithOut()
// 这里把targetPages参数设置为数组,因为目标页面可能有多个,比如B -> C, B -> D
export function useKeepPage(targetPages: string[] | 'all') {
  /**
   * @description: 设置页面缓存
   * @param {toName} string 目标页面name
   * @param {formName} string 来源页面name
   * @return {*}
   */
  const setKeepPage = (toName: string, formName: string) => {
    // 判断是否进入目标页面
    if (targetPages === 'all') {
      // 如果为 all 代表 则目标页面跳到到 任何页面再返回过来时都需要被缓存
      return keepPageStore.addIncludePage(formName)
    }
    if (!targetPages.includes(toName)) {
      // 当前页面不需要缓存目标页面,则就移除在 include 中移除目标页面
      keepPageStore.removeIncludePage(formName)
    } else {
      // 当前面需要缓存目标页面,则就在 include 中添加目标页面
      keepPageStore.addIncludePage(formName)
    }
  }

  // 在目标页面离开时触发
  onBeforeRouteLeave((to, form) => {
    setKeepPage(to.name as string, form.name as string)
  })
}
  1. 最后在需要缓存的页面里 用 useKeepPage 这个hook函数即可
// 需要缓存的页面使用
keepPageStore(['C详情页面的路由名'])   // 比如用在B列表页面,跳到C详情页面再返回过来 此时B列表页面就是缓存的了, 如果从其他页面返回过来的,B列表页面就不是缓存的
  1. 返回目标页面时 滚动到上一次离开时的位置
// router.ts
import { createRouter, createWebHistory, RouterScrollBehavior } from 'vue-router'
import routes from 'virtual:generated-pages'
const keepPageStore = useKeepPageStoreWithOut()
const scrollBehavior: RouterScrollBehavior = () => {
  // 异步滚动操作
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ left: 0, top: keepPageStore.scrollTop })
    }, 0)
  })
}
export const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes,
  scrollBehavior
})

待优化地方

看上述方案,其实都可以把所有逻辑都写在 一个hook 函数里,不用多此一举再写一个状态管理去保存 include。 (说明下,我这样写是为了在测试时 可以更加方便的用 Vue Devtools 工具查看)

结尾

如果有比较细心的小伙伴会发现,在第五步时,我并没有用到 scrollBehavior 自带的第三个参数 savePosition, 而是用的自己实现记住位置的变量(scrollTop)。

其实在测试过程中发现,用该函数自带的 savePosition参数 在一些复杂场景下并不准确。

比如: B列表页面 -> C详情页面(假设此时的B列表页面的滚动距离为 1000)-> B列表页面(滚动距离:1000) -> A页面 -> B列表页面 -> C详情页面(假设此时的B列表页面的滚动距离为 2000) -> B列表页面 (此时的滚动距离应该为 2000, 但实际 savePosition 存的top值为 1000)