vuejs / vue

This is the repo for Vue 2. For Vue 3, go to https://github.com/vuejs/core
http://v2.vuejs.org
MIT License
207.74k stars 33.68k forks source link

Lazy Load for components #8106

Closed rhengles closed 6 years ago

rhengles commented 6 years ago

What problem does this feature solve?

I have a problem that, in the architecture of my apps, I want to load all the components of my application asynchronally. But in Vue, to declare a async component, I must define a constructor function for each one, thus I need to have a list of each and every component name that exists in my app, even if it isn't loaded.

This is how my first app works, you can see in this file, I have an array called componentsList:

https://www.portaldorevendedorraizen.com.br/js/index.js

I want to have a function, a Dynamic Component Factory, that receives the name of a component encountered in a template or render function, and tries to load this component dynamically. This is mostly useful with Async Components, which then tries to load the component definition over the network, and resolve with a component constructor.

I already have a modified version of Vue with these changes, and I successfully use this technique to build and ship three different web apps. Currently, this dynamic component factory is injected by a mixin into the $options object.

I want to define the lowest-level interface possible, designed to be implemented by a plugin which can be customized further with more options and provide a high-level interface.

What does the proposed API look like?

MyPlugin.install = function (Vue, options) {
  Vue.prototype._dynamicComponent = function (id) {
    // if the "id" matches some expected pattern,
    // return a component constructor - probably an async constructor.
    // otherwise, return nothing and the normal process continues
  }
}

Vue.use(MyPlugin, { someOption: true })
yyx990803 commented 6 years ago

I'm pretty sure you missed how async components work in Vue...

posva commented 6 years ago

Looking at your componentsList array, you can perfectly create a function that loads an array of components with names decided at runtime using webpack's import() (and probably others too). Vue can handle promises that return component descriptors as per https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components If you prefer that factory API, feel free to create a plugin that leverages Vue's async api and release it as a plugin 🙂 Closing as this is more of a question. BTW you can (and should) use the official forum and the discord server for questions

rhengles commented 6 years ago

Thank you for your attention, your time and your excellent work. However, this is not a question, and I sincerely do not understand why you frame It as such.

I do not want to use webpack, I already have my custom workflow which is much more light and easier to debug.

There is a fundamental issue here, and that is why I am posting this feature request. The current Async Component API demands that I have that Component name list. I do not want to have that Component name list.

I absolutely need a function in Vue that receives the name of the components needed in runtime and then I dinamically load that Component from my own name rules.

Please see this file for my current approach:

https://carrinho.oi.com.br/oiempresas/js/index.js

yyx990803 commented 6 years ago

Sorry but the more important question is why is this even necessary, which you didn't explain. You simply said this is absolutely necessary for you, but obviously it's not so necessary for others as this is the first time we get anyone asking for anything like this. So, unless you have a strong reason on why this could potentially be useful for others as well, we don't see a convincing reason to consider it.

rhengles commented 6 years ago

@yyx990803

Why is this even necessary?

In order to have an architecture of an app with hundreds of organized components which are loaded on demand, and this is the crucial part: without a build step. I understand that in a high traffic application, you need the optimization opportunities that a build step provides.

However, in my company case, and especially because of the back-end engineers, they want to be able to give maintenance and support to our shipped applications. We're short staffed with front-end engineers, só they leave only the heavy development for us. We have some applications that require constant changes because of business requirements, and they want to do them quickly.

They are used to the old way of doing things, where you had one page with some jquery commands, and they think they could deliver the necessary changes much faster back then, without a build step.

In our case, the performance of our applications is very satisfatory to our clients, but we need to deliver the changes faster, so our bottleneck is in our development team's time. But at the same time, we recognize that to develop a modern and complex application, but in an organized fashion, we need to break It down into components, defining a very clear separation of concerns.

The only way we can develop these components separately and deploy them without a build step is by loading them asynchronally on runtime. We've defined a naming pattern and a structure, a way in which our components must be created. With these rules, we can tell Vue how to load our components without having a pre-defined component name list, which, without a build step, would have to be manually mantained.

There is a reason I believe this is a simple change, and one that does not break backward compatibility - I'm simply defining one function which gives the app an opportunity to resolve a component constructor in runtime. It is really a shame that what I need to do really isn't possible to achieve without modificating the framework itself.

The API for constructors is pretty well established, it is the same for named components. So I think this really doesn't add any complexity to future updates in the framework itself, it is not a costly feature, It is designed with the absolute bare minimum requirements so I can build upon It.

fnlctrl commented 6 years ago

I do not want to have that Component name list

It's really simple to write a component loader that loads a component by id at runtime... https://jsfiddle.net/qe89fwru/1/

rhengles commented 6 years ago

@fnlctrl thank you for your answer, but considering a buildless architecture, there are two problems with that that I can think from the top of my head:

  1. It's not only a few components that will be loaded dynamically and asynchronally, it's every single one. So I'd have to replace every single direct component invocation with a dynamic loader. This is an implementation detail and it obscures unecessarily every component definition.

  2. Have you noticed that your dynamic loader example, without a build step, only works with render functions and not with templates? Can you expect back-end engineers to mantain and update that? Why shouldn't it be possible to support that with templates? (Spoiler: It is, we just need one function.)

fnlctrl commented 6 years ago

1.

It's not only a few components that will be loaded dynamically and asynchronally, it's every single one. So I'd have to replace every single direct component invocation with a dynamic loader

