vue-loader icon indicating copy to clipboard operation
vue-loader copied to clipboard

Hot reload problem when use keep-alive wrap router-view with a dynamic key

Open hxlyd opened this issue 6 years ago • 28 comments

Version

13.3.0

Reproduction link

https://codesandbox.io/s/vyy3yokjj0

Steps to reproduce

1.modify the code in the script tag of the Hello component

2.the router-view component disappear from the page

3.we can find the component is activated with devtools

What is expected?

Hot reload normally

What is actually happening?

Have to refresh the page manually each time modify the code

hxlyd avatar May 31 '18 13:05 hxlyd

I came here to report this issue back here, but it's already reported, so I just share the original issue from vue-router then to create a link.

https://github.com/vuejs/vue-router/issues/2150

maksnester avatar Jul 17 '18 15:07 maksnester

Does anyone have a work around?

twickstrom avatar Mar 23 '19 21:03 twickstrom

Anyone? :)

franciscolourenco avatar Apr 26 '19 06:04 franciscolourenco

Still nothing?

NikitaKA avatar May 28 '19 15:05 NikitaKA

This still persists. Is everyone using keep-alive just refreshing each time still?

mcoope31 avatar Jul 03 '19 14:07 mcoope31

This still persists. Is everyone using keep-alive just refreshing each time still?

For the most part... sigh

twickstrom avatar Jul 11 '19 21:07 twickstrom

Very annoying. I just spent a few hours to find where the issue comes from (blank page after HMR). In my case it seems to happen only if the edit involves a new line character.

vitto32 avatar Sep 24 '19 10:09 vitto32

@mcoope31

This still persists. Is everyone using keep-alive just refreshing each time still?

I am commenting <keep-alive> while working on a component inside scoped inside it.

ThomasKientz avatar Sep 24 '19 10:09 ThomasKientz

Any workaround?

7iomka avatar Dec 17 '19 12:12 7iomka

The best workaround I found is <nuxt :keep-alive="!isDev" /> where isDev = process.env.NODE_ENV === 'development'

haven't thought about it since.

mcoope31 avatar Dec 17 '19 15:12 mcoope31

Same problem.

hellomrbigshot avatar Jan 03 '20 07:01 hellomrbigshot

Workaround: disable keep-alive for debug. Warning: view component must have name

<template>
    <keep-alive :exclude="exclude">
      <router-view></router-view>
    </keep-alive>
</template>
<script>
export default {
  computed: {
    exclude () {
      if (process.env.NODE_ENV === 'production') {
        return ''
      }
      return /.+/
    }
  }
}
</script>

nikolawan avatar Jan 14 '20 17:01 nikolawan

因为router-view 给了key 所以会白屏

<keep-alive>
    <router-view :key="$route.fullPath"></router-view>
</keep-alive>

keep-alive 的源码: 缓存的key 就是取的 router-view 的key;

const { cache, keys } = this
      const key: ?string = 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)
        }
      }

每次hot update 的时候 cid 是会变化的 看 下面 vue-hot-reload 的源码 record.Ctor.cid = newCtor.cid

if (record.Ctor) {
      if (version[1] < 2) {
        // preserve pre 2.2 behavior for global mixin handling
        record.Ctor.extendOptions = options
      }
      var newCtor = record.Ctor.super.extend(options)
      // prevent record.options._Ctor from being overwritten accidentally
      newCtor.options._Ctor = record.options._Ctor
      record.Ctor.options = newCtor.options
      record.Ctor.cid = newCtor.cid
      record.Ctor.prototype = newCtor.prototype
      if (newCtor.release) {
        // temporary global mixin strategy used in < 2.0.0-alpha.6
        newCtor.release()
      }
    } else {
      updateOptions(record.options, options)
    }

修改方法:keep-alive组件缓存 key的取法 加上 (componentOptions.Ctor as any).cid

const key = vnode.key
        ? (componentOptions.Ctor as any).cid + (componentOptions.tag ? `::${componentOptions.tag}` : "")
        : vnode.key + (componentOptions.Ctor as any).cid;

组件全局覆盖,伪代码:

