【Vue3实战】路由按需keep-alive方案探索

725 阅读2分钟

在系统开发中,我们会通过路由划分模块。在路由来回切换时候,通常需要我们保存该页的状态。这一需求在具有表单填写、Tab切换、表格分页功能的页面上,尤为突出。

如何在系统中,随心所欲地控制缓存,是开发者必然要面对的问题。

改造路由

以一个传统的后台管理模板为例

image-20230505144622062.png

通常中间的 RouterView 通常是有缓存需求的。

因此根据官方的用例,对Layout 中的 RouterView 中使用 KeepAlive


<RouterView  v-slot="{ Component }">
    <KeepAlive>
      <component :is="Component"></component>
    </KeepAlive>
</RouterView>

经过改造,来回切换路由时,发现我们已经可以正常缓存路由了。

KeepAlive

接下来,需要直面的问题是,并不是所有路由都需要缓存。

查阅官网 KeepAlive , 可知 include/exclude 可以帮助我们解决这一问题。

include

如果我们采用 include ,优势是可以清楚地知晓我们已缓存了哪些组件。

那么不需要缓存某个组件,只需要将他从数组中剔除即可。

根据官网的描述include 需要提供的是实际 路由组件name, 并非 routename

路由组件指的是 路由数据中 component 字段 import 的组件

如此问题变成了,如何收集路由组件的 name 并管理。

这是一个具有全局性质的数据,我们很容易想到使用 Pinia 来管理。

通过 Pinia 管理

定义好 添加/删除 方法

export const useKeepAliverStore = defineStore('keepAliver', () => {
  const includeSet = ref(new Set<string>())
  const include = computed(() => Array.from(includeSet.value))

  const addInclude = (name: string) => {
    includeSet.value.add(name)
  }
  const removeInclude = (name: string) => {
    includeSet.value.delete(name)
  }

  return { 
    include,
    addInclude,
    removeInclude,
  }
})
<RouterView  v-slot="{ Component }">
  <KeepAlive :include="keepAliverStore.include">
    <component :is="Component"></component>
  </KeepAlive>
</RouterView>

动态收集组件名称

可以通过监听路由变化,将组件名称动态地收集到 include

  // useKeepAliverStore
  const collectingInclude = () => {
    const route = useRoute()
    return watch(() => route.matched, async (matched) => {
      const item = matched.at(-1)
      if (!item) return
     
      if (item.instances) {
		// item.instances中的值是异步的添加的,所以需要有值后再执行下一步
        await waiting(() => isNotEmptyObject(item.instances))
          
        for (const key in item.instances) {
          const instance = item.instances[key]
          const vm = instance?.$
          if (vm) {
            vm.type.name && addInclude(vm.type.name)
          }
        }
      }

    }, {
      immediate: true,
    })
  } 

根据以上思路改造好的 Layout 大致是这样的:

<script lang="ts" setup>
const keepAliverStore = useKeepAliverStore()
// 监听路由,自动添加include
keepAliverStore.collectingInclude()
</script>
<template>

...

<ElScrollbar ref="scrollbarNode">
  <RouterView  v-slot="{ Component }">
    <KeepAlive :include="keepAliverStore.include">
      <component :is="Component"></component>
    </KeepAlive>
  </RouterView>

</ElScrollbar>

...

</template>


通过路由数据按需缓存

通常情况下,我们希望通过路由元数据来决定是否自动缓存某个路由组件

declare module 'vue-router' {

  interface RouteMeta {
    
    /**
     * 是否缓存
     * 0: 缓存
     * 1: 不缓存
     * 2: 自定义缓存策略(预留)
     */
    noCache?: 0|1|2
      
   	// ...
  }
}

在自动收集的监听中加入判断

  const collectingInclude = () => {
    const route = useRoute()
    return watch(() => route.matched, async (matched) => {
      const item = matched.at(-1)
      if (!item) return
        
      // +++++++++++++++++++++++++++++++++++
      if (item.meta.noCache == 1) return
      
      if (item.instances) {
        // item.instances中的值是异步的添加的,所以需要有值后再执行下一步
        await waiting(() => isNotEmptyObject(item.instances))

        for (const key in item.instances) {
          const instance = item.instances[key]
          const vm = instance?.$

          if (vm) {
            vm.type.name && addInclude(vm.type.name)
          }
        }
      }

    }, {
      immediate: true,
    })
  } 

在路由数据中加入标识

          {
            meta: {
              title: '多列表单',
              noCache: 1,
            },
            path: 'multiple',
            component: ...
          }

观察 include 数组,可验证 noCache: 1 的路由组件将不会被自动收集,即该组件不会被缓存

经过上述改造。实战过程中,面对一般业务,我们只需要关注

  • 给需要缓存的 路由组件 加上 name

    不加 name 的路由组件因为不能被 include 收录,所以不被缓存

  • 在路由数据 meta 中设置 noCache: 1 来确保该路由组件不被缓存

但也有一些边界情况值得我们讨论

  • 如何缓存页面中的滚动条进度
  • 如何在对同一个页面按需缓存

如果感兴趣,我们可以在一章里讨论,也可在评论区留下思路。