If your component were all dynamic, how do you give each invocation of them a different name? So you can't do <my-name/> <my-name-2/> or that would be the equivalent of a defined static component name list. So you must at least be using something like <component :is="..."/> which is the same syntax as what I've proposed.

  1. Have you noticed that your dynamic loader example, without a build step, only works with render functions and not with templates?

You can use templates, as long as you use a runtime+compiler build of vue. I'm just using that render function just for demo purpose. Demo using template: https://jsfiddle.net/fx61o9zq/

rhengles commented 6 years ago

1. If your component were all dynamic, how do you give each invocation of them a different name? So you can't do <my-name/> <my-name-2/> or that would mean a defined static component name list. So you must at least be using something like <componet is="..."/> which is the same syntax as what I've proposed.

Exactly! That is the beauty and elegance of my approach! I can do <my-name/> and <my-name-2/> simply by having a function that Vue calls when searching for a component constructor or definition.

Today, it already does this search, but it searches only for a named property on the component's $options.components object. If we have a function, Vue can call: Constructor = context.$options.componentLoader(id) or Constructor = context._componentLoader(id) and voilá! I no longer need a defined static component name list.

It really is awesome, you can check this working on these sites:

These are the necessary changes:

 // src/core/instance/lifecycle.js

@@ -41,6 +41,7 @@ export function initLifecycle (vm: Component) {

   vm._watcher = null
   vm._inactive = null
+  vm._componentLoader = noop;
   vm._directInactive = false
   vm._isMounted = false
   vm._isDestroyed = false

 // src/core/vdom/create-element.js

@@ -102,17 +102,23 @@ export function _createElement (
   let vnode, ns
   if (typeof tag === 'string') {
     let Ctor
     ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
     if (config.isReservedTag(tag)) {
       // platform built-in elements
       vnode = new VNode(
         config.parsePlatformTagName(tag), data, children,
         undefined, undefined, context
       )
-    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
+    } else if (isDef(Ctor =
+      resolveAsset(context.$options, 'components', tag) ||
+      context._componentLoader(tag)
+    )) {
       // component
       vnode = createComponent(Ctor, data, context, children, tag)
     } else {
       // unknown or unlisted namespaced elements
       // check at runtime because it may get assigned a namespace when its
       // parent normalizes children
       vnode = new VNode(
         tag, data, children,
         undefined, undefined, context
       )
     }
   } else {
     // direct component options / constructor

Then I'll use it like this:

MyPlugin.install = function (Vue, options) {
  let cache = options.cache || {}
  Vue.prototype._componentLoader = function (id) {
    // if the "id" matches some expected pattern,
    // return a component constructor - probably an async constructor.
    // otherwise, return nothing and the normal process continues
    var factory = cache[id]
    if (factory) {
      return factory
    }
    var prefix = options.prefix.toLowerCase()
    var plen = prefix.length
    if (id.substr(0, plen).toLowerCase() === prefix) {
      var path = id.substr(plen).replace(/--/g,'/')
      var last = path.lastIndexOf('/')
      last = path.substr(last+1)
      cache[id] = factory = getLoader(path, last)
      return factory
    }
  }
  function getLoader(path, last) {
    // here I load my component with ajax
    return function(resolve, reject) {
      var html, js
      var done = function done() {
        if (html && js) {
          js.template = html
          resolve(js)
        }
      }
      var href = options.baseUrl + path + '/' + last
      Utils.loadScript(href+'.js', function(err) {
        if (err) {
          return reject({
            message: 'Error loading component '+path+' script',
            error: err
          })
        }
        js = options.componentMap[path]
        done()
      })
      Utils.loadAjax({
        url: href+'.html',
        cb: function(err, response) {
          if (err) {
            return reject({
              message: 'Error loading component '+path+' template',
              error: err
            })
          }
          html = response
          done()
        }
      })
      Utils.loadStylesheet(href+'.css', function(err) {
        if (err) {
          console.log('Error loading stylesheet for component '+path)
          // CSS is optional and should not stop the component from being created
        }
      })
    }
  }
}

Vue.use(MyPlugin, {
  prefix: 'my-prefix--',
  baseUrl: myBaseUrl + 'components/',
  // each component defines the constructor in a global variable
  // this makes the script very easy to debug in the developer tools
  componentMap: MyGlobalVar.components
})

I have already spent too much time writing this, will answer the second point later. Thank you.

spevilgenius commented 6 years ago

I too have a similar need as my environment doesn't allow the use of node or webpack. I also have to support IE 11 (yuck!!) so I have reverted to storing all my templates as strings in a separate file. I tried to use http-vue-loader but that never worked for me.

CreativSpeed commented 6 years ago

warp your component with a function import,

example below: will load the component only if click (e) fired

<template>
    <div class="home">
        <button @click="toggler = true">Load Compnent</button>
        <img alt="Vue logo" src="../assets/logo.png">
        <HelloWorld v-if="toggler" msg="Welcome to Your Vue.js App"/>
</div>
</template>
<script>
    const HelloWorld = () => import('@/components/HelloWorld.vue')
    export default {
        name: 'home',
        data() {
            return {
                toggler: false
            }
        },
        components: {
            HelloWorld
    }
}
</script>
rhengles commented 6 years ago

@CreativSpeed

Thank you, but the question is not simply "Async Components" - these already exist. The problem is the need to have that function already registered before the component is loaded.

With my modifications from PR #8807, I can do exactly that - register and load the component only when it is called on a template.