Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loading asynchronously a non .vue component fails #1379

Closed
fpoliquin opened this issue Apr 26, 2017 · 19 comments
Closed

Loading asynchronously a non .vue component fails #1379

fpoliquin opened this issue Apr 26, 2017 · 19 comments

Comments

@fpoliquin
Copy link

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...

@LinusBorg
Copy link
Member

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.

@fpoliquin
Copy link
Author

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.

@yyx990803
Copy link
Member

*.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)

@fpoliquin
Copy link
Author

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.

@CKGrafico
Copy link

CKGrafico commented Jul 11, 2017

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)

@Ttou
Copy link

Ttou commented Jul 22, 2017

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

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

@JDrechsler
Copy link

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

@LinusBorg
Copy link
Member

LinusBorg commented Jul 27, 2017

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) } /

@JDrechsler
Copy link

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"

@JDrechsler
Copy link

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') })

@CKGrafico
Copy link

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 💃

@cybernetlab
Copy link

@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.

@CKGrafico
Copy link

I think that is not possible without babel now

@cybernetlab
Copy link

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

@tmacintosh
Copy link

tmacintosh commented Dec 5, 2017

@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?

@CKGrafico
Copy link

CKGrafico commented Dec 6, 2017

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

@mustafaekim
Copy link

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?

@CKGrafico
Copy link

@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

@mustafaekim
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants