实现动态路由缓存:解决不同路由复用同一组件时,vue缓存(keep-alive) 同时删除的问题 (解决缓存同一个组件不同的页面,如何移除某个页面时不影响其他同组件的页面的问题)

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

一、 问题背景

在做后台管理系统(本文以若依管理系统为例:若依文档:介绍 | 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博客

  • 7
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
### 回答1: 在Vue中,解决跳转同一路由后数据缓存有几种方式。 1. 使用<router-link>的replace属性:将<router-link>的replace属性设置为true,可以在跳转替换浏览器的历史记录,这样在跳转后再返回到该路由,不会触发数据缓存。 2. 使用<keep-alive>组件:将需要缓存数据的组件包裹在<keep-alive>标签中,<keep-alive>会缓存组件的状态,当再次访问该组件,会直接从缓存中读取数据,而不是重新渲染。 3. 在路由配置中设置meta属性:在路由配置的meta属性中添加一个标志位,用来标记该路由是否需要缓存数据。然后在组件的created或mounted钩子函数中判断这个标志位,根据需要决定是否重新获取数据。 4. 使用beforeRouteEnter和beforeRouteUpdate守卫:在路由配置的beforeRouteEnter守卫中,可以通过to和from参数判断当前路由是否为同一路由,如果是,则可以手动调用next函数来执行组件的渲染逻辑,避免数据缓存。在beforeRouteUpdate守卫中也可以使用相同的方法来判断是否需要重新获取数据。 综上所述,Vue中可以通过设置<router-link>的replace属性、使用<keep-alive>组件、在路由配置中设置meta属性或使用守卫来解决跳转同一路由后数据缓存问题。根据具体需求选择合适的方法即可。 ### 回答2: 在Vue中,可以通过以下几种方式来解决跳转同一路由后数据缓存问题: 1. 使用key属性:在路由的<router-view>标签中添加:key=" $route.fullPath"属性。这样,每次路由切换Vue会重新创建组件,从而避免了数据的缓存。 2. 使用$route的钩子函数:Vue的$route对象提供了多个钩子函数,可以在路由切换的不同生命周期中一些操作。其中,beforeRouteUpdate钩子函数可以在组件复用进行特定的逻辑处理,例如清空数据、重新获取数据、判断数据是否需要更新等。 3. 使用watch监听$route对象的变化:可以通过watch监听$route对象的变化,在路由变化触发相应的操作,例如清空数据、重新获取数据等。 4. 使用Vuex进行数据管理:将需要缓存的数据存储到Vuex中,通过mutations对数据进行更新。这样,在路由重新渲染,可以及Vuex中获取最新的数据。 5. 使用keep-alive组件:将需要缓存组件包裹在<keep-alive>标签中,这样在路由切换组件的状态和数据会被缓存下来,不会被销毁和重新创建,从而实现数据的缓存。 综上所述,Vue提供了多种解决跳转同一路由后数据缓存的方式,可以根据实际需求选择相应的方法来解决问题。 ### 回答3: 在Vue中,可以通过使用路由导航守卫和缓存组件解决跳转同一路由后数据缓存问题。 1. 使用路由导航守卫:在进入路由之前,可以通过beforeRouteUpdate钩子函数来检查当前路由是否与上一个路由相同。如果相同,则可以手动清除掉之前路由的数据缓存,可以使用如下代码: ``` beforeRouteUpdate(to, from, next) { if (to.path === from.path) { // 清除数据缓存的操作 } next(); } ``` 2. 使用缓存组件:可以为需要缓存组件设置一个keep-alive标签,使组件在切换路由保持缓存状态。例如: ``` <template> <keep-alive> <router-view></router-view> </keep-alive> </template> ``` 这样,即使跳转到同一路由,之前的数据仍然会被缓存,不会丢失。 需要注意的是,使用缓存组件,如果希望在重新进入路由更新数据,可以在组件中使用activated钩子函数,该钩子函数会在组件激活调用。例如: ``` activated() { // 更新数据的操作 } ``` 综上所述,通过使用路由导航守卫和缓存组件,可以解决Vue中跳转同一路由后数据缓存问题

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值