Vue3除了keep-alive还有哪些实现页面缓存详解

 更新时间:2024年04月27日 08:33:42   作者:We-5211314  
Vue3中的keep-alive组件用于缓存页面,以便在切换页面时保留其状态,下面这篇文章主要给大家介绍了关于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。最多可以缓存多少组件实例。

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

import { ref, nextTick } from 'vue'

const caches = ref<string[]>([])

export default function useRouteCache () {
  // 添加缓存的路由组件
  function addCache (componentName: string | string []) {
    if (Array.isArray(componentName)) {
      componentName.forEach(addCache)
      return
    }
    
    if (!componentName || caches.value.includes(componentName)) return

    caches.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
  }
}

hook的用法如下:

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

<script setup lang="ts">
import useRouteCache from './hooks/useRouteCache'
const { caches, addCache } = useRouteCache()

<!-- 将列表页组件名称添加到需要缓存名单中 -->
addCache(['List'])
</script>

清除列表页缓存如下:

import useRouteCache from '@/hooks/useRouteCache'

const { removeCacheEntry } = useRouteCache()
removeCacheEntry('List')

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

列表页清除缓存的时机

进入列表页后清除缓存

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

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

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

点击链接跳转前清除缓存

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

// 首页 Home.vue

<li>
  <router-link to="/list" @click="removeCacheBeforeEnter">列表页</router-link>
</li>


<script setup lang="ts">
import useRouteCache from '@/hooks/useRouteCache'

defineOptions({
  name: 'Home'
})

const { removeCacheEntry } = useRouteCache()

// 进入页面前,先清除缓存实例
function removeCacheBeforeEnter () {
  removeCacheEntry('List')
}
</script>

状态管理实现缓存

通过状态管理库存储页面的状态和数据也能实现页面缓存。此处状态管理使用的是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

然后在列表页中使用store:

<div>
  <el-page-header @back="goBack">
    <template #content>状态管理实现列表页缓存</template>
  </el-page-header>
  <el-table v-loading="loading" :data="tableData" border style="width: 100%; margin-top: 30px;">
    <el-table-column prop="id" label="id" />
    <el-table-column prop="content" label="内容"/>
    <el-table-column label="操作">
      <template v-slot="{ row }">
        <el-link type="primary" @click="gotoDetail(row)">进入详情</el-link>
        <el-tag type="success" v-if="row.id === listStore.curRow?.id">刚点击</el-tag>
      </template>
    </el-table-column>
  </el-table>
  <el-pagination
    v-model:currentPage="listStore.currentPage"
    :page-size="listStore.pageSize"
    layout="total, prev, pager, next"
    :total="listStore.list.length"
  />
</div>
  
<script setup lang="ts">
import useListStore from '@/store/listStore'
const listStore = useListStore()

...
</script>

通过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 = true
    setTimeout(() => {
      listStore.setList(getData())
      loading.value = false
    }, 1000)
    listStore.useCache = true
  }
})

缺点

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

页面弹窗实现缓存

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

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

弹窗组件实现:

// PopupPage.vue

<template>
  <div class="popup-page" :class="[!dialogVisible && 'hidden']">
    <slot v-if="dialogVisible"></slot>
  </div>
</template>

<script setup lang="ts">
import { useLockscreen } from 'element-plus'
import { computed, defineProps, defineEmits } from 'vue'
import useHistoryPopup from './useHistoryPopup'

const props = defineProps({
  modelValue: {
    type: Boolean,
    default: false
  },
  // 路由记录
  history: {
    type: Object
  },
  // 配置了history后,初次渲染时,如果有url上有history参数,则自动打开弹窗
  auto: {
    type: Boolean,
    default: true
  },
  size: {
    type: String,
    default: '50%'
  },
  full: {
    type: Boolean,
    default: false
  }
})
const emit = defineEmits(
  ['update:modelValue', 'autoOpen', 'autoClose']
)

const dialogVisible = computed<boolean>({ // 控制弹窗显示
  get () {
    return props.modelValue
  },
  set (val) {
    emit('update:modelValue', val)
  }
})

useLockscreen(dialogVisible)

useHistoryPopup({
  history: computed(() => props.history),
  auto: props.auto,
  dialogVisible: dialogVisible,
  onAutoOpen: () => emit('autoOpen'),
  onAutoClose: () => emit('autoClose')
})
</script>