const cmpt = Vue.component("KeepAlive"); // 混入原生组件,重写render
delete (cmpt as any).mounted;
const newKeepAlive= Vue.extend({
  name: "keep-alive",
  mixins: [cmpt],
 
  render() {
    // override
 }

export default function install(vue){
  process.env.NODE_ENV === "develop" && vue.component("keep-alive",newKeepAlive)
}

@sodatea 大佬能修复下吗,没搞过pr

nailfar avatar Mar 20 '20 07:03 nailfar

Workaround: disable keep-alive for debug. Warning: view component must have name

<template>
    <keep-alive :exclude="exclude">
      <router-view></router-view>
    </keep-alive>
</template>
<script>
export default {
  computed: {
    exclude () {
      if (process.env.NODE_ENV === 'production') {
        return ''
      }
      return /.+/
    }
  }
}
</script>

it's working!👍

syoueicc avatar Apr 30 '20 06:04 syoueicc

@nailfar for vue 2.6, i did some work around, this will require name to be set in component:

import Vue from "vue"
/*
* https://github.com/vuejs/vue-loader/issues/1332#issuecomment-601572625
*/
function isDef(v) {
    return v !== undefined && v !== null
}
function isAsyncPlaceholder(node) {
    return node.isComment && node.asyncFactory
}
function getFirstComponentChild(children) {
    if (Array.isArray(children)) {
    for (var i = 0; i < children.length; i++) {
        var c = children[i]
        if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
        return c
        }
    }
    }
}
function getComponentName(opts) {
    return opts && (opts.Ctor.options.name || opts.tag)
}
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 remove(arr, item) {
    if (arr.length) {
    var index = arr.indexOf(item)
    if (index > -1) {
        return arr.splice(index, 1)
    }
    }
}
function pruneCacheEntry(cache, key, keys, current) {
    var cached$$1 = cache[key]
    if (cached$$1 && (!current || cached$$1.tag !== current.tag)) {
    cached$$1.componentInstance.$destroy()
    }
    cache[key] = null
    remove(keys, key)
}
function pruneCache(keepAliveInstance, filter) {
    var cache = keepAliveInstance.cache
    var keys = keepAliveInstance.keys
    var _vnode = keepAliveInstance._vnode
    const cachedNameKeyMap = keepAliveInstance.cachedNameKeyMap
    for (var key in cache) {
    var cachedNode = cache[key]
    if (cachedNode) {
        var name = getComponentName(cachedNode.componentOptions)
        if (name && !filter(name)) {
        delete cachedNameKeyMap[name]
        pruneCacheEntry(cache, key, keys, _vnode)
        }
    }
    }
}
const patternTypes = [String, RegExp, Array]
const KeepAlive = {
  name: "keep-alive",
  abstract: true,

  props: {
    include: patternTypes,
    exclude: patternTypes,
    max: [String, Number],
  },

  created() {
    this.cache = Object.create(null)
    this.cachedNameKeyMap = 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, cachedNameKeyMap, 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 old component for hmr
        if (name && cachedNameKeyMap[name] && cachedNameKeyMap[name] !== key) {
          pruneCacheEntry(cache, cachedNameKeyMap[name], keys)
        }
        cachedNameKeyMap[name] = 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])
  },
}
// ovveride original keep-alive
process.env.NODE_ENV === "development" && Vue.component("KeepAlive", KeepAlive)

ericwu-wish avatar May 27 '20 03:05 ericwu-wish

For me, this only happens when I'm adding new line / deleting existing line to the template. If I'm modifying existing line, the hot reload works fine.

SuspiciousLookingOwl avatar Jul 07 '20 19:07 SuspiciousLookingOwl

The newline issue is tracked here: https://github.com/vuejs/vue-loader/issues/1682

sodatea avatar Jul 08 '20 00:07 sodatea

@nailfar & @ericwu-wish thank you so much for the direction here. I would have been completely lost without the callout regarding componentOptions.Ctor.cid.

The solution I landed on is pretty much on par with what @nailfar wrote up which I tested and have verified in multiple scenarios.

To make this easier I've created a plugin which will resolve the issue for anyone interested: https://www.npmjs.com/package/vue-keep-alive-dev

Though, honestly, I wonder if there's really much harm in appending the cid to the cache globally as it stays stationary in line with the component even when HMR isn't running (though I may be missing something) so I'm going to throw out a PR to Vue for consideration.

dwatts3624 avatar Jul 13 '20 13:07 dwatts3624

I refreshing each time.Very annoying.

wuyuweixin avatar Sep 22 '20 11:09 wuyuweixin

