简体   繁体   中英

How to dynamically load components in routes

I'm a Vue newbie and I'm experimenting with vue-router and dynamic loading of components without using any additional libraries (so no webpack or similar).

I have created an index page and set up a router. When I first load the page I can see that subpage.js has not been loaded, and when I click the <router-link> I can see that the subpage.js file is loaded. However, the URL does not change, nor does the component appear.

This is what I have so far:

index.html

<html>
<head>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
</head>
<body>
    <div id="app">
      <h1>Hello App!</h1>
      <router-link to="/subpage">To subpage</router-link>
      <router-view></router-view>
    </div>
    <script src="main.js"></script>
</body>
</html>

main.js

const router = new VueRouter({
  routes: [
    { path: '/subpage', component: () => import('./subpage.js') }
  ]
})

const app = new Vue({
    router
}).$mount('#app');

subpage.js

export default {
    name: 'SubPage',
    template: '<div>SubPage path: {{msg}}</div>'
    data: function() {
        return {
            msg: this.$route.path
        }
    }
};

So the question boils down to: How can I dynamically load a component?

How can I dynamically load a component?

Try this:

App.vue

<template>
  <div id="app">
    <router-link to="/">Home</router-link>
    <router-link to="/about">About</router-link>
    <hr/>
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: 'app',
  components: {}
};
</script>

main.js

import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';

Vue.use(VueRouter);
Vue.config.productionTip = false;

const Home = () => import('./components/Home.vue');
const About = () => import('./components/About.vue');

const router = new VueRouter({
  mode: 'history',
  routes:[
    {path:'/', component: Home},
    {path:'/about',component: About}
  ]
})
new Vue({
  router,
  render: h => h(App)
}).$mount('#app');

Home.vue

<template>
  <div>
    <h2>Home</h2>
  </div>
</template>

<script>
export default {
  name: 'Home'
};
</script>

About.vue

<template>
  <div>
    <h2>About</h2>
  </div>
</template>

<script>
export default {
  name: 'About'
};
</script>

This way, the component Home will be automatically loaded.

This is the demo: https://codesandbox.io/s/48qw3x8mvx

I share your wishes for "as lean as possible" codebase and therefore made this simple example code below (also accessible at https://codesandbox.io/embed/64j8pypr4k ).

I am no Vue poweruser either, but when researching I have thought about three possibilities;

  • dynamic import s,
  • require js,
  • old school JS generated <script src /> include.

It looks like the last is the easiest and takes least effort too :D Probably not best practice and probably obsolete soon (at least affter dynamic import support).

NB: This example is friendly to more recent browsers (with native Promises, Fetch, Arrow functions...). So - use latest Chrome or Firefox to test :) Supporting older browsers may be done with some polyfills and refactoring etc. But it will add a lot to codebase...

So - dynamically loading components, on demand (and not included before):


index.html

<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="x-ua-compatible" content="ie=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Vue lazyload test</title>
  <style>
html,body{
  margin:5px;
  padding:0;
  font-family: sans-serif;
}

nav a{
  display:block;
  margin: 5px 0;
}

nav, main{
  border:1px solid;
  padding: 10px;
  margin-top:5px;
}

    .output {
        font-weight: bold;
    }

  </style>
</head>

<body>
    <div id="app">
    <nav>
      <router-link to="/">Home</router-link>
      <router-link to="/simple">Simple component</router-link>
      <router-link to="/complex">Not sooo simple component</router-link>
    </nav>
      <main>
          <router-view></router-view>
    </main>
    </div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/2.0.1/vue-router.min.js"></script>
<script>
    function loadComponent(componentName, path) {
      return new Promise(function(resolve, reject) {
        var script = document.createElement('script');

        script.src = path;
        script.async = true;

        script.onload = function() {
          var component = Vue.component(componentName);

          if (component) {
            resolve(component);
          } else {
            reject();
          }
        };
        script.onerror = reject;

        document.body.appendChild(script);
      });
    }

    var router = new VueRouter({
      mode: 'history',
      routes: [
        {
          path: '/',
          component: {
            template: '<div>Home page</div>'
          },
        },
        {
          path: '/simple',
          component: function(resolve, reject) {
            loadComponent('simple', 'simple.js').then(resolve, reject);
          }
        },
         { path: '/complex', component: function(resolve, reject) { loadComponent('complex', 'complex.js').then(resolve, reject); }
        }
      ]
    });

    var app = new Vue({
      el: '#app',
      router: router,
    });
</script>

</body>

</html>

simple.js :

Vue.component("simple", {
  template: "<div>Simple template page loaded from external file</div>"
});

complex.js :

Vue.component("complex", {
  template:
    "<div class='complex-content'>Complex template page loaded from external file<br /><br />SubPage path: <i>{{path}}</i><hr /><b>Externally loaded data with some delay:</b><br /> <span class='output' v-html='msg'></span></div>",
  data: function() {
    return {
      path: this.$route.path,
      msg: '<p style="color: yellow;">Please wait...</p>'
    };
  },
  methods: {
    fetchData() {
      var that = this;
      setTimeout(() => {
        /* a bit delay to simulate latency :D */
        fetch("https://jsonplaceholder.typicode.com/todos/1")
          .then(response => response.json())
          .then(json => {
            console.log(json);
            that.msg =
              '<p style="color: green;">' + JSON.stringify(json) + "</p>";
          })
          .catch(error => {
            console.log(error);
            that.msg =
              '<p style="color: red;">Error fetching: ' + error + "</p>";
          });
      }, 2000);
    }
  },
  created() {
    this.fetchData();
  }
});

As you can see - function loadComponent() does the "magic" thing of loading components here.

So it works, but it is probably not the best solution, with regards to the (at least) following:

  • inserting tags with JS can be treated as a security problem in the near future,
  • performance - synchronously loading files block the thread (this can be a major no-no later in app's life),
  • I did not test caching etc. Can be a real problem in production,
  • You loose the beauty of (Vue) components - like scoped css, html and JS that can be automatically bundled with Webpack or something,
  • You loose the Babel compilation/transpilation,
  • Hot Module Replacement (and state persistance etc) - gone, I believe,
  • I probably forgot about other problems that are obvious for senior-seniors :D

Hope I helped you though :D

I wanted to see how usefull are "new" dynamic imports today ( https://developers.google.com/web/updates/2017/11/dynamic-import ), so I did some experiments with it. They do make async imports way easier and below is my example code (no Webpack / Babel / just pure Chrome-friendly JS).

I will keep my old answer ( How to dynamically load components in routes ) for potential reference - loading scripts that way works in more browsers than dynamic imports do ( https://caniuse.com/#feat=es6-module-dynamic-import ).

So at the end I noticed that you were actually very, very, very close with your work - it was actually just a syntax error when exporting imported JS module (missing comma).

Example below was also working for me (unfortunately Codesandbox's (es)lint does not allow the syntax, but I have checked it locally and it worked (in Chrome, even Firefox does not like the syntax yet: (SyntaxError: the import keyword may only appear in a module) ));


index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" /> 
  <title>Page Title</title>
</head>

<body>
    <div id="app">
      <h1>Hello App!</h1>
      <router-link to="/temp">To temp</router-link>
      <router-link to="/module">To module</router-link>
      <router-view></router-view>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
    <script src="main.js"></script>
</body>
</html>

main.js:

'use strict';

const LazyRouteComponent = {
    template: '<div>Route:{{msg}}</div>',
    data: function() {
        return {
            msg: this.$route.path
        }
    }
}


const router = new VueRouter({
  routes: [
    {
        path: '/temp',
        component: {
            template: '<div>Hello temp: {{msg}}</div>',
            data: function() {
                return {
                    msg: this.$route.path
                }
            }
        }
    },
    { path: '/module', name: 'module', component:  () => import('./module.js')},
    { path: '*', component: LazyRouteComponent }
  ]
})

const app = new Vue({
    router
}).$mount('#app');

and the key difference, module.js :

export default {
    name: 'module',
    template: '<div>Test Module loaded ASYNC this.$route.path:{{msg}}</div>',
    data: function () {
       return {
          msg: this.$route.path
       }
    },
    mounted: function () {
      this.$nextTick(function () {
        console.log("entire view has been rendered after module loaded Async");
      })
    }
}

So - allmost exactly like your code - but with all the commas;

subpage.js

export default {
    name: 'SubPage',
    template: '<div>SubPage path: {{msg}}</div>',
    data: function() {
        return {
            msg: this.$route.path
        }
    }
};

So - your code works (i tested it by copy pasting) - you were actually just missing a comma after template: '<div>SubPage path: {{msg}}</div>' .

Nevertheless this only seems to work in:

  • Chrome >= v63
  • Chrome for Android >= v69
  • Safari >= v11.1
  • IOS Safari >= v11.2

( https://caniuse.com/#feat=es6-module-dynamic-import )...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM