theme: fancy

超细tab标签页缓存应该具备

  • 简略配置就能缓存页面
  • 反对标签页刷新
  • 一键敞开其余标签页
  • 地址跳转主动关上或切换对应标签页
  • 放弃滚动地位
  • 反对多级缓存
  • 反对手动删除标签页缓存
  • 蕴含 Vue2 和 Vue3 两种实现计划

快捷入口

vue2 demo: https://xiaocheng555.github.i... (PC关上食用更佳)

vue3 demo: https://xiaocheng555.github.i... (PC关上食用更佳)

代码: https://github.com/xiaocheng5...

效果图:

Vue2 实现计划

实现缓存

毫无疑问,缓存用的是 <keep-alive> 组件,用 <keep-alive> 包裹路由组件,从而缓存tab标签页。

<keep-alive> 组件有个 include 属性,在 include 数组里增加或者移除组件名可让tab标签页增加或删除缓存;为了对立治理,把缓存的操作写到vuex里。

缓存标签页:

<keep-alive ref="alive" :include="caches">  <router-view></router-view></keep-alive>

操作 caches 缓存:

// src/store/cache.jsimport Vue from 'vue'export default {  namespaced: true,  state: {    caches: []  },  actions: {    // 增加缓存的路由组件    addCache ({ state, dispatch }, componentName) {      const { caches } = state      if (!componentName || caches.includes(componentName)) return      caches.push(componentName)    },    // 移除缓存的路由组件    removeCache ({ state, dispatch }, componentName) {      const { caches } = state      const index = caches.indexOf(componentName)      if (index > -1) {        return caches.splice(index, 1)[0]      }    },    // 移除缓存的路由组件的实例    async removeCacheEntry ({ dispatch }, componentName) {      const cacheRemoved = await dispatch('removeCache', componentName)      if (cacheRemoved) {        await Vue.nextTick()        dispatch('addCache', componentName)      }    }  }}

2、缓存做成可配置

如果手动增加缓存的路由组件到 caches 里,会很繁琐且容易出错;广泛做法是,在路由元信息把须要缓存的路由设置为 keepAlive: true,如:

{  path: '/article',  component: () => import('./views/ArticleList.vue'),  name: 'article-list',  meta: {    keepAlive: true,    title: '文章列表'  }}

而后监听路由变动,在 $route.matched 路由记录数组拿到路由的组件实例,组件实例中就有组件的名称,再将组件的名称存到 caches 里,即可实现组件缓存。整顿为一句话就是:收集缓存。

// src/App.vuemethods: {  ...mapActions('cache', [    'addCache',    'removeCache'  ]),  // 收集缓存(通过监听)  collectCaches () {    // 收集以后路由相干的缓存    this.$route.matched.forEach(routeMatch => {      const componentName = routeMatch.components?.default?.name            // 配置了meta.keepAlive的路由组件增加到缓存      if (routeMatch.meta.keepAlive) {        this.addCache(componentName)      } else {        this.removeCache(componentName)      }    })  }  },  watch: {    '$route.path': {      immediate: true,      handler () {        this.collectCaches()      }    }  }

实现tab标签页

新增、切换标签页

tab标签与路由是一一对应的,一个路由对应一个tab标签,所以将tab标签的key值与路由记录的门路做映射(此处的门路path与路由配置的path是一样的,如路由配置了 /detail/:id,路由记录的门路就是 /detail/:id, 而不会是实在门路 /detail/10)。

之后,通过监听路由,获取以后路由记录的门路作为key值,通过key值判断tab标签页是否存在,存在则切换到该tab标签页,不存在则创立新的tab标签页。其中tab标签页的题目是配置在路由 meta.title 上,同时记录以后路由 path、query、params、hash,后续切换tab时依据这些参数做跳转,还有componentName是用来记录或革除路由缓存的。

