core icon indicating copy to clipboard operation
core copied to clipboard

feat(KeepAlive): support matchBy + allow custom caching strategy

Open HcySunYang opened this issue 4 years ago • 51 comments

This is a copy of the old PR https://github.com/vuejs/vue-next/pull/3414, for some reasons, I had to reopen a new one.

RFC vuejs/rfcs#284

HcySunYang avatar Aug 15 '21 04:08 HcySunYang

Looking forward to this feature.

hezhongfeng avatar Aug 16 '21 00:08 hezhongfeng

Looking forward to this feature.

nandongdong avatar Nov 14 '21 15:11 nandongdong

大概在之后哪个版本可以正式用上他呢

huangqian0310 avatar Nov 19 '21 01:11 huangqian0310

Looking forward to this feature.

wcldyx avatar Jan 17 '22 08:01 wcldyx

Looking forward to this feature

dfengwei avatar Jan 27 '22 03:01 dfengwei

ABOUT这个,有没有计划上线啊... BROTHER. [狗头]

0x30 avatar Feb 18 '22 09:02 0x30

Looking forward...

luoguibin avatar Feb 28 '22 03:02 luoguibin

This keep-alive custom purge feature is so important for multi tab pages. And it have been discussed many time since Vue2. I don't why this is not fixed yet now. Vue3 team should really think about it.

tony-gm avatar Mar 04 '22 04:03 tony-gm

As @tony-gm says, this feature is essential for multi-tab pages where the tabs are instances of the same component and the key is being used to differentiate them. For me it is enough that the include/exclude can match on key as well as name, as the custom caching part is what I would consider advanced functionality. So can the key matching be implemented by itself as this is a much simpler (and presumably less risky?) change. Hard to see why it has not already been done when it seems so straightforward.

paama avatar Mar 08 '22 12:03 paama

Here is a temporary solution I can share, hope can help somebody who also work on sort of "multi tab page" project.

  1. Since keep-alive just a component, so we can get it's instance by set a ref
  2. I went through keepAlive's source code, found that all the cached vnode keep in a map named __v_cache. Keep-alive internal also just check the incude, exclude, max and delete it form the __v_cache, It's not complicate.
  3. So I follow the internal pruneCacheEntry function make a removeCache function to manipulate the __v_cache by myself.
function removeCache(cacheKey) {
      const keepAliveComponent = vm.proxy.$refs.keepAlive.$;
      const cacheMap = keepAliveComponent.__v_cache;
      const cachedVnode = cacheMap.get(cacheKey);
      if (cachedVnode) {
        const COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8;
        const COMPONENT_KEPT_ALIVE = 1 << 9;
        let shapeFlag = cachedVnode.shapeFlag;
        if (shapeFlag & COMPONENT_SHOULD_KEEP_ALIVE) {
          shapeFlag -= COMPONENT_SHOULD_KEEP_ALIVE;
        }
        if (shapeFlag & COMPONENT_KEPT_ALIVE) {
          shapeFlag -= COMPONENT_KEPT_ALIVE;
        }
        cachedVnode.shapeFlag = shapeFlag;
        const keepAliveRenderer = keepAliveComponent.ctx.renderer;
        keepAliveRenderer.um(
          cachedVnode,
          keepAliveComponent,
          keepAliveComponent.suspense,
          false,
          false
        );
        cacheMap.delete(cacheKey);
      }
    }

It's work , now we can remove any cache by a key; BUT, in keepAlive source code, the __v_cache only valid for dev mode, not production mode.

    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
      ;(instance as any).__v_cache = cache
    }

Base on this, the removeCache function is not suite for production mode. Then I try to follow the official keepAlive to create a myKeepAlive component. Unfortunately keepAlive used a lot of vue internal core function which not exposed for enduser. So I failed and give it up.

FINALLY, there is only one way I can do is to modify the vue distributed file to remove the DEV predication, just like below

let current = null;
//if ((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) {
     instance.__v_cache = cache;
//}
const parentSuspense = instance.suspense;

Now removeCache working again. And I made a build script to automatically replace it, and added it in my build command.

var fs = require("fs");
var path = require("path");
var vue_bundler_file = path.resolve(
  __dirname,
  "../../node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js"
);
fs.readFile(vue_bundler_file, "utf8", function (err, data) {
  if (err) console.error(err);
  let orginal_str =
    "        if ((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) {\r\n            instance.__v_cache = cache;\r\n        }";
  let target_str =
    "        //if ((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) {\r\n            instance.__v_cache = cache;\r\n        //}";
  var result = data.replace(orginal_str, target_str);
  fs.writeFile(vue_bundler_file, result, "utf8", function (err) {
    if (err) return console.error(err);
  });
});

  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "node ./src/build/replace-vue.js && vue-cli-service build",
    "lint": "vue-cli-service lint"
  }
