本文主要解决缓存同一个组件不同的页面,如何移除某个页面时不影响其他同组件的页面的问题。

一、 问题背景

在做后台管理系统(本文以若依管理系统为例:若依文档:介绍 | 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不就好了吗?但是这样是不行的,会导致本该被缓存的页面没有被缓存到,因此需要重新考虑可行的方案。

 三、可行的方案

  1. 通过动态改变组件名来使相同组件在不同路由下使用不同组件名。

  2. 更改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源码实现动态路由缓存 动态路由 - 掘金

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 缓存的组件 - 掘金

不同路由复用同一组件,keepAlive解决方案 - 掘金

vue页面缓存keep-alive,可删除缓存的页面,再添加-CSDN博客

Vue匿名组件使用keep-alive后动态清除缓存_孙先生213的博客-CSDN博客

Vue 全站缓存之 keep-alive : 动态移除缓存-CSDN博客

转转|神颜小哥哥手把手带你玩转Vue多实例路由-CSDN博客

Vue缓存 菜单、多Tab解决方案 - 掘金

Vue3后台管理系统标签页<KeepAlive>终极解决方案 - 掘金

使用 Vue keep-alive 组件遇到的问题总结 - 掘金

超细的tab标签页缓存方案(Vue2/Vue3) - 掘金

vue组件及路由缓存keep-alive——vue路由缓存页面,组件缓存_vue 多级路由缓存_LIUupup_的博客-CSDN博客

Logo

前往低代码交流专区

更多推荐