<template>  <div class="layout-tabs">    <el-tabs      type="border-card"      v-model="curTabKey"      closable      @tab-click="clickTab"      @tab-remove="removeTab">      <el-tab-pane        v-for="item in tabs"        :label="item.title"        :name="item.tabKey"        :key="item.tabKey">        <template slot="label">{{item.title}}</template>      </el-tab-pane>    </el-tabs>  </div></template>export default {  props: {    // 【依据我的项目批改】tab页面在路由的第几层,或者说第几层的 router-view 组件(以后我的项目为第二层)    tabRouteViewDepth: {      type: Number,      default: 2     },    // tab页面的key值,从route对象中取,一个key值对应一个tab页面    // 默认为matchRoute.path值    getTabKey: {      type: Function,      default: function (routeMatch/* , route */) {        return routeMatch.path      }    },    // tab页签的题目,默认从路由meta.title中获取    tabTitleKey: {      type: String,      default: 'title'    }  },  data () {    return {      tabs: [],      curTabKey: ''    }  },  methods: {    // 切换tab    changeCurTab () {      // 以后路由信息      const { path, query, params, hash, matched } = this.$route      // tab标签页路由信息:meta、componentName      const routeMatch = matched[this.tabRouteViewDepth - 1]      const meta = routeMatch.meta      const componentName = routeMatch.components?.default?.name      // 获取tab标签页信息:tabKey标签页key值;title-标签页题目;tab-存在的标签页      const tabKey = this.getTabKey(routeMatch, this.$route)      const title = String(meta[this.tabTitleKey] || '')      const tab = this.tabs.find(tab => tab.tabKey === tabKey)            if (!tabKey) { // tabKey默认为路由的name值        console.warn(`LayoutTabs组件:${path} 路由没有匹配的tab标签页,如有须要请配置tab标签页的key值`)        return       }            // 如果同一tab门路变了(例如门路为 /detail/:id),则革除缓存实例      if (tab && tab.path !== path) {        this.removeCacheEntry(componentName || '')        tab.title = ''      }            const newTab = {        tabKey,        title: tab?.title || title,        path,        params,        query,        hash,        componentName       }      tab ? Object.assign(tab, newTab) : this.tabs.push(newTab)      this.curTabKey = tabKey    }  },  watch: {    '$route.path': {      handler () {        this.changeCurTab()      },      immediate: true    }  }}

敞开标签页,革除缓存

敞开标签页时,如果是最初一个tab标签页,则不能删除;如果删除的是其余标签页,则敞开该标签页;如果删除的是以后标签页,则敞开以后标签页并切换到最初一个标签页;最初,革除敞开后的标签页缓存。

// 移除tabasync removeTab (tabKey) {  // 剩下一个时不能删  if (this.tabs.length === 1) return    const index = this.tabs.findIndex(tab => tab.tabKey === tabKey)  if (index < -1) return     const tab = this.tabs[index]  this.tabs.splice(index, 1)    // 如果删除的是以后tab,则切换到最初一个tab  if (tab.tabKey === this.curTabKey) {    const lastTab = this.tabs[this.tabs.length - 1]    lastTab && this.gotoTab(lastTab)  }  this.removeCache(tab.componentName || '')}

标签页刷新

我所晓得的组件刷新办法有两种:

(1)key:先给组件绑定key值,通过扭转key就能刷新该组件

(2)v-if:先后设置v-if的值为false和true 来刷新组件,如下

<test-component v-if="isRender"></test-component>this.isRender = falsethis.$nextTick(() => {  this.isRender = true})

通过实际发现,key刷新会有问题。当key绑定 <router-view>(如下),扭转key值尽管能刷新以后页面,然而原来的缓存仍然在,也就是说一个key对应一个缓存,如果key始终在扭转,就会造成缓存越堆越多。

<keep-alive>  <router-view :key="key" /></keep-alive>

那么,只能应用v-if的计划,先来波剖析:

如果非缓存的组件,应用v-if计划是能够失常刷新,然而我发现对于缓存的组件是有效的。因为 v-if=false 时,组件并没有销毁,而是缓存起来了,这就令我很头疼。不过,我还是想到了解决办法:组件 v-if=false 时,我将组件缓存革除掉,而后再设置 v-if=true,那么组件是不是就会从新渲染了?通过实际,这个方法是可行的。写下伪代码:

<button @click="refreshTab">刷新</button><keep-alive :include="caches">  <router-view v-if="isRenderTab"></router-view></keep-alive>export default {  methods: {    // 刷新以后tab页面    async refreshPage () {      this.isRenderTab = false      const index = this.caches.indexOf('以后组件名称')      if (index > -1) {        // 革除缓存        this.caches.splice(index, 1)      }      this.$nextTick(() => {        this.caches.push('以后组件名称') // 从新增加缓存        this.isRenderTab = true      })    }  }}

残缺代码

多级缓存

Demo中tab标签页处于一级缓存,在它上面也能够做二级缓存。写法跟失常的 keep-alive 缓存写法一样(如下代码),二级缓存复用 cachesuseRouteCache 中对缓存的操作;配置缓存同样是在路由里设置meta的 keepAlive: true

<router-view v-slot="{ Component }">  <keep-alive :include="caches">    <component :is="Component" />  </keep-alive></router-view>import useRouteCache from '@/hooks/useRouteCache'const { caches } = useRouteCache()

非凡场景

有一个详情页 /detail/:id,我心愿每次关上详情页都是一个独立的标签页。举个例子,关上 /detail/1 对应一个标签页,关上 /detail/2 对应另一个标签页,Demo中是不反对的,具体能够这样实现:tab标签页的key值设置为路由的实在门路,那么每个详情页都有一个tab标签页了,为了让每个详情页的缓存都不一样,给标签页路由加上key值为 '$route.path'。然而会有一个问题,应用 removeCache 革除详情页缓存时,会将所有详情页的缓存都革除。

  <layout-tabs :getTabKey="(routeMatch , route) => route.path"></layout-tabs>  <keep-alive :include="caches">    <router-view :key="$route.path">    </router-view>  </keep-alive>

放弃缓存页滚动地位

剖析一下需要:当来到页面时,记录当前页的滚动地位;下次再进入该页面,拿到之前记录的值并复原滚动的地位。这里波及两个事件:来到页面(beforeRouteLeave)、进入页面(activated)

// src/mixins/keepScroll.jsconst setting = {  scroller: 'html'}let gobal = false// 获取全副选项function getOptions ($options) {  return {    ...setting,    ...$options.keepScroll  }}// 配置设置export function configSetting (data) {  Object.assign(setting, data)}const keepScroll = {  methods: {    // 复原滚动地位    restoreKeepScrollPos () {      if (gobal && !this.$options.keepScroll) return      if (!this.__pos) this.__pos = [0, 0]      const options = getOptions(this.$options)      const scroller = document.querySelector(options.scroller)      if (!scroller) {        console.warn(`keepScroll mixin: 未找到 ${options.scroller} Dom滚动容器`)        return      }      this.__scroller = scroller      scroller.scrollTop = this.__pos[0]      scroller.scrollLeft = this.__pos[1]    },    // 记录滚动地位    recordKeepScrollPos () {      if (gobal && !this.$options.keepScroll) return      if (!this.__scroller) return      const scroller = this.__scroller      this.__pos = [scroller.scrollTop, scroller.scrollLeft]    },    // 重置滚动地位    resetKeepScrollPos () {      if (gobal && !this.$options.keepScroll) return      if (!this.__scroller) return      const scroller = this.__scroller      scroller.scrollTop = 0      scroller.scrollLeft = 0    }  },  activated () {    this.restoreKeepScrollPos()  },  deactivated () {    this.resetKeepScrollPos()  },  beforeRouteLeave (to, from, next) {    this.recordKeepScrollPos()    next()  }}// 全局调用 Vue.use(keepScroll, setting)function install (Vue, data = {}) {  gobal = true  Object.assign(setting, data)  Vue.mixin(keepScroll)}// 反对全局或部分引入keepScroll.install = installexport default keepScroll

实现代码有点长,次要是为了反对全局引入和部分引入。

全局援用

import keepScrollMixin from './mixins/keepScroll'Vue.use(keepScrollMixin, {  scroller: '滚动的容器' // 默认滚动容器是html})

在组件中配置 keepScroll: true 即可:

export default {  keepScroll: true,  data () {...}}

部分援用

import keepScrollMixin from './mixins/keepScroll'export default {  mixins: [keepScrollMixin],  data () {...}}

如果须要设置滚动容器的,能够部分批改:

export default {  mixins: [keepScrollMixin],  keepScroll: {    scroller: '滚动容器'  }}

或者全局批改:

import { configKeepScroll } from './mixins/keepScroll'configKeepScroll({  scroller: '滚动容器'})

Vue3 实现计划

Vue3 和 Vue2 的实现计划大体上差不多,上面会简略介绍一下,想具体理解能够看源码。

实现缓存

将缓存的操作写在一个hook里,不便调用。

// src/hooks/useRouteCache.tsimport { ref, nextTick, watch } from 'vue'import { useRoute } from 'vue-router'const caches = ref<string[]>([])let collect = falselet cmpNames: { [index: string]: string } = {}export default function useRouteCache () {  const route = useRoute()    // 收集以后路由相干的缓存  function collectRouteCaches () {    route.matched.forEach(routeMatch => {      const componentDef: any = routeMatch.components?.default      const componentName = componentDef?.name || componentDef?.__name            // 配置了meta.keepAlive的路由组件增加到缓存      if (routeMatch.meta.keepAlive) {        if (!componentName) {          console.warn(`${routeMatch.path} 路由的组件名称name为空`)          return        }        addCache(componentName)      } else {        removeCache(componentName)      }    })  }    // 收集缓存(通过监听)  function collectCaches () {    if (collect) {      console.warn('useRouteCache:不须要反复收集缓存')      return    }    collect = true    watch(() => route.path, collectRouteCaches, {      immediate: true    })  }    // 增加缓存的路由组件  function addCache (componentName: string | string[]) {    if (Array.isArray(componentName)) {      componentName.forEach(addCache)      return    }    if (!componentName || caches.value.includes(componentName)) return    caches.value.push(componentName)    console.log('缓存路由组件:', componentName)  }  // 移除缓存的路由组件  function removeCache (componentName: string | string[]) {    if (Array.isArray(componentName)) {      componentName.forEach(removeCache)      return    }        const index = caches.value.indexOf(componentName)    if (index > -1) {      console.log('革除缓存的路由组件:', componentName)      return caches.value.splice(index, 1)    }  }  // 移除缓存的路由组件的实例  async function removeCacheEntry (componentName: string) {    if (removeCache(componentName)) {      await nextTick()      addCache(componentName)    }  }  // 革除缓存的路由组件的实例  function clearEntry () {    caches.value.slice().forEach(key => {      removeCacheEntry(key)    })  }  return {    collectCaches,    caches,    addCache,    removeCache,    removeCacheEntry  }}

缓存路由:

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

收集缓存

// src/App.vueimport useRouteCache from '@/hooks/useRouteCache'// 收集路由配置meta为keepAlive: ture的缓存const { collectCaches } = useRouteCache()collectCaches()

实现tab标签页

残缺代码

标签页刷新

当我应用 v-if 的刷新计划时,发现报错了,只有在 <keep-alive> 下 <router-view> 中加 v-if 就会报错,网上一查发现是vue3的bug,issue上有相似问题:

这样的话 v-if 就不能用了,那有没有办法实现类型的成果呢?还真有:标签页点刷新时,先跳转到一个空白页,而后革除标签页的缓存,而后再跳转回来,就能达到一个刷新成果。

先配置空白路由:

{  // 空白页,刷新tab页时用来做直达  path: '/_empty',  name: '_empty',  component: Empty}

标签页刷新:

// 刷新tab页面async function refreshTab (tab: Tab) {  await router.push('/_empty')  removeCache(tab.componentName || '')  router.go(-1)}

放弃缓存页滚动地位

来到页面,记录滚动地位,再次进入页面,复原滚动地位。逻辑写为hook:

// src/hooks/useKeepScrollimport { onActivated } from 'vue'import { onBeforeRouteLeave } from 'vue-router'let gobalScrollBox = 'html' // 全局滚动盒子export function configKeepScroll (scrollBox: string) {  gobalScrollBox = scrollBox}export default function useKeepScroll (scrollBox?: string) {  let pos = [0, 0]  let scroller: HTMLElement | null    onActivated(() => {    scroller = document.querySelector(scrollBox || gobalScrollBox)    if (!scroller) {      console.warn(`useKeepScroll: 未找到 ${scrollBox || gobalScrollBox} Dom滚动容器`)      return    }    scroller.scrollTop = pos[0]    scroller.scrollLeft = pos[1]  })    onBeforeRouteLeave(() => {    if (scroller) {      pos = [scroller.scrollTop, scroller.scrollLeft]    }  })}

页面上应用:

<script setup lang="ts">import useKeepScroll from '@/hooks/useKeepScroll'useKeepScroll()</script>

补充

1、在vue3中应用 <keep-alive> 加上 <router-view> 偶然会热更新报错,应该是 Vue3的bug。

2、Demo中详情页,删除详情页后跳转到列表页

// 跳转列表页if (window.history.state?.back === '/article') {  router.go(-1)} else {  router.replace('/article') }

其中,window.history.state?.back 获取的是返回页的地址,如果上一页的地址是 /article,应用 router.replace('/article') 跳转会产生两条 /article 的历史记录,体验不敌对,所以改为 router.go(-1)

结尾

以上是我的一些不成熟想法,有谬误或表述不清欢送交换与斧正。

再次附上地址:

vue2 demo: https://xiaocheng555.github.i... (PC关上食用更佳)

vue3 demo: https://xiaocheng555.github.i... (PC关上食用更佳)

代码: https://github.com/xiaocheng5...