Hope it can be fixed soon.

wuyuweixin avatar Sep 22 '20 11:09 wuyuweixin

@sodatea hao dalao,when fix it?

mingyuyuyu avatar Oct 03 '20 04:10 mingyuyuyu

its so unfriendly

mingyuyuyu avatar Oct 03 '20 06:10 mingyuyuyu

Update: I hear this is a non issue on the more granular Webpack 5... so I'll just focus on upgrading to that. Though if I need this now, I'll look on getting rid of the dynamic ID on the router-view and use props, vuex and better state management instead.


I need a better workaround...

  • Is there a way to detect HMR?
  • Can we reload keep-alive with a button instead?
  • I have video playing in a destroyed keep alive ghost element... wth, maybe that's a clue on how to fix it. To clarify, HMR killed my keep-alive video element... it's not in the dom from what I can tell from the inspector, but the audio is still playing through my speakers...

I really can't disable keep-alive as it's critical to letting the browser know I've interacted with the page. And the runtime behavior of the app is completely different without keep alive.

FossPrime avatar Apr 29 '21 05:04 FossPrime

Any update ?

DoveAz avatar Jun 07 '21 08:06 DoveAz

Add cid comparison.

import Vue from "vue";

let patternTypes = [String, RegExp, Array];

function pruneCacheEntry(cache, key, keys, current) {
    let cached$$1 = cache[key];
    if (cached$$1 && (!current || cached$$1.tag !== current.tag)) {
        cached$$1.componentInstance.$destroy();
    }
    cache[key] = null;
    remove(keys, key);
}

function pruneCache(keepAliveInstance, filter) {
    let cache = keepAliveInstance.cache;
    let keys = keepAliveInstance.keys;
    let _vnode = keepAliveInstance._vnode;
    for (let key in cache) {
        let cachedNode = cache[key];
        if (cachedNode) {
            let name = getComponentName(cachedNode.componentOptions);
            if (name && !filter(name)) {
                pruneCacheEntry(cache, key, keys, _vnode);
            }
        }
    }
}

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 isDef(v) {
    return v !== undefined && v !== null
}

/**
 * Remove an item from an array.
 */
function remove(arr, item) {
    if (arr.length) {
        var index = arr.indexOf(item);
        if (index > -1) {
            return arr.splice(index, 1)
        }
    }
}

function getFirstComponentChild(children) {
    if (Array.isArray(children)) {
        for (let i = 0; i < children.length; i++) {
            let c = children[i];
            if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
                return c
            }
        }
    }
}

function getComponentName(opts) {
    return opts && (opts.Ctor.options.name || opts.tag)
}

const keepAlive = {
    name: 'keep-alive',
    abstract: true,

    props: {
        include: patternTypes,
        exclude: patternTypes,
        max: [String, Number]
    },

    created: function created() {
        this.cache = Object.create(null);
        this.keys = [];
        this.$emit('getInstance', this);
    },

    destroyed: function destroyed() {
        for (let key in this.cache) {
            pruneCacheEntry(this.cache, key, this.keys);
        }
    },

    mounted: function mounted() {
        let this$1 = this;

        this.$watch('include', function (val) {
            pruneCache(this$1, function (name) {
                return matches(val, name);
            });
        });
        this.$watch('exclude', function (val) {
            pruneCache(this$1, function (name) {
                return !matches(val, name);
            });
        });
    },

    render: function render() {
        let slot = this.$slots.default;
        let vnode = getFirstComponentChild(slot);
        let componentOptions = vnode && vnode.componentOptions;
        if (componentOptions) {
            if (componentOptions.Ctor) {
                vnode._cid = componentOptions.Ctor.cid;//记录cid
            }
            // check pattern
            let name = getComponentName(componentOptions);
            let ref = this;
            let include = ref.include;
            let exclude = ref.exclude;
            if (
                // not included
                (include && (!name || !matches(include, name))) ||
                // excluded
                (exclude && name && matches(exclude, name))
            ) {
                return vnode
            }

            let ref$1 = this;
            let cache = ref$1.cache;
            let keys = ref$1.keys;
            let 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]) {
                //判断cid是否相同, 不同则有过热重载的reload, 需要重建缓存
                if (vnode._cid === cache[key]._cid) {
                    vnode.componentInstance = cache[key].componentInstance;
                    // make current key freshest
                    remove(keys, key);
                    keys.push(key);
                } else {
                    cache[key].componentInstance.$destroy();
                    cache[key] = vnode;
                }

            } 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])
    }
};
//只在开发模式下生效
if (process.env.NODE_ENV === "development") {
    Vue.component('keep-alive', keepAlive);
}


