vue-loader
vue-loader copied to clipboard
Hot reload problem when use keep-alive wrap router-view with a dynamic key
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
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
Does anyone have a work around?
Anyone? :)
Still nothing?
This still persists. Is everyone using keep-alive just refreshing each time still?
This still persists. Is everyone using keep-alive just refreshing each time still?
For the most part... sigh
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.
@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.
Any workaround?
The best workaround I found is
<nuxt :keep-alive="!isDev" />
where isDev = process.env.NODE_ENV === 'development'
haven't thought about it since.
Same problem.
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>
因为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
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!👍
@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)
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.
The newline issue is tracked here: https://github.com/vuejs/vue-loader/issues/1682
@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.
I refreshing each time.Very annoying.
Hope it can be fixed soon.
@sodatea hao dalao,when fix it?
its so unfriendly
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.
Any update ?
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);
}
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); }
最好不要粘贴大段的代码,尽量把代码分段解释。
@sodatea Is it possible to fix this in Vue 2? Or at least for Vue 3?
use transition。eg:
<transition mode="out-in"> <keep-alive :include="cacheView"> <router-view :key="key"></router-view> </keep-alive> </transition>