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.js
import 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.vue
methods: {
...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 标签页,则不能删除;如果删除的是其余标签页,则敞开该标签页;如果删除的是以后标签页,则敞开以后标签页并切换到最初一个标签页;最初,革除敞开后的标签页缓存。
// 移除 tab
async 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 = false
this.$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.js
const 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 = install
export 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.ts
import {ref, nextTick, watch} from 'vue'
import {useRoute} from 'vue-router'
const caches = ref<string[]>([])
let collect = false
let 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.vue
import 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/useKeepScroll
import {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…