beishuangzz avatar Jun 10 '21 01:06 beishuangzz

Add cid comparison.

import Vue from "vue";

let patternTypes = [String, RegExp, Array];

function pruneCacheEntry(cache, key, keys, current) {
    let cached$$1 = cache[key];
    if (cached$$1 && (!current || cached$$1.tag !== current.tag)) {
        cached$$1.componentInstance.$destroy();
    }
    cache[key] = null;
    remove(keys, key);
}

function pruneCache(keepAliveInstance, filter) {
    let cache = keepAliveInstance.cache;
    let keys = keepAliveInstance.keys;
    let _vnode = keepAliveInstance._vnode;
    for (let key in cache) {
        let cachedNode = cache[key];
        if (cachedNode) {
            let name = getComponentName(cachedNode.componentOptions);
            if (name && !filter(name)) {
                pruneCacheEntry(cache, key, keys, _vnode);
            }
        }
    }
}

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 isDef(v) {
    return v !== undefined && v !== null
}

/**
 * Remove an item from an array.
 */
function remove(arr, item) {
    if (arr.length) {
        var index = arr.indexOf(item);
        if (index > -1) {
            return arr.splice(index, 1)
        }
    }
}

function getFirstComponentChild(children) {
    if (Array.isArray(children)) {
        for (let i = 0; i < children.length; i++) {
            let c = children[i];
            if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
                return c
            }
        }
    }
}

function getComponentName(opts) {
    return opts && (opts.Ctor.options.name || opts.tag)
}

const keepAlive = {
    name: 'keep-alive',
    abstract: true,

    props: {
        include: patternTypes,
        exclude: patternTypes,
        max: [String, Number]
    },

    created: function created() {
        this.cache = Object.create(null);
        this.keys = [];
        this.$emit('getInstance', this);
    },

    destroyed: function destroyed() {
        for (let key in this.cache) {
            pruneCacheEntry(this.cache, key, this.keys);
        }
    },

    mounted: function mounted() {
        let this$1 = this;

        this.$watch('include', function (val) {
            pruneCache(this$1, function (name) {
                return matches(val, name);
            });
        });
        this.$watch('exclude', function (val) {
            pruneCache(this$1, function (name) {
                return !matches(val, name);
            });
        });
    },

    render: function render() {
        let slot = this.$slots.default;
        let vnode = getFirstComponentChild(slot);
        let componentOptions = vnode && vnode.componentOptions;
        if (componentOptions) {
            if (componentOptions.Ctor) {
                vnode._cid = componentOptions.Ctor.cid;//记录cid
            }
            // check pattern
            let name = getComponentName(componentOptions);
            let ref = this;
            let include = ref.include;
            let exclude = ref.exclude;
            if (
                // not included
                (include && (!name || !matches(include, name))) ||
                // excluded
                (exclude && name && matches(exclude, name))
            ) {
                return vnode
            }

            let ref$1 = this;
            let cache = ref$1.cache;
            let keys = ref$1.keys;
            let 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]) {
                //判断cid是否相同, 不同则有过热重载的reload, 需要重建缓存
                if (vnode._cid === cache[key]._cid) {
                    vnode.componentInstance = cache[key].componentInstance;
                    // make current key freshest
                    remove(keys, key);
                    keys.push(key);
                } else {
                    cache[key].componentInstance.$destroy();
                    cache[key] = vnode;
                }

            } 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])
    }
};
//只在开发模式下生效
if (process.env.NODE_ENV === "development") {
    Vue.component('keep-alive', keepAlive);
}

最好不要粘贴大段的代码,尽量把代码分段解释。

alexxiyang avatar Jun 25 '21 17:06 alexxiyang

@sodatea Is it possible to fix this in Vue 2? Or at least for Vue 3?

rightaway avatar Mar 07 '22 16:03 rightaway

use transition。eg: <transition mode="out-in"> <keep-alive :include="cacheView"> <router-view :key="key"></router-view> </keep-alive> </transition>

mzongx avatar Mar 21 '24 06:03 mzongx