vuejs / vue-loader

📦 Webpack loader for Vue.js components
MIT License
4.99k stars 913 forks source link

Hot reload doesn't drop the old component if it's wrapped in <keep-alive> #700

Open pevzi opened 7 years ago

pevzi commented 7 years ago

(not sure if I've chosen the appropriate bug tracker for this)

Steps to reproduce:

  1. Create an empty Vue project with the webpack template.
  2. Open App.vue and wrap the <hello> component in a <keep-alive>.
  3. Run the dev server.
  4. Modify the script section of Hello component (modifying the template doesn't trigger the issue).
  5. See Components tab in vue-devtools.

Expected behavior: the wrapped component is replaced with the new version. Observed behavior: the old version is kept alive, and each change results in an additional inactive component.

LinusBorg commented 7 years ago

Would you mind creating a small reproduction so we can be sure the behaviour can be replicated in isolation?

Thanks!

pevzi commented 7 years ago

Here is a small project based on webpack-simple template. Just do npm run dev and try to change the msg field in src/Foo.vue.

LinusBorg commented 7 years ago

Thanks!

ericwu-wish commented 4 years ago

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)
dwatts3624 commented 4 years ago

@ericwu-wish thanks for sharing the example! Being able to work around the HMR problem without disabling keep-alive entirely in dev would definitely shore up a ton of confusing code we've had to write inside components to determine if keep-alive is being used or not since we have different behaviors in certain cases within created/destroyed (e.g. maybe we only refresh a small amount of data when the component is activated).

Are you still using this? I tried implementing it and have confirmed that the code is running after some debugging but my components still disappear when HMR runs.

I took over this project which was setup using Laravel Mix so I suspect I need to dig into the way it's configuring webpack and the versions it's using but figured I'd verify that I should expect HMR to not wipe out my components on reload when using your overridden version of keep-alive here.

Any advice?

dwatts3624 commented 4 years ago

I posted this in #1332 but wanted to share here.

I ended up rolling the comments from @nailfar & @ericwu-wish into a plugin: https://www.npmjs.com/package/vue-keep-alive-dev

As mentioned on the other thread, I'm still wondering if it might be easier just to always append the value of componentOptions.Ctor.cid to the cache key. This is a lot of extra code for what is ultimately a small modification to a single line which doesn't have any negative affect in production.

Drumstix42 commented 4 years ago

It's a shame this issue still exists. I've just run into the bug as well. Do we know if this will be fixed/addressed in Vue 3?