• Vue3 除了keep-alive,还有哪些页面缓存的实现方案


    引言

    有这么一个需求:列表页进入详情页后,切换回列表页,需要对列表页进行缓存,如果从首页进入列表页,就要重新加载列表页。

    对于这个需求,我的第一个想法就是使用keep-alive来缓存列表页,列表和详情页切换时,列表页会被缓存;从首页进入列表页时,就重置列表页数据并重新获取新数据来达到列表页重新加载的效果。

    但是,这个方案有个很不好的地方就是:如果列表页足够复杂,有下拉刷新、下拉加载、有弹窗、有轮播等,在清除缓存时,就需要重置很多数据和状态,而且还可能要手动去销毁和重新加载某些组件,这样做既增加了复杂度,也容易出bug。

    接下来说说我的想到的新实现方案(代码基于Vue3)。

    keep-alive 缓存和清除

    keep-alive 缓存原理:进入页面时,页面组件渲染完成,keep-alive 会缓存页面组件的实例;离开页面后,组件实例由于已经缓存就不会进行销毁;当再次进入页面时,就会将缓存的组件实例拿出来渲染,因为组件实例保存着原来页面的数据和Dom的状态,那么直接渲染组件实例就能得到原来的页面。

    keep-alive 最大的难题就是缓存的清理,如果能有简单的缓存清理方法,那么keep-alive 组件用起来就很爽。

    但是,keep-alive 组件没有提供清除缓存的API,那有没有其他清除缓存的办法呢?答案是有的。我们先看看 keep-alive 组件的props:

    include - string | RegExp | Array。只有名称匹配的组件会被缓存。
    exclude - string | RegExp | Array。任何名称匹配的组件都不会被缓存。
    max - number | string。最多可以缓存多少组件实例。 
    
    • 1
    • 2
    • 3

    从include描述来看,我发现include是可以用来清除缓存,做法是:将组件名称添加到include里,组件会被缓存;移除组件名称,组件缓存会被清除。根据这个原理,用hook简单封装一下代码:

    import { ref, nextTick } from 'vue'
    
    const caches = ref([])
    
    export default function useRouteCache () {// 添加缓存的路由组件function addCache (componentName: string | string []) {if (Array.isArray(componentName)) {componentName.forEach(addCache)return}if (!componentName || caches.value.includes(componentName)) returncaches.value.push(componentName)}// 移除缓存的路由组件function removeCache (componentName: string) {const index = caches.value.indexOf(componentName)if (index > -1) {return caches.value.splice(index, 1)}}// 移除缓存的路由组件的实例async function removeCacheEntry (componentName: string) {if (removeCache(componentName)) {await nextTick()addCache(componentName)}}return {caches,addCache,removeCache,removeCacheEntry}
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    hook的用法如下:

    
    
    
     
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    清除列表页缓存如下:

    import useRouteCache from '@/hooks/useRouteCache'
    
    const { removeCacheEntry } = useRouteCache()
    removeCacheEntry('List') 
    
    • 1
    • 2
    • 3
    • 4

    此处removeCacheEntry方法清除的是列表组件的实例,‘List’ 值仍然在 组件的include里,下次重新进入列表页会重新加载列表组件,并且之后会继续列表组件进行缓存。

    列表页清除缓存的时机

    进入列表页后清除缓存

    在列表页路由组件的beforeRouteEnter勾子中判断是否是从其他页面(Home)进入的,是则清除缓存,不是则使用缓存。

    defineOptions({name: 'List1',beforeRouteEnter (to: RouteRecordNormalized, from: RouteRecordNormalized) {if (from.name === 'Home') {const { removeCacheEntry } = useRouteCache()removeCacheEntry('List1')}}
    }) 
    
    • 1
    • 2

    这种缓存方式有个不太友好的地方:当从首页进入列表页,列表页和详情页来回切换,列表页是缓存的;但是在首页和列表页间用浏览器的前进后退来切换时,我们更多的是希望列表页能保留缓存,就像在多页面中浏览器前进后退会缓存原页面一样的效果。但实际上,列表页重新刷新了,这就需要使用另一种解决办法,点击链接时清除缓存清除缓存

    点击链接跳转前清除缓存

    在首页点击跳转列表页前,在点击事件的时候去清除列表页缓存,这样的话在首页和列表页用浏览器的前进后退来回切换,列表页都是缓存状态,只要当重新点击跳转链接的时候,才重新加载列表页,满足预期。

    // 首页 Home.vue
    
    
  • 列表页
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    状态管理实现缓存

    通过状态管理库存储页面的状态和数据也能实现页面缓存。此处状态管理使用的是pinia。

    首先使用pinia创建列表页store:

    import { defineStore } from 'pinia'
    
    interface Item {id?: number,content?: string
    }
    
    const useListStore = defineStore('list', {// 推荐使用 完整类型推断的箭头函数state: () => {return {isRefresh: true,pageSize: 30,currentPage: 1,list: [] as Item[],curRow: null as Item | null}},actions: {setList (data: Item []) {this.list = data},setCurRow (data: Item) {this.curRow = data},setIsRefresh (data: boolean) {this.isRefresh = data}}
    })
    
    export default useListStore 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    然后在列表页中使用store:

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    通过beforeRouteEnter钩子判断是否从首页进来,是则通过 listStore.$reset() 来重置数据,否则使用缓存的数据状态;之后根据 listStore.isRefresh 标示判断是否重新获取列表数据。

    defineOptions({beforeRouteEnter (to: RouteLocationNormalized, from: RouteLocationNormalized) {if (from.name === 'Home') {const listStore = useListStore()listStore.$reset()}}
    })
    
    onBeforeMount(() => {if (!listStore.useCache) {loading.value = truesetTimeout(() => {listStore.setList(getData())loading.value = false}, 1000)listStore.useCache = true}
    }) 
    
    • 1
    • 2
    • 3
    • 4
    • 5

    缺点

    通过状态管理去做缓存的话,需要将状态数据都存在stroe里,状态多起来的话,会有点繁琐,而且状态写在store里肯定没有写在列表组件里来的直观;状态管理由于只做列表页数据的缓存,对于一些非受控组件来说,组件内部状态改变是缓存不了的,这就导致页面渲染后跟原来有差别,需要额外代码操作。

    页面弹窗实现缓存

    将详情页做成全屏弹窗,那么从列表页进入详情页,就只是简单地打开详情页弹窗,将列表页覆盖,从而达到列表页 “缓存”的效果,而非真正的缓存。

    这里还有一个问题,打开详情页之后,如果点后退,会返回到首页,实际上我们希望是返回列表页,这就需要给详情弹窗加个历史记录,如列表页地址为 ‘/list’,打开详情页变为 ‘/list?id=1’。

    弹窗组件实现:

    // PopupPage.vue
    
    
    
    
    
     
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    弹窗组件调用:

    
     
    
    • 1
    • 2

    hook:useHistoryPopup 参考文章:juejin.cn/post/713994…

    缺点

    弹窗实现页面缓存,局限比较大,只能在列表页和详情页中才有效,离开列表页之后,缓存就会失效,比较合适一些简单缓存的场景。

    父子路由实现缓存

    该方案原理其实就是页面弹窗,列表页为父路由,详情页为子路由,从列表页跳转到详情页时,显示详情页字路由,且详情页全屏显示,覆盖住列表页。

    声明父子路由:

    {path: '/list',name: 'list',component: () => import('./views/List.vue'),children: [{path: '/detail',name: 'detail',component: () => import('./views/Detail.vue'),}]
    } 
    
    • 1
    • 2

    列表页代码:

    // 列表页
    
    
     
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    最后

    最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。



    有需要的小伙伴,可以点击下方卡片领取,无偿分享

  • 相关阅读:
    计算机网络-计算机网络体系结构-传输层
    论文导读 | 并发数据结构与并发控制
    linux(三) -- 系统管理
    二十一、CANdelaStudio深入-SPRMIB的配置
    mapstruct实体转换 转换成不同类型
    RPC 基础系列
    【vue】在vue项目中按顺序动态24个英文字母选项:A B C D E F......
    如何为 Kubernetes 监控安装 Pixie
    ES6 Symbol
    云原生之使用Docker部署Affine知识库工具
  • 原文地址:https://blog.csdn.net/web2022050903/article/details/127579255