实现动态路由缓存:解决不同路由复用同一组件时,vue缓存(keep-alive) 同时删除的问题 (解决缓存同一个组件不同的页面,如何移除某个页面时不影响其他同组件的页面的问题)
本文主要解决缓存同一个组件不同的页面,如何移除某个页面时不影响其他同组件的页面的问题。更改keep-alive源码使自定义key为path,根据页面路由判断缓存。动态路由页面,同时打开多个详情页(例:路由为/detail/:id的两个详情页/detail/1,/detail/2),当关闭/detail/1标签页时,/detail/2的页面缓存也会被清除。
本文主要解决缓存同一个组件不同的页面,如何移除某个页面时不影响其他同组件的页面的问题。
一、 问题背景
在做后台管理系统(本文以若依管理系统为例:若依文档:介绍 | RuoYi;若依源码:gitee:ruoyi 源码_gitee )的时候,有这样一个需求:
由于存在tab栏,当从查询页面点击列表进入详情时,需求是详情页都会新开一个tab,并缓存,tab中的切换不会重新加载详情页数据,但是关闭一个详情tab,再次从查询页点击这条详情数据时,是需要重新加载的。
二、 问题产生与原因分析
由于keepalive使用include或者exclude去匹配组件名(组件的name)进行缓存匹配;tab更新也是根据include或者exclude动态添加取消缓存。(keep-alive 官方文档: 内置组件 | Vue.js)
问题: 动态路由页面,同时打开多个详情页(例:路由为/detail/:id的两个详情页/detail/1, /detail/2),当关闭/detail/1标签页时,/detail/2的页面缓存也会被清除。
不同路由共用同一组件时(如详情页路由fullPath分别是/detail/1, /detail/2组件都是detail.vue),组件是相同的,组件名也是相同的。所以缓存清除时是一块清除的。当删除一个详情页的缓存,其他打开的详情页也会被清除。如果不删除,点击刚才那条列表数据还是取的缓存中的值。
可能有人会想,既然组件会被复用,组件名一致,那直接将include内的组件名换成路由path不就好了吗?但是这样是不行的,会导致本该被缓存的页面没有被缓存到,因此需要重新考虑可行的方案。
三、可行的方案
-
通过动态改变组件名来使相同组件在不同路由下使用不同组件名。
-
更改keep-alive源码使自定义key为
path
,根据页面路由判断缓存。
本文主要通过更改源码,使用自定义keep-alive来达到目的。
四、具体解决办法
1、新建 keepAlive.js 文件:重写 keep-alive 源码,使 include 可以按照路由地址path匹配,而不是按照组件的 name 匹配。
/**
* base-keep-alive 主要解决问题场景:多级路由缓存;缓存同一个组件不同的页面,如何移除某个页面时不影响其他同组件的页面
* 前提:保证动态路由生成的route name 值都声明了且唯一
* 基于以上对keep-alive进行以下改造:
* 1. 组件名称获取更改为路由名称
* 2. cache缓存key也更改为路由名称
* 3. pruneCache
*/
const _toString = Object.prototype.toString
function isRegExp(v) {
return _toString.call(v) === '[object RegExp]'
}
export function remove(arr, item) {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
/**
* 1. 主要更改了 name 值获取的规则
* @param {*} opts
*/
function getComponentName(opts) {
// return opts && (opts.Ctor.options.name || opts.tag)
return this.$route.path
}
function isDef(v) {
return v !== undefined && v !== null
}
function isAsyncPlaceholder(node) {
return node.isComment && node.asyncFactory
}
function getFirstComponentChild(children) {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; i++) {
const c = children[i]
if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
return c
}
}
}
}
function matches(pattern, name) {
if (Array.isArray(pattern)) {
return pattern.indexOf(name) > -1
} else if (typeof pattern === 'string') {
return pattern.split(',').indexOf(name) > -1
} else if (isRegExp(pattern)) {
return pattern.test(name)
}
/* istanbul ignore next */
return false
}
function pruneCache(keepAliveInstance, filter) {
console.log("🚀 ~ file: keepalive.js:58 ~ pruneCache ~ keepAliveInstance, filter:", keepAliveInstance, filter)
const { cache, keys, _vnode } = keepAliveInstance
for (const key in cache) {
const cachedNode = cache[key]
if (cachedNode) {
// ------------ 3. 之前默认从router-view取储存key值, 现在改为路由name, 所以这里得改成当前key
// const name = getComponentName.call(keepAliveInstance, cachedNode.componentOptions)
const name = key
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
function pruneCacheEntry(
cache,
key,
keys,
current
) {
const cached = cache[key]
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
const patternTypes = [String, RegExp, Array]
export default {
name: 'keep-alive-custom', // 名称最好改一下,不要与原keep-alive一致
// abstract: true,
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created() {
this.cache = Object.create(null)
this.keys = []
},
destroyed() {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted() {
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
render() {
const slot = this.$slots.default
const vnode = getFirstComponentChild(slot)
const componentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
const name = getComponentName.call(this, componentOptions)
console.log("🚀 ~ file: keepalive.js:124 ~ render ~ name:", name)
// ---------- 对于没有name值得设置为路由得name, 支持vue-devtool组件名称显示
if (!componentOptions.Ctor.options.name) {
vnode.componentOptions.Ctor.options.name
}
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
// ------------------- 储存的key值, 默认从router-view设置的key中获取
// const key = vnode.key == null
// ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
// : vnode.key
// ------------------- 2. 储存的key值设置为路由中得name值
const key = name
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key)
} else {
cache[key] = vnode
keys.push(key)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}
2、在 main.js 中引入自定义 keep-alive 组件——BaseKeepAlive
// main.js 文件添加以下代码
import BaseKeepAlive from '@/utils/base/KeepAlive'
Vue.component('BaseKeepAlive', BaseKeepAlive)
3、把原 keep-alive 替换为自定义 BaseKeepAlive (若依系统在AppMain.vue文件内)
<BaseKeepAlive :include="cachedViews">
<router-view :key="$route.path" />
</BaseKeepAlive>
(以若依系统为例:)
<router-view :key="$route.path" /> 的key值一般设置为路由路径:
- 若不设置key:
vue会复用相同组件,对于路由有多个子路由来说,当在子路由来回切换时,会导致页面不刷新的问题,这是因为不再执行created和mounted这些钩子函数,可以通过watch来监听$route的变化从而加载不同的组件。 - 设置 key 属性值为 $route.path
/detail/1 => /detail/2 (路由内配置path: 'detail/:id')
由于这两个路由的$route.path不一样, 所以组件被强制不复用, 相关钩子加载顺序为: beforeRouteUpdate => created => mounted
/detail?id=1 => /detail?id=2,
由于这两个路由的$route.path一样, 所以和没设置 key 属性一样, 会复用组件, 相关钩子加载顺序为: beforeRouteUpdate - 设置 key 属性值为 $route.fullPath
/detail/1 => /detail/2
由于这两个路由的$route.fullPath不一样, 所以组件被强制不复用, 相关钩子加载顺序为: beforeRouteUpdate => created => mounted
/detail?id=1 => /detail?id=2
由于这两个路由的$route.fullPath不一样, 所以组件被强制不复用, 相关钩子加载顺序为: beforeRouteUpdate => created => mounted
cachedViews 未改keep-alive源码前就是新打开标签时,若设置了缓存,则将当前组件名(name)加入到include数组里,关闭标签时从数组中删除关闭标签。
换成自定义BaseKeepAlive后,cachedViews 内存的是 路由的path。(依次将name换成path,不止换 ADD_CACHED_VIEW 内的。)
4、 cachedViews 存入 vuex
具体做法可看若依系统:
步骤一: 在 src\store\modulse 文件夹下建立 tagsView.js (有删减,这里只展示 cachedViews 相关内容)
const state = {
// visitedViews: [],
cachedViews: [],
// iframeViews: []
}
const mutations = {
ADD_CACHED_VIEW: (state, view) => {
// if (state.cachedViews.includes(view.name)) return
if (state.cachedViews.includes(view.path)) return
if (view.meta && !view.meta.noCache) {
// state.cachedViews.push(view.name)
state.cachedViews.push(view.path)
}
},
DEL_CACHED_VIEW: (state, view) => {
// const index = state.cachedViews.indexOf(view.name)
const index = state.cachedViews.indexOf(view.path)
index > -1 && state.cachedViews.splice(index, 1)
},
DEL_OTHERS_CACHED_VIEWS: (state, view) => {
// const index = state.cachedViews.indexOf(view.name)
const index = state.cachedViews.indexOf(view.path)
if (index > -1) {
state.cachedViews = state.cachedViews.slice(index, index + 1)
} else {
state.cachedViews = []
}
},
DEL_ALL_CACHED_VIEWS: state => {
state.cachedViews = []
},
DEL_RIGHT_VIEWS: (state, view) => {
const index = state.visitedViews.findIndex(v => v.path === view.path)
if (index === -1) {
return
}
state.visitedViews = state.visitedViews.filter((item, idx) => {
if (idx <= index || (item.meta && item.meta.affix)) {
return true
}
// const i = state.cachedViews.indexOf(item.name)
const i = state.cachedViews.indexOf(item.path)
if (i > -1) {
state.cachedViews.splice(i, 1)
}
if(item.meta.link) {
const fi = state.iframeViews.findIndex(v => v.path === item.path)
state.iframeViews.splice(fi, 1)
}
return false
})
},
DEL_LEFT_VIEWS: (state, view) => {
const index = state.visitedViews.findIndex(v => v.path === view.path)
if (index === -1) {
return
}
state.visitedViews = state.visitedViews.filter((item, idx) => {
if (idx >= index || (item.meta && item.meta.affix)) {
return true
}
// const i = state.cachedViews.indexOf(item.name)
const i = state.cachedViews.indexOf(item.path)
if (i > -1) {
state.cachedViews.splice(i, 1)
}
if(item.meta.link) {
const fi = state.iframeViews.findIndex(v => v.path === item.path)
state.iframeViews.splice(fi, 1)
}
return false
})
}
}
const actions = {
addView({ dispatch }, view) {
// dispatch('addVisitedView', view)
dispatch('addCachedView', view)
},
addCachedView({ commit }, view) {
commit('ADD_CACHED_VIEW', view)
},
delView({ dispatch, state }, view) {
return new Promise(resolve => {
dispatch('delVisitedView', view)
dispatch('delCachedView', view)
resolve({
// visitedViews: [...state.visitedViews],
cachedViews: [...state.cachedViews]
})
})
},
delCachedView({ commit, state }, view) {
return new Promise(resolve => {
commit('DEL_CACHED_VIEW', view)
resolve([...state.cachedViews])
})
},
delOthersViews({ dispatch, state }, view) {
return new Promise(resolve => {
// dispatch('delOthersVisitedViews', view)
dispatch('delOthersCachedViews', view)
resolve({
// visitedViews: [...state.visitedViews],
cachedViews: [...state.cachedViews]
})
})
},
delOthersCachedViews({ commit, state }, view) {
return new Promise(resolve => {
commit('DEL_OTHERS_CACHED_VIEWS', view)
resolve([...state.cachedViews])
})
},
delAllViews({ dispatch, state }, view) {
return new Promise(resolve => {
// dispatch('delAllVisitedViews', view)
dispatch('delAllCachedViews', view)
resolve({
// visitedViews: [...state.visitedViews],
cachedViews: [...state.cachedViews]
})
})
},
delAllCachedViews({ commit, state }) {
return new Promise(resolve => {
commit('DEL_ALL_CACHED_VIEWS')
resolve([...state.cachedViews])
})
},
delRightTags({ commit }, view) {
return new Promise(resolve => {
commit('DEL_RIGHT_VIEWS', view)
resolve([...state.visitedViews])
})
},
delLeftTags({ commit }, view) {
return new Promise(resolve => {
commit('DEL_LEFT_VIEWS', view)
resolve([...state.visitedViews])
})
},
}
export default {
namespaced: true,
state,
mutations,
actions
}
步骤二:在 src\store 文件夹下建立文件 getters.js
const getters = {
cachedViews: state => state.tagsView.cachedViews,
}
export default getters
步骤三:在 src\store 文件夹下建立文件 index.js
import Vue from 'vue'
import Vuex from 'vuex'
import tagsView from './modules/tagsView'
import getters from './getters'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
tagsView
},
getters
})
export default store
步骤四:在 AppMain.vue 文件内获取 cachedViews
<template>
<section class="app-main">
<transition name="fade-transform" mode="out-in">
<!-- <keep-alive :include="cachedViews">
<router-view v-if="!$route.meta.link" :key="key" />
</keep-alive> -->
<BaseKeepAlive :include="cachedViews">
<router-view :key="$route.path" />
</BaseKeepAlive>
</transition>
</section>
</template>
<script>
export default {
name: 'AppMain',
components: { iframeToggle },
computed: {
cachedViews() {
return this.$store.state.tagsView.cachedViews
},
key() {
return this.$route.path
}
}
}
</script>
5、路由配置
若依系统控制页面是否缓存的关键字段是 noCache (如果设置为true,则不会被 <keep-alive> 缓存(默认 false))
感兴趣的可以都看看:
主要参考(vue中如何使用keep-alive动态删除已缓存组件_vue keep alive 动态清除_FighterLiu的博客-CSDN博客)
vue3中tab详情页多开keepalive根据key去动态缓存与清除缓存
Vue 更改keep-alive源码,满足条件性缓存(多个页签之间切换缓存,关闭页签重新打开不缓存)_keepalive后关闭tag再次进入没有mounted_相约在一年四季的博客-CSDN博客
不同路由复用同一组件时,vue缓存(keep-alive) 必须同时删除 的解决方案 - 掘金
Vue 更改keep-alive源码,满足条件性缓存(多个页签之间切换缓存,关闭页签重新打开不缓存)_keepalive后关闭tag再次进入没有mounted_相约在一年四季的博客-CSDN博客
使用自定义的keep-alive组件,将公共页面缓存多份,都有自己的生命周期_vue共同用一个页面怎么做多个编辑缓存_Litluecat的博客-CSDN博客
前端 - 如何给VUE组件动态生成name? - SegmentFault 思否
面试必备之vue中带参动态路由遇到keep-alive 擦出美妙火花╰( ̄▽ ̄)╮ - 掘金
vue keep-alive使用:多标签保留缓存和销毁缓存;实现标签的刷新、删除、复制;解决缓存导致的浏览器爆栈 - 掘金
关于Vue keep-alive缓存的那点事及后台管理用tabs缓存问题及独立页缓存问题!!! - 掘金
Vue黑科技--从原理层面清除 keep-alive 缓存的组件 - 掘金
vue页面缓存keep-alive,可删除缓存的页面,再添加-CSDN博客
Vue匿名组件使用keep-alive后动态清除缓存_孙先生213的博客-CSDN博客
Vue 全站缓存之 keep-alive : 动态移除缓存-CSDN博客
转转|神颜小哥哥手把手带你玩转Vue多实例路由-CSDN博客
Vue3后台管理系统标签页<KeepAlive>终极解决方案 - 掘金
使用 Vue keep-alive 组件遇到的问题总结 - 掘金
vue组件及路由缓存keep-alive——vue路由缓存页面,组件缓存_vue 多级路由缓存_LIUupup_的博客-CSDN博客
更多推荐
所有评论(0)