<style lang='less'>
.popup-page {
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  z-index: 100;
  overflow: auto;
  padding: 10px;
  background: #fff;
  
  &.hidden {
    display: none;
  }
}
</style>

弹窗组件调用:

<popup-page 
  v-model="visible" 
  full
  :history="{ id: id }">
  <Detail></Detail>
</popup-page>

缺点

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

父子路由实现缓存

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

声明父子路由:

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

列表页代码:

// 列表页
<template>
  <el-table v-loading="loading" :data="tableData" border style="width: 100%; margin-top: 30px;">
    <el-table-column prop="id" label="id" />
    <el-table-column prop="content" label="内容"/>
    <el-table-column label="操作">
      <template v-slot="{ row }">
        <el-link type="primary" @click="gotoDetail(row)">进入详情</el-link>
        <el-tag type="success" v-if="row.id === curRow?.id">刚点击</el-tag>
      </template>
    </el-table-column>
  </el-table>
  <el-pagination
    v-model:currentPage="currentPage"
    :page-size="pageSize"
    layout="total, prev, pager, next"
    :total="list.length"
  />
  
  <!-- 详情页 -->
  <router-view class="popyp-page"></router-view>
</template>

<style lang='less' scoped>
.popyp-page {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 100;
  background: #fff;
  overflow: auto;
}
</style>

总结 

到此这篇关于Vue3除了keep-alive还有哪些实现页面缓存的文章就介绍到这了,更多相关Vue3页面缓存内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 深入探讨Vue 3中的组合式函数编程方式

    深入探讨Vue 3中的组合式函数编程方式

    Vue 3中引入了组合式函数编程方式,可以更好地实现代码的复用和可维护性。通过定义可组合的函数,可以将组件的逻辑和状态进行拆分和组合,实现更灵活的代码组织方式。同时,组合式函数也支持响应式数据和生命周期钩子函数,更加贴近Vue开发的实际场景
    2023-05-05
  • vue3 ref 和reactive的区别详解

    vue3 ref 和reactive的区别详解

    本文主要介绍了vue3 ref 和reactive的区别详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-03-03
  • Vue打包后出现一些map文件的解决方法

    Vue打包后出现一些map文件的解决方法

    本篇文章主要介绍了Vue打包后出现一些map文件的解决方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-02-02
  • Vue实现push数组并删除的例子

    Vue实现push数组并删除的例子

    今天小编就为大家分享一篇Vue实现push数组并删除的例子,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2019-11-11
  • Vue SPA首屏加载缓慢问题解决方案

    Vue SPA首屏加载缓慢问题解决方案

    这篇文章主要介绍了Vue SPA首屏加载缓慢问题解决方案,首屏时间(First Contentful Paint),指的是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容
    2023-03-03
  • vue项目nginx二级域名配置方式

    vue项目nginx二级域名配置方式

    这篇文章主要介绍了vue项目nginx二级域名配置方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-06-06
  • Vue常见报错整理大全(从此报错不害怕)

    Vue常见报错整理大全(从此报错不害怕)

    写代码的过程中一定会遇到报错,遇到报错不要担心,认真分析就可以解决报错,同时积累经验,早日成为大牛,这篇文章主要给大家介绍了关于Vue常见报错整理的相关资料,需要的朋友可以参考下
    2022-08-08
  • vue组件component的注册与使用详解

    vue组件component的注册与使用详解

    组件是Vue是一个可以重复使用的Vue实例, 它拥有独一无二的组件名称,它可以扩展HTML元素,以组件名称的方式作为自定义的HTML标签,这篇文章主要介绍了vue组件component的注册与使用,需要的朋友可以参考下
    2022-08-08
  • 详解vue-router 动态路由下子页面多页共活的解决方案

    详解vue-router 动态路由下子页面多页共活的解决方案

    这篇文章主要介绍了vue-router 动态路由下子页面多页共活的解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-12-12
  • 解决vue页面刷新vuex中state数据丢失的问题

    解决vue页面刷新vuex中state数据丢失的问题

    这篇文章介绍了解决vue页面刷新vuex中state数据丢失的问题,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-01-01

最新评论