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
缓存写法一样(如下代码),二级缓存复用 caches
和 useRouteCache
中对缓存的操作;配置缓存同样是在路由里设置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...