``

So far it works for me, I hope it would be help for somebody. If anybody have some other solution. Also please shared it to me, we can discuss it together, thanks.

tony-gm avatar Mar 08 '22 14:03 tony-gm

Thanks @tony-gm, I had already encountered the solution to use the cache directly and also ran into the production mode problem but your subsequent fix for this has solved this for me too. It will keep us going until Vue can add the correct functionality to keep-alive Many thanks for this!

paama avatar Mar 08 '22 15:03 paama

Since the keep-alive keep bother me a long time. I have a proposal

  1. Provide a composoable function createKeepAliveCache, return a KeepAliveCache object.
export interface KeepAliveCache {
  include: Ref<MatchPattern | undefined>
  exclude: Ref<MatchPattern | undefined>
  max: Ref<number | string | undefined>
  cache: Map<string, VNode>
  remove: (key: string) => void
  clear: () => void
}
  1. KeepAlive component, provide a new prop named cache, then user can v-bind to createKeepAliveCache() returned KeepAliveCache object.

  2. User can change include,exclude,max to maniplate cache as current API (since it's a Ref), also remove(key) can remove a cache by key, clear() to clear all cache.

Here is a full example

<template>
  <button @click="onCloseTab">Close Current Tab</button>
  <button @click="onCloseAllTabs">Close All Tabs</button>
  <router-view v-slot="{ Component, route }">
    <keep-alive :cache="pageCache">
      <component :is="Component" :key="route.fullPath" />
    </keep-alive>
  </router-view>
</template>

<script lang="ts">
import { defineComponent, createKeepAliveCache, ref } from 'vue'
export default defineComponent({
  name: 'App',
  setup() {
    const currentPath = ref('')
    const pageCache = createKeepAliveCache()
    pageCache.include.value = ['Component1']

    function onCloseTab() {
      pageCache.remove(currentPath.value)
    }

    function onCloseAllTabs() {
      pageCache.clear()
    }

    return {
      pageCache,
      onCloseTab,
      onCloseAllTabs
    }
  }
})
</script>

I document this in a RFC , more detail please check RFC Link

And I also did a prototype implement base on this RFC, currently it's work. Implement Link

Hope vuejs team can enhance keep-alive component like this.

tony-gm avatar Mar 09 '22 17:03 tony-gm

we really need the feature, more and more apps with multi tabs pop up

dreambo8563 avatar Jun 08 '22 09:06 dreambo8563

Need this feature.

hezhongfeng avatar Jun 09 '22 00:06 hezhongfeng

This feature is the only thing keeping me from migrating to vue 3, looking forward to this feature.

marceloatg avatar Jun 27 '22 18:06 marceloatg

Need this feature !

yokiyokiyoki avatar Jul 13 '22 11:07 yokiyokiyoki

include 其实就可以实现了,把组件包裹一层自定义一下的 name 就可以了。 参考: https://github.com/hminghe/md-admin-element-plus/blob/main/src/components/multi-window/components/MultiWindowKeepAlive.vue

include actually does that,Wrap the component around a custom name. see: https://github.com/hminghe/md-admin-element-plus/blob/main/src/components/multi-window/components/MultiWindowKeepAlive.vue

不过还有一点点BUG, 等这个合并了就完美了。https://github.com/vuejs/core/pull/6235

hminghe avatar Jul 14 '22 17:07 hminghe

已换react,但是还是想问什么时候可以提供,至少vue2还提供了$destroy,vue3把这个api去掉了,还不提供删除keepalive缓存的方法,也不提供动态修改组件name的方法

liweijian1 avatar Aug 17 '22 08:08 liweijian1

应该大部分都是做多tab单页应用的来提这个feature.

@liweijian1 在页面组件外面包个壳,像下面这样子,能完全可控操作keep-alive缓存, 而且不需要额外的内部规范,比如,页面组件必须强制加上组件名,配合在meta里配置上页面组件名称,

<template>
  <router-view v-slot="{ Component, route }">
     <keep-alive :include="include">
        <component :is="wrap(route.fullPath, Component)" :key="route.fullPath" />
     </keep-alive>
  </router-view>
</tempate>

<script>
import { h } from "vue";

// 自定义name的壳的集合
const wrapperMap = new Map();

export default {
  data() {
    return {
      include: [],
    };
  },
  watch: {
    $route: {
      handler(next) {
          // ??这个按自己业务需要,看是否需要cache页面组件

          const index = store.list.findIndex(
            (item) => item.fullPath === next.fullPath
          );
          // 如果没加入这个路由记录,则加入路由历史记录
          if (index === -1) {
            this.include.push(next.fullPath);
          }
      },
      immediate: true,
    },
  },
  methods: {
    // 为keep-alive里的component接收的组件包上一层自定义name的壳.
    wrap(fullPath, component) {
      let wrapper;
      // 重点就是这里,这个组件的名字是完全可控的,
      // 只要自己写好逻辑,每次能找到对应的外壳组件就行,完全可以写成任何自己想要的名字.
      // 这就能配合 keep-alive 的 include 属性可控地操作缓存.
      const wrapperName = fullPath; 
      if (wrapperMap.has(wrapperName)) {
        wrapper = wrapperMap.get(wrapperName);
      } else {
        wrapper = {
          name: wrapperName,
          render() {
            return h("div", { className: "vaf-page-wrapper" }, component);
          },
        };
        wrapperMap.set(wrapperName, wrapper);
      }
      return h(wrapper);
    },
  },
};
</script>

同时,这里包了一个壳,也有另外一个好处。 当外边有transition做过渡时,页面组件即使有多个节点,因为包了这个壳,也能顺利完成过渡效果。

虽然,但是,如果本身提供直接完全可控操作keep-alive缓存的api,显然是更好的。

chenhaihong avatar Sep 07 '22 06:09 chenhaihong

我这边是想完全自主的管理缓存,不用keep-alive,要使用自己的组件,只是确实这个APi

hezhongfeng avatar Sep 07 '22 07:09 hezhongfeng

@hezhongfeng 还是得用keep-alive。现在提供的api也不能让你不用keep-alive就去操作组件缓存。

chenhaihong avatar Sep 09 '22 09:09 chenhaihong

可以的,Vue2.x 就可以 vue-page-stack

hezhongfeng avatar Sep 10 '22 01:09 hezhongfeng

为什么这么好的提议不被支持呢

wcldyx avatar Sep 14 '22 08:09 wcldyx

需要这个特性。

maxwls avatar Sep 29 '22 09:09 maxwls

应该大部分都是做多tab单页应用的来提这个feature.

@liweijian1 在页面组件外面包个壳,像下面这样子,能完全可控操作keep-alive缓存, 而且不需要额外的内部规范,比如,页面组件必须强制加上组件名,配合在meta里配置上页面组件名称,

<template>
  <router-view v-slot="{ Component, route }">
     <keep-alive :include="include">
        <component :is="wrap(route.fullPath, Component)" :key="route.fullPath" />
     </keep-alive>
  </router-view>
</tempate>

<script>
import { h } from "vue";

// 自定义name的壳的集合
const wrapperMap = new Map();

export default {
  data() {
    return {
      include: [],
    };
  },
  watch: {
    $route: {
      handler(next) {
          // ??这个按自己业务需要,看是否需要cache页面组件

          const index = store.list.findIndex(
            (item) => item.fullPath === next.fullPath
          );
          // 如果没加入这个路由记录,则加入路由历史记录
          if (index === -1) {
            this.include.push(next.fullPath);
          }
      },
      immediate: true,
    },
  },
  methods: {
    // 为keep-alive里的component接收的组件包上一层自定义name的壳.
    wrap(fullPath, component) {
      let wrapper;
      // 重点就是这里,这个组件的名字是完全可控的,
      // 只要自己写好逻辑,每次能找到对应的外壳组件就行,完全可以写成任何自己想要的名字.
      // 这就能配合 keep-alive 的 include 属性可控地操作缓存.
      const wrapperName = fullPath; 
      if (wrapperMap.has(wrapperName)) {
        wrapper = wrapperMap.get(wrapperName);
      } else {
        wrapper = {
          name: wrapperName,
          render() {
            return h("div", { className: "vaf-page-wrapper" }, component);
          },
        };
        wrapperMap.set(wrapperName, wrapper);
      }
      return h(wrapper);
    },
  },
};
</script>

同时,这里包了一个壳,也有另外一个好处。 当外边有transition做过渡时,页面组件即使有多个节点,因为包了这个壳,也能顺利完成过渡效果。

虽然,但是,如果本身提供直接完全可控操作keep-alive缓存的api,显然是更好的。

按照这个包裹实现之后,vue报性能警告 image

@chenhaihong 请问大佬知道是怎么回事吗?

qq229338869 avatar Dec 19 '22 08:12 qq229338869

应该大部分都是做多tab单页应用的来提这个feature. @liweijian1 在页面组件外面包个壳,像下面这样子,能完全可控操作keep-alive缓存, 而且不需要额外的内部规范,比如,页面组件必须强制加上组件名,配合在meta里配置上页面组件名称,

<template>
  <router-view v-slot="{ Component, route }">
     <keep-alive :include="include">
        <component :is="wrap(route.fullPath, Component)" :key="route.fullPath" />
     </keep-alive>
  </router-view>
</tempate>

<script>
import { h } from "vue";

// 自定义name的壳的集合
const wrapperMap = new Map();

export default {
  data() {
    return {
      include: [],
    };
  },
  watch: {
    $route: {
      handler(next) {
          // ??这个按自己业务需要,看是否需要cache页面组件

          const index = store.list.findIndex(
            (item) => item.fullPath === next.fullPath
          );
          // 如果没加入这个路由记录,则加入路由历史记录
          if (index === -1) {
            this.include.push(next.fullPath);
          }
      },
      immediate: true,
    },
  },
  methods: {
    // 为keep-alive里的component接收的组件包上一层自定义name的壳.
    wrap(fullPath, component) {
      let wrapper;
      // 重点就是这里,这个组件的名字是完全可控的,
      // 只要自己写好逻辑,每次能找到对应的外壳组件就行,完全可以写成任何自己想要的名字.
      // 这就能配合 keep-alive 的 include 属性可控地操作缓存.
      const wrapperName = fullPath; 
      if (wrapperMap.has(wrapperName)) {
        wrapper = wrapperMap.get(wrapperName);
      } else {
        wrapper = {
          name: wrapperName,
          render() {
            return h("div", { className: "vaf-page-wrapper" }, component);
          },
        };
        wrapperMap.set(wrapperName, wrapper);
      }
      return h(wrapper);
    },
  },
};
</script>

同时,这里包了一个壳,也有另外一个好处。 当外边有transition做过渡时,页面组件即使有多个节点,因为包了这个壳,也能顺利完成过渡效果。 虽然,但是,如果本身提供直接完全可控操作keep-alive缓存的api,显然是更好的。

按照这个包裹实现之后,vue报性能警告 image

@chenhaihong 请问大佬知道是怎么回事吗?

不要使用reactive包裹你的组件变量, 可以用shallowReactive

hminghe avatar Dec 19 '22 08:12 hminghe

应该大部分都是做多tab单页应用的来提这个feature. @liweijian1 在页面组件外面包个壳,像下面这样子,能完全可控操作keep-alive缓存, 而且不需要额外的内部规范,比如,页面组件必须强制加上组件名,配合在meta里配置上页面组件名称,

<template>
  <router-view v-slot="{ Component, route }">
     <keep-alive :include="include">
        <component :is="wrap(route.fullPath, Component)" :key="route.fullPath" />
     </keep-alive>
  </router-view>
</tempate>

<script>
import { h } from "vue";

// 自定义name的壳的集合
const wrapperMap = new Map();

export default {
  data() {
    return {
      include: [],
    };
  },
  watch: {
    $route: {
      handler(next) {
          // ??这个按自己业务需要,看是否需要cache页面组件

          const index = store.list.findIndex(
            (item) => item.fullPath === next.fullPath
          );
          // 如果没加入这个路由记录,则加入路由历史记录
          if (index === -1) {
            this.include.push(next.fullPath);
          }
      },
      immediate: true,
    },
  },
  methods: {
    // 为keep-alive里的component接收的组件包上一层自定义name的壳.
    wrap(fullPath, component) {
      let wrapper;
      // 重点就是这里,这个组件的名字是完全可控的,
      // 只要自己写好逻辑,每次能找到对应的外壳组件就行,完全可以写成任何自己想要的名字.
      // 这就能配合 keep-alive 的 include 属性可控地操作缓存.
      const wrapperName = fullPath; 
      if (wrapperMap.has(wrapperName)) {
        wrapper = wrapperMap.get(wrapperName);
      } else {
        wrapper = {
          name: wrapperName,
          render() {
            return h("div", { className: "vaf-page-wrapper" }, component);
          },
        };
        wrapperMap.set(wrapperName, wrapper);
      }
      return h(wrapper);
    },
  },
};
</script>

同时,这里包了一个壳,也有另外一个好处。 当外边有transition做过渡时,页面组件即使有多个节点,因为包了这个壳,也能顺利完成过渡效果。 虽然,但是,如果本身提供直接完全可控操作keep-alive缓存的api,显然是更好的。

按照这个包裹实现之后,vue报性能警告 image @chenhaihong 请问大佬知道是怎么回事吗?

不要使用reactive包裹你的组件变量, 可以用shallowReactive

原因找到了,我用了pinia存储了wrapperMap,所以组件变成了响应式的。

qq229338869 avatar Dec 20 '22 01:12 qq229338869

Looking forward to this feature

danyadev avatar Jan 01 '23 15:01 danyadev

keep-alive这个特性可以使用了吗?

alaywn avatar Feb 10 '23 12:02 alaywn

#7702

This scheme can be tried while waiting for the merger, although it is extremely harmful. But you can use it now

jiangshengdev avatar Feb 13 '23 10:02 jiangshengdev