Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

HcySunYang
Copy link
Member

This is a copy of the old PR #3414, for some reasons, I had to reopen a new one.

RFC vuejs/rfcs#284

@hezhongfeng
Copy link

Looking forward to this feature.

@nandongdong
Copy link

Looking forward to this feature.

@huangqian0310
Copy link

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

@wcldyx
Copy link

wcldyx commented Jan 17, 2022

Looking forward to this feature.

@dfengwei
Copy link

Looking forward to this feature

@0x30
Copy link

0x30 commented Feb 18, 2022

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

@luoguibin
Copy link

Looking forward...

@tony-gm
Copy link

tony-gm commented Mar 4, 2022

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.

@paama
Copy link

paama commented Mar 8, 2022

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.

@tony-gm
Copy link

tony-gm commented Mar 8, 2022

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.

@paama
Copy link

paama commented Mar 8, 2022

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!

@tony-gm
Copy link

tony-gm commented Mar 9, 2022

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.

@dreambo8563
Copy link

dreambo8563 commented Jun 8, 2022

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

@hezhongfeng
Copy link

Need this feature.

@hminghe
Copy link

hminghe commented Dec 19, 2022

应该大部分都是做多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

@qq229338869
Copy link

应该大部分都是做多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,所以组件变成了响应式的。

@danyadev
Copy link

danyadev commented Jan 1, 2023

Looking forward to this feature

@alaywn
Copy link

alaywn commented Feb 10, 2023

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

@jiangshengdev
Copy link

#7702

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

@majoson-chen
Copy link

#7928

@wf-soft
Copy link

wf-soft commented Apr 20, 2023

这个问题可真是如鲠在喉,对移动端很多网站来说这是重大缺陷,如今很多都是滚动加载的页面,谁能接受他回不去,网站都写完了才发现这个问题。。而且nuxt3的NuxtPage对keepalive包含排除还有BUG,还不能用原生的路由替换,默契配合超级加倍

@xiaobc1234
Copy link

这个特性能用了吗

@alaywn
Copy link

alaywn commented Jun 12, 2023

这个特性能用了吗?

@hezhongfeng
Copy link

我试着完成了一个不完善的页面堆栈管理,目前唯一的缺点是不能使用transition.
https://github.com/hezhongfeng/vue-page-stack

@Riant
Copy link

Riant commented Aug 31, 2023

#5105
这样一个很小的改动就可以了嘛,不懂为何一直没有支持呢?

@Riant
Copy link

Riant commented Aug 31, 2023

找到一个对第三库源代码打补丁的方法:

npm/yarn:patch package

我用的是 pnpm,他内置了一个 patch 命令 ( pnpm patch ),下面说一下具体操作:
在你的项目文件夹运行命令(注意检查你本地的版本号,并修改3.3.4对应版本号)

pnpm patch @vue/[email protected]

然后 pnpm 会打印出库文件的临时源代码目录,进入修改 ./dist 目录的 3 个 js 文件:

  1. 搜索 const KeepAliveImpl = { 找到 KeepAlive 组件代码区域;
  2. setup 代码块,第二个参数新增解构 expose;return 语句前面加 expose({pruneCacheEntry})。当然,也可以参考这个 pr 进行更细致的修改: feat(keepAlive): expose pruneCacheEntry and add key match support on exclude/include changed in pruneCache. #5105

最后运行上述 pnpm 语句给出的 pnpm patch-commit 语句,然后就可以愉快的使用 this.$refs.keepAlive.pruneCacheEntry(xxxx) 移除路由页面缓存了。

@trydofor
Copy link

trydofor commented Sep 11, 2023

see
vuejs/vue#8028
#6219
#6235

@Harmaccor
Copy link

need this feature

1 similar comment
@zeta-core
Copy link

need this feature

@coader
Copy link

coader commented Dec 9, 2023

+1

1 similar comment
@littleBoBoy
Copy link

+1

@NickBoomBoom
Copy link

需要这个功能,实在是很困扰

@fcl999
Copy link

fcl999 commented Apr 24, 2024

需要这个功能

@alaywn
Copy link

alaywn commented Apr 24, 2024

unplanned?

@kevinforrestconnors
Copy link

Really need this feature

@haoqunjiang haoqunjiang added version: minor 🍰 p2-nice-to-have Priority 2: this is not breaking anything but nice to have it addressed. labels May 27, 2024
@fgt1t5y
Copy link

fgt1t5y commented Aug 24, 2024

这个特性能用了吗?

@alaywn
Copy link

alaywn commented Aug 25, 2024

这个特性能用了吗?

想多了

@fgt1t5y
Copy link

fgt1t5y commented Aug 25, 2024

这个特性能用了吗?

想多了

我真服了,我直接patch源代码得了

@alaywn
Copy link

alaywn commented Aug 25, 2024

这个特性能用了吗?

想多了

我真服了,我直接patch源代码得了

我用的都是楼上的一个大牛的方案做的,等这个特性要死人的

@fgt1t5y
Copy link

fgt1t5y commented Aug 25, 2024

这个特性能用了吗?

想多了

我真服了,我直接patch源代码得了

我用的都是楼上的一个大牛的方案做的,等这个特性要死人的

大概率unplanned了...

刚把源代码patch了一下,先凑合用吧,反正patch能跟着项目走

图片

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🍰 p2-nice-to-have Priority 2: this is not breaking anything but nice to have it addressed. scope: keep-alive version: minor
Projects
Development

Successfully merging this pull request may close these issues.