Vue-router: Loading asynchronously a non .vue component fails

Created on 26 Apr 2017  ·  19Comments  ·  Source: vuejs/vue-router

Version

2.5.2

Reproduction link

https://github.com/fpoliquin/vue-bug-async.git

Steps to reproduce

Create a simple JavaScript or TypeScript component:

Async.ts

import Vue from 'vue'
import Component from 'vue-class-component'

@Component({
    template: '<p>Hello {{msg}}</p>'
})
export default class Async extends Vue {
    msg: 'world'
}

Try to load async:

Router.ts

const Async = resolve => require(['Async'], resolve)

export default new Router({
    mode: 'history',
    routes: [
        {
            path: '/async',
            component: Async
        }
    ...

What is expected?

The component should be mounted and operational after loading the webpack chunk

What is actually happening?

The console shows this error:

Failed to mount component: template or render function not defined.


I found a workaround by replacing a little piece of code :
From src/history/base.js line 341

          // save resolved on async factory in case it's used elsewhere
          def.resolved = typeof resolvedDef === 'function'
            ? resolvedDef
            : _Vue.extend(resolvedDef)
          match.components[key] = resolvedDef

To

          if (resolvedDef.default) {
            def.resolved = resolvedDef.default;
            match.components[key] = resolvedDef.default;
          } else {
            // save resolved on async factory in case it's used elsewhere
            def.resolved = typeof resolvedDef === 'function'
              ? resolvedDef
              : _Vue.extend(resolvedDef);
            match.components[key] = resolvedDef;
          }

This works for me but I am not sure if it is a good fix for everyone else...

Most helpful comment

What versions of vue-loader and vue-router are you using? vue-loader 13.0 introduced changes that required an upgrade of vue-router for this to work. With older versions of the router, you now have to do

{ path: '/forum', component: () => import('./components/Forum.vue').then(m => m.default) } /

All 19 comments

This is because you define an es6 export, but use commonjs require. In that case, you have to actually extract the component from the .default key, which would require to use the long form of the lazy load code instead of the shortcut form you used.

It should work as expected if you use es6 import style from webpack:

component: () => import('. /async.js')

Vue-loqder works around that problem, but because of this, vue files cannot have named exports. So this is a trade-off.

Thanks LinusBorg for your answer, this syntax does not compile in JS or TS with my setup. I granted you some access in this repo https://github.com/fpoliquin/vue-bug-async.git if you could show me a working example.

Thanks.

*.vue files normalizes the export for you automatically so you can resolve the imported value directly. For a normal ES module, you need resolve the .default yourself:

component: () => import('. /async.js').then(m => m.default)

Hi @yyx990803, thank you for your answer.

Typescript doesn't support this synthax yet so I had to change it a little bit like this :

component: = (resolve) => (require as any)(['./async'], function (module) {
    resolve(module.default)
})

And now it is working.

I don't know if it would be a good idea do update the documentation with this. I know I lost a lot of time figuring this out.

Thanks for your help.

Amazing @fpoliquin 💃💃if someone has problems I was exporting my modules without default:

app.router.ts

const ModelExplorerComponent = resolve => (require as any)(['./model-explorer/model-explorer.component'], module => resolve(module.default));

./model-explorer/model-explorer.component

export default class ModelExplorerComponent extends ToolComponent { }

And if you'te using webpack don't forget

output: {
  publicPath: 'js/'
},

(your path could be other)

set module to esnext in tsconfig.json, now you can use

component: () => import('. /async.js').then(m => m)

Hey guys,

I cannot get my configuration to work. I have the same problem as fpoliquin. If I want to lazy load my component I get this message: Failed to mount component: template or render function not defined.

If I use the normal way the routing is working.

{ path: '/forum', component: Forum} // is working
{ path: '/forum', component: () => import('./components/Forum.vue') } //is not working

I am almost sure that it has to do with my webpack configuration. I am using the config for quasar 0.14 so I can see changes on my phone as well which is very handy.

If anybody could help me or point me in a direction I would be very grateful. I uploaded my project on github:

https://github.com/JDrechsler/Problem-with-vue-router

Thank you

Johannes

What versions of vue-loader and vue-router are you using? vue-loader 13.0 introduced changes that required an upgrade of vue-router for this to work. With older versions of the router, you now have to do

{ path: '/forum', component: () => import('./components/Forum.vue').then(m => m.default) } /

Thank you very much! This actually did the trick and it is now working again.

I am using a version by Daniel Rosenwasser so I can use TypeScript in a nicer way.

"vue": "git+https://github.com/DanielRosenwasser/vue.git#540a38fb21adb7a7bc394c65e23e6cffb36cd867",
"vue-router": "git+https://github.com/DanielRosenwasser/vue-router.git#01b8593cf69c2a5077df45e37e2b24d95bf35ce3"
"vue-loader": "^12.2.2"

Now I can auto register my routes again. Thank you!

function load(compName: string) {
    return () => import(`./components/${compName}.vue`).then(m => m.default)
}

declare var compNamesFromWebpack: string[]

var routes: { path: string, component: () => Promise<any> }[] = []

compNamesFromWebpack.forEach(element => {
    var compName = element.replace('.vue', '')
    var compDomainName = `/${compName.toLocaleLowerCase()}`
    console.log(`Created route ${compDomainName}`)
    routes.push({ path: compDomainName, component: load(compName) })
});

routes.push({ path: '/', component: load('Home') })
routes.push({ path: '*', component: load('Error404') })

Finally I've the best solution, simply reading the official docs and your answers https://router.vuejs.org/en/advanced/lazy-loading.html

1.- tsconfig.json

{
  "compilerOptions": {
    "target": "es6",
    "lib": [
      "dom",
      "es2015",
      "es2016"
    ],
    "module": "esnext",
    "moduleResolution": "node",
    "isolatedModules": false,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "suppressImplicitAnyIndexErrors": true,
    "allowSyntheticDefaultImports": true,
    "sourceMap": true
  },
  "compileOnSave": false
}

2.- .babelrc

 {
    "presets": [
        "es2015"
    ],
    "plugins": [
        "babel-polyfill",
        "syntax-dynamic-import"
    ]
}

3.- webpack.config.js

output: {
            publicPath: 'scripts/'
 }

Note: For this take care about the name of the folder 'scripts' in my case is where I've the js transpiled ('./dist/scripts')

4.- Configure routes

path: '/cities',
name: 'cities',
component: () => import('./cities.component'),

Now is perfect :D 💃

@CKGrafico shoud it work without babel?

My tsconfig.json is:

{
  "compilerOptions": {
    "lib": ["dom", "es5", "es2015", "es2015.promise", "es2016"],
    "module": "esnext",
    "moduleResolution": "node",
    "target": "es6",
    "isolatedModules": false,
    "sourceMap": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "allowSyntheticDefaultImports": true,
    "suppressImplicitAnyIndexErrors": true,
    "outDir": "./dist/"
  }
}

and main index.ts:

import Vue from 'vue';
import VueRouter from 'vue-router';

Vue.use(VueRouter);

const App = () => import('./app').then(m => m.default);
const routes = [
  {path: '/', redirect: '/app'},
  {path: '/app', component: App},
];

const router = new VueRouter({routes});
...

And I get following error on compile:

ERROR in ./src/frontend/index.ts
(40,30): error TS2345: Argument of type '{ routes: ({ path: string; redirect: string; } | { path: string; component: () => Promise<typeof ...' is not assignable to parameter of type 'RouterOptions'.
  Types of property 'routes' are incompatible.
    Type '({ path: string; redirect: string; } | { path: string; component: () => Promise<typeof App>; ...' is not assignable to type 'RouteConfig[]'.
      Type '{ path: string; redirect: string; } | { path: string; component: () => Promise<typeof App>; }' is not assignable to type 'RouteConfig'.
        Type '{ path: string; component: () => Promise<typeof App>; }' is not assignable to type 'RouteConfig'.
          Types of property 'component' are incompatible.
            Type '() => Promise<typeof App>' is not assignable to type 'Component'.
              Type '() => Promise<typeof App>' has no properties in common with type 'ComponentOptions<Vue>'.

looks like typescript cannot resolve type of dynamically imported component.

I think that is not possible without babel now

@CKGrafico Thank you for a quick reply. I confirm that your solution works fine with babel

@cybernetlab @CKGrafico
I'm getting the same error as cybernetlab and I'm trying to include the babel-polyfill but still get the same error.

This is my setup:
tsconfig.json

{
    "compilerOptions": {
        "allowSyntheticDefaultImports": true,
        "experimentalDecorators": true,
        "module": "esnext",
        "moduleResolution": "node",
        "target": "es6",
        "sourceMap": true,
        "skipDefaultLibCheck": true,
        "noImplicitReturns ": true, 
        "noFallthroughCasesInSwitch ": null, 
        //"strict": true,
        "types": [ "webpack-env", "@types/googlemaps" ],
        "baseUrl": "./ClientApp",
        "lib": [ "dom", "es5", "es2015", "es2015.promise", "es2016" ],
        "isolatedModules": false,
        "emitDecoratorMetadata": true,
        "suppressImplicitAnyIndexErrors": true
    },
  "exclude": [
    "bin",
    "node_modules"
  ]
}

index.ts

import 'babel-polyfill';

Vue.use(VueRouter);

const HomeComponent = () => import('./components/home/home.vue.html').then(m => m.default);

const router = new VueRouter({
    mode: 'history',
    routes: [
        { path: '/', component: HomeComponent, meta: { title: 'Home' } }
    ]
});

var app = new Vue({
   router: router
})

home.vue.html

<template>
    <div>
        ....
    </div>
</template>
<script src="./home.ts"></script>

and home.ts

import Vue from 'vue';
import { Component } from 'vue-property-decorator';

@Component
export default class HomeComponent extends Vue {
...
}

webpack.config.js

entry: {
     'main': './ClientApp/index.ts' }
},
module: {
     rules: [
                { test: /\.vue\.html$/,
                    include: /ClientApp/,
                    loader: 'vue-loader',
                    options: {
                        loaders: {
                            js: 'awesome-typescript-loader?silent=true',
                            less: extractComponentLESS.extract({ use: isDevBuild ? ['css-loader', 'less-loader'] : ['css-loader?minimize', 'less-loader'] })
                        }
                    }
                },
                { test: /\.ts$/, include: /ClientApp/, use: 'awesome-typescript-loader?silent=true' },
                { test: /\.css(\?|$)/, use: extractMainCSS.extract({ use: isDevBuild ? 'css-loader' : 'css-loader?minimize' }) },
                { test: /\.less(\?|$)/, use: extractLESS.extract({ use: isDevBuild ? ['css-loader', 'less-loader'] : ['css-loader?minimize', 'less-loader'] }) },
                { test: /\.(png|jpg|jpeg|gif|svg)$/, use: [{ loader: 'url-loader', options: { limit: 25000, mimetype: 'image/png' } }] },
                { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "url-loader?limit=10000&mimetype=application/font-woff" },
                { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "file-loader" },
                { test: /\.vue$/, loader: 'vue-loader' }
            ]
        },

this is the error I'm getting...

ERROR in [at-loader] ./ClientApp/index.ts:66:36 
    TS2307: Cannot find module './components/home/home.vue.html'.
ERROR in [at-loader] ./ClientApp/index.ts:68:30 
    TS2345: Argument of type '{ mode: "history"; routes: ({ path: string; component: () => Promise<any>; meta: { title: string;...' is not assignable to parameter of type 'RouterOptions'.
  Types of property 'routes' are incompatible.
    Type '({ path: string; component: () => Promise<any>; meta: { title: string; }; } | { path: string; com...' is not assignable to type 'RouteConfig[]'.
      Type '{ path: string; component: () => Promise<any>; meta: { title: string; }; } | { path: string; comp...' is not assignable to type 'RouteConfig'.
        Type '{ path: string; component: () => Promise<any>; meta: { title: string; }; }' is not assignable to type 'RouteConfig'.
          Types of property 'component' are incompatible.
            Type '() => Promise<any>' is not assignable to type 'Component'.
              Type '() => Promise<any>' has no properties in common with type 'ComponentOptions<Vue>'.

Do I also need to add babel somewhere in my webpack.config.js? And am I supposed to create a .bablerc file? If so, how do you implement this file?? Or is it simply the way I have my components structured (having the .ts code residing in a separate file) is not compatible with lazy-loading?

I've a .vue + typescript boilerplate maybe it helps :) https://github.com/CKGrafico/Frontend-Boilerplates/tree/vue

My Vue project is written in Typescript and it uses commonjs as the module system. Can I still use lazy routes? I cannot move to esnext. How can I proceed?

@mustafaekim if you check the boilerplates is note really difficult, the idea is to compile to es6 using TS and after that add babel to compile to the modules you need

I cannot use esnext as the module system in tsconfig. And it seems like commonjs does not work well with dynamic imports.

Was this page helpful?
0 / 5 - 0 ratings