From 9a21fe3b096b70bc9a0eb6e8a1ce8d71fbfae8e4 Mon Sep 17 00:00:00 2001
From: iczer <1126263215@qq.com>
Date: Fri, 7 Aug 2020 22:18:02 +0800
Subject: [PATCH] =?UTF-8?q?fix:=20the=20cache=20problem=20in=20tabs=20mode?=
 =?UTF-8?q?;:bug:=20=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=A4=9A=E9=A1=B5=E7=AD=BE?=
 =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E4=B8=8B=E7=9A=84=E7=BC=93=E5=AD=98=E9=97=AE?=
 =?UTF-8?q?=E9=A2=98=EF=BC=9B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/components/cache/AKeepAlive.js | 141 +++++++++++++++++++++++++++++
 src/layouts/BlankView.vue          |   7 +-
 src/layouts/PageView.vue           |   5 +-
 src/layouts/tabs/TabsView.vue      |  78 +++++++---------
 src/store/modules/setting.js       |   4 -
 src/utils/util.js                  |  21 +++++
 6 files changed, 199 insertions(+), 57 deletions(-)
 create mode 100644 src/components/cache/AKeepAlive.js
 create mode 100644 src/utils/util.js

diff --git a/src/components/cache/AKeepAlive.js b/src/components/cache/AKeepAlive.js
new file mode 100644
index 0000000..f0f412e
--- /dev/null
+++ b/src/components/cache/AKeepAlive.js
@@ -0,0 +1,141 @@
+import {isDef, isRegExp, remove} from '@/utils/util'
+
+const patternTypes = [String, RegExp, Array]
+
+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 getComponentName (opts) {
+  return opts && (opts.Ctor.options.name || opts.tag)
+}
+
+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) || c.isAsyncPlaceholder)) {
+        return c
+      }
+    }
+  }
+}
+
+function pruneCache (keepAliveInstance, filter) {
+  const { cache, keys, _vnode } = keepAliveInstance
+  for (const key in cache) {
+    const cachedNode = cache[key]
+    if (cachedNode) {
+      const name = getComponentName(cachedNode.componentOptions)
+      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)
+}
+
+export default {
+  name: 'AKeepAlive',
+  abstract: true,
+  model: {
+    prop: 'clearCaches',
+    event: 'clear',
+  },
+  props: {
+    include: patternTypes,
+    exclude: patternTypes,
+    max: [String, Number],
+    clearCaches: Array
+  },
+
+  watch: {
+    clearCaches: function(val) {
+      if (val && val.length > 0) {
+        const {cache, keys} = this
+        val.forEach(key => {
+          pruneCacheEntry(cache, key, keys, this._vnode)
+        })
+        this.$emit('clear', [])
+      }
+    }
+  },
+
+  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(componentOptions)
+      const { include, exclude } = this
+      if (
+        // not included
+        (include && (!name || !matches(include, name))) ||
+        // excluded
+        (exclude && name && matches(exclude, name))
+      ) {
+        return vnode
+      }
+
+      const { cache, keys } = this
+      const key = vnode.key == null
+        // same constructor may get registered as different local components
+        // so cid alone is not enough (#3269)
+        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
+        : vnode.key
+      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])
+  }
+}
diff --git a/src/layouts/BlankView.vue b/src/layouts/BlankView.vue
index 742f3b3..6e3f81a 100644
--- a/src/layouts/BlankView.vue
+++ b/src/layouts/BlankView.vue
@@ -1,9 +1,6 @@
 <template>
   <page-toggle-transition :disabled="animate.disabled" :animate="animate.name" :direction="animate.direction">
-    <keep-alive :exclude="dustbins" v-if="multiPage">
-      <router-view />
-    </keep-alive>
-    <router-view v-else />
+    <router-view />
   </page-toggle-transition>
 </template>
 
@@ -15,7 +12,7 @@ export default {
   name: 'BlankView',
   components: {PageToggleTransition},
   computed: {
-    ...mapState('setting', ['multiPage', 'animate', 'dustbins'])
+    ...mapState('setting', ['multiPage', 'animate'])
   }
 }
 </script>
diff --git a/src/layouts/PageView.vue b/src/layouts/PageView.vue
index 725f86a..6b14270 100644
--- a/src/layouts/PageView.vue
+++ b/src/layouts/PageView.vue
@@ -4,10 +4,7 @@
       <img :src="extraImage"/>
     </div>
     <page-toggle-transition :disabled="animate.disabled" :animate="animate.name" :direction="animate.direction">
-      <keep-alive :exclude="dustbins" v-if="multiPage">
         <router-view ref="page" />
-      </keep-alive>
-      <router-view ref="page" v-else />
     </page-toggle-transition>
   </page-layout>
 </template>
@@ -26,7 +23,7 @@ export default {
     }
   },
   computed: {
-    ...mapState('setting', ['isMobile', 'multiPage', 'animate', 'dustbins']),
+    ...mapState('setting', ['isMobile', 'multiPage', 'animate']),
     desc() {
       return this.page.desc
     },
diff --git a/src/layouts/tabs/TabsView.vue b/src/layouts/tabs/TabsView.vue
index 5c3aa3a..2f35161 100644
--- a/src/layouts/tabs/TabsView.vue
+++ b/src/layouts/tabs/TabsView.vue
@@ -17,9 +17,9 @@
     </a-tabs>
     <div class="tabs-view-content" :style="`margin-top: ${multiPage ? -24 : 0}px`">
       <page-toggle-transition :disabled="animate.disabled" :animate="animate.name" :direction="animate.direction">
-        <keep-alive :exclude="dustbins" v-if="multiPage">
-          <router-view :key="$route.fullPath" />
-        </keep-alive>
+        <a-keep-alive v-if="multiPage" v-model="clearCaches">
+          <router-view ref="tabContent" :key="$route.fullPath" />
+        </a-keep-alive>
         <router-view v-else />
       </page-toggle-transition>
     </div>
@@ -32,20 +32,23 @@ import Contextmenu from '@/components/menu/Contextmenu'
 import PageToggleTransition from '@/components/transition/PageToggleTransition'
 import {mapState, mapMutations} from 'vuex'
 import {getI18nKey} from '@/utils/routerUtil'
+import AKeepAlive from '@/components/cache/AKeepAlive'
 
 export default {
   name: 'TabsView',
   i18n: require('./i18n'),
-  components: { PageToggleTransition, Contextmenu, AdminLayout },
+  components: { PageToggleTransition, Contextmenu, AdminLayout , AKeepAlive },
   data () {
     return {
+      clearCaches: [],
       pageList: [],
+      cachedKeys: [],
       activePage: '',
       menuVisible: false
     }
   },
   computed: {
-    ...mapState('setting', ['multiPage', 'animate', 'layout', 'dustbins']),
+    ...mapState('setting', ['multiPage', 'animate', 'layout']),
     menuItemList() {
       return [
         { key: '1', icon: 'vertical-right', text: this.$t('closeLeft') },
@@ -67,6 +70,7 @@ export default {
   },
   mounted () {
     this.correctPageMinHeight(-this.tabsOffset)
+    this.cachedKeys.push(this.$refs.tabContent.$vnode.key)
   },
   beforeDestroy() {
     window.removeEventListener('page:close', this.closePageListener)
@@ -75,10 +79,12 @@ export default {
   watch: {
     '$route': function (newRoute) {
       this.activePage = newRoute.fullPath
-      this.putCache(newRoute)
       if (!this.multiPage) {
         this.pageList = [newRoute]
       } else if (this.pageList.findIndex(item => item.fullPath == newRoute.fullPath) == -1) {
+        this.$nextTick(() => {
+          this.cachedKeys.push(this.$refs.tabContent.$vnode.key)
+        })
         this.pageList.push(newRoute)
       }
     },
@@ -107,9 +113,9 @@ export default {
         return this.$message.warning(this.$t('warn'))
       }
       let index = this.pageList.findIndex(item => item.fullPath === key)
-      let pageRoute = this.pageList[index]
-      this.clearCache(pageRoute)
-      this.pageList = this.pageList.filter(item => item.fullPath !== key)
+      //清除缓存
+      this.clearCaches = this.cachedKeys.splice(index, 1)
+      this.pageList.splice(index, 1)
       if (next) {
         this.$router.push(next)
       } else if (key === this.activePage) {
@@ -136,56 +142,40 @@ export default {
     },
     closeOthers (pageKey) {
       const index = this.pageList.findIndex(item => item.fullPath === pageKey)
-      // 要关闭的页面清除缓存
-      this.pageList.forEach(item => {
-        if (item.fullPath !== pageKey){
-          this.clearCache(item)
-        }
-      })
+      // 清除缓存
+      this.clearCaches = this.cachedKeys.filter((item, i) => i != index)
+      this.cachedKeys = this.cachedKeys.slice(index, index + 1)
+
       this.pageList = this.pageList.slice(index, index + 1)
-      this.activePage = this.pageList[0].fullPath
-      this.$router.push(this.activePage)
+      if (this.activePage != pageKey) {
+        this.activePage = pageKey
+        this.$router.push(this.activePage)
+      }
     },
     closeLeft (pageKey) {
       const index = this.pageList.findIndex(item => item.fullPath === pageKey)
       // 清除缓存
-      this.pageList.forEach((item, i) => {
-        if (i < index) {
-          this.clearCache(item)
-        }
-      })
+      this.clearCaches = this.cachedKeys.filter((item, i) => i < index)
+      this.cachedKeys = this.cachedKeys.slice(index)
+
       this.pageList = this.pageList.slice(index)
-      if (this.pageList.findIndex(item => item.fullPath === this.activePage) === -1) {
-        this.activePage = this.pageList[0].fullPath
+      if (!this.pageList.find(item => item.fullPath === this.activePage)) {
+        this.activePage = pageKey
         this.$router.push(this.activePage)
       }
     },
     closeRight (pageKey) {
       const index = this.pageList.findIndex(item => item.fullPath === pageKey)
       // 清除缓存
-      this.pageList.forEach((item, i) => {
-        if (i > index) {
-          this.clearCache(item)
-        }
-      })
+      this.clearCaches = this.cachedKeys.filter((item, i) => i > index)
+      this.cachedKeys = this.cachedKeys.slice(0, index+1)
+
       this.pageList = this.pageList.slice(0, index + 1)
-      if (this.pageList.findIndex(item => item.fullPath === this.activePage) === -1) {
-        this.activePage = this.pageList[this.pageList.length - 1].fullPath
+      if (!this.pageList.find(item => item.fullPath === this.activePage)) {
+        this.activePage = pageKey
         this.$router.push(this.activePage)
       }
     },
-    clearCache(route) {
-      const componentName = route.matched.slice(-1)[0].components.default.name
-      if (this.dustbins.findIndex(item => item === componentName) === -1) {
-        this.setDustbins(this.dustbins.concat(componentName))
-      }
-    },
-    putCache(route) {
-      const componentName = route.matched.slice(-1)[0].components.default.name
-      if (this.dustbins.includes(componentName)) {
-        this.setDustbins(this.dustbins.filter(item => item !== componentName))
-      }
-    },
     pageName(page) {
       return this.$t(getI18nKey(page.matched[page.matched.length - 1].path))
     },
@@ -194,7 +184,7 @@ export default {
       const closePath = typeof closeRoute === 'string' ? closeRoute : closeRoute.path
       this.remove(closePath, nextRoute)
     },
-    ...mapMutations('setting', ['setDustbins', 'correctPageMinHeight'])
+    ...mapMutations('setting', ['correctPageMinHeight'])
   }
 }
 /**
diff --git a/src/store/modules/setting.js b/src/store/modules/setting.js
index 9fd65b3..7f0c22b 100644
--- a/src/store/modules/setting.js
+++ b/src/store/modules/setting.js
@@ -6,7 +6,6 @@ export default {
     isMobile: false,
     animates: ADMIN.animates,
     palettes: ADMIN.palettes,
-    dustbins: [],
     pageMinHeight: 0,
     menuData: [],
     ...config,
@@ -42,9 +41,6 @@ export default {
     setHideSetting(state, hideSetting) {
       state.hideSetting = hideSetting
     },
-    setDustbins(state, dustbins) {
-      state.dustbins = dustbins
-    },
     correctPageMinHeight(state, minHeight) {
       state.pageMinHeight += minHeight
     },
diff --git a/src/utils/util.js b/src/utils/util.js
new file mode 100644
index 0000000..96b965c
--- /dev/null
+++ b/src/utils/util.js
@@ -0,0 +1,21 @@
+export function isDef (v){
+  return v !== undefined && v !== null
+}
+
+/**
+ * Remove an item from an array.
+ */
+export function remove (arr, item) {
+  if (arr.length) {
+    const index = arr.indexOf(item)
+    if (index > -1) {
+      return arr.splice(index, 1)
+    }
+  }
+}
+
+export function isRegExp (v) {
+  return _toString.call(v) === '[object RegExp]'
+}
+
+const _toString = Object.prototype.toString