Vue Lazy load refactor - vue.js

I am trying to lazy load a lot of components in my app, I want to show a loading spinner while any of them is loading, and the same for error; so there is a lot of duplication.
export default c => ({
component: import(`${c}`),
loading: loadingComponent,
timeout: 3000
})
I am trying to refactor this into a single function and using it like that
import lazyload from './lazyload';
Collection: lazyload("./Collection.vue")
But webpack is not extracting the component as it normally does, I know that I am missing something.

You need to be creating an async component factory (meaning function). Also the import module cannot be fully dynamic, there needs to be some prefix to the module path otherwise it could match literally any module and webpack needs to know which subset of modules it could possibly match at runtime to include them in the build.
Fully dynamic statements, such as import(foo), will fail because webpack requires at least some file location information. This is because foo could potentially be any path to any file in your system or project. The import() must contain at least some information about where the module is located, so bundling can be limited to a specific directory or set of files.
I've made some adjustments to your code (untested):
lazyload.js
export default c => () => ({
component: import(`./components/${c}`),
loading: loadingComponent,
timeout: 3000
})
Example usage
import lazyload from './lazyload'
export default {
components: {
Collection: lazyload('collection.vue')
}
}
A better implementation, in my opinion, would be to avoid creating the dynamic import. I prefer webpack to know for certain which modules are definitely needed at build time instead of bundling together a subset of modules inside a directory.
lazyload.js
export default componentFn => () => ({
component: componentFn(),
loading: loadingComponent,
timeout: 3000
})
Example usage
import lazyload from './lazyload'
export default {
components: {
Collection: lazyload(() => import('./collection.vue'))
}
}
Now lazyload has no dependency on any specific component directory and can be used with any component.

Related

Dynamic import of component based on variable name in NextJS

I'm looking for a way to use a component in a dynamic way:
const getDynamicComponent = (componentName) => dynamic(() => import(${componentName}), {
ssr: false,
loading: () => <p>Loading...</p>,
});
const Test = () => {
const router = useRouter();
const { component } = router.query;
const DynamicComponent = getDynamicComponent(component);
return <DynamicComponent />
}
Obiovusly if I specify a folder name there like components/${componentName} it searches ALL components.. and not the one specified in the variable.
I know the documentation states explicitly that template strings are not an option for dynamic imports, but I would like to know how I would be able to import a specific component from node_modules, without importing all node_modules folder 🙂
Only static strings are permitted?
I have a dynamic route file as [componentId].js which should import its component from node_modules based on the route name.. any ideas?
Should I try to configure babel in a specific way to make this work?
Thanks!
PS: I know it was asked here, but those answers are not quite correct as the whole folder is trying to get imported, not just the component specified.

Vue dynamic import is being imported even if it is never used

I am building a vue plugin called vue-scan-field. At the previous version (0.1.2) the plugin only supported vuetify. Now I want to also add quasar to this plugin. So what I have decided to do is use dynamic imports for the components. For example the quasar file looks like this:
export default {
components: {
scan_text_field: () => import('quasar/src/components/input/QInput'),
scan_textarea: () => import('quasar/src/components/input/QInput'),
scan_checkbox: () => import('quasar/src/components/checkbox/QCheckbox'),
scan_select: () => import('quasar/src/components/select/QSelect')
},
...
}
And I also have this for vuetify:
export default {
components: {
scan_text_field: () => import('vuetify/lib/components/VTextField'),
scan_textarea: () => import('vuetify/lib/components/VTextarea'),
scan_checkbox: () => import('vuetify/lib/components/VCheckbox/VCheckbox'),
scan_select: () => import('vuetify/lib/components/VAutocomplete'),
},
...
}
Now download the module from npm and run my quasar project I get these errors:
These dependencies were not found:
* vuetify/lib/components in ./node_modules/vue-scan-field/dist/vue-scan-field.esm.js
* vuetify/lib/components/VAutocomplete in ./node_modules/vue-scan-field/dist/vue-scan-field.esm.js
* vuetify/lib/components/VCheckbox/VCheckbox in ./node_modules/vue-scan-field/dist/vue-scan-field.esm.js
* vuetify/lib/components/VTextField in ./node_modules/vue-scan-field/dist/vue-scan-field.esm.js
* vuetify/lib/components/VTextarea in ./node_modules/vue-scan-field/dist/vue-scan-field.esm.js
Now this can be because maybe my compiler imports the components on build. I don't know if I can safely disable this function (I don't think so). Is there any way to fix this?
NPM Package: https://www.npmjs.com/package/vue-scan-field
Github page: https://github.com/jessielaf/vue-scan-field
Dynamic components in Webpack (and in general) work because bundler at build time prepares js files (with component code) which are at runtime loaded (or not ...depending on logic) by the app. Bundler can not execute your code and evaluate the logic which decides which component should be loaded. It must prepare the budle for all possible outcomes.
So yes, of course your bundler looks for and bundle all the files even they are used in dynamic import.
I do not know exactly how to set up Rollup correctly but I'm pretty sure you don't want to include any Vuetify/Quasar code as part of your library. Such code would be duplicate and possibly from different version of the framework the user of your library is already using! So instead of using string configuration (framework: 'vuetify'), leave it to your users to pass the component mapping (and maybe add sample to your docs)...
// dynamic
Vue.use(ScanField, { components: {
scan_text_field: () => import('quasar/src/components/input/QInput'),
scan_textarea: () => import('quasar/src/components/input/QInput'),
scan_checkbox: () => import('quasar/src/components/checkbox/QCheckbox'),
scan_select: () => import('quasar/src/components/select/QSelect'),
scan_date: () => null
}})
// static
import QInput from 'quasar/src/components/input/QInput'
Vue.use(ScanField, { components: {
scan_text_field: QInput,
...
}})
Alternative solution is to use normal (not dynamic) imports, but configure Rollup so that both Vuetify and Quasar are treated as peerDependencies (dependency your lib requires but the user is responsible to add/install it into his project) - see the docs

Component dynamic import doesn't work from router.js

With Nuxt.js, in my router.js I'm trying to import my route component thus:
{
path: '/',
component: () => import(/* webpackChunkName: "index" */ '~/pages/index.vue')
}
I get this error:
render function or template not defined in component: anonymous
I came across someone else's Nuxt.js project where they add the following at the end, and with this it works:
{
path: '/',
component: () => import(/* webpackChunkName: "index" */ '~/pages/index.vue').then(m => m.default || m)
}
The returned object of my import() looks like this:
In a another Vue.js, non-Nuxt project, the same kind of import() looks like that:
In both cases, the component is nested in some "default" object. But with Nuxt.js it seems you must import that object explicitly, whereas with regular Vue.js you don't have to specify that.
Why?
Regarding why the module is nested inside a { default } object: it's because Nuxt uses ES6 style modules. ES6 style modules are written the following:
export default {
// some object
};
Default is not the only property you can have. More syntax.
Regarding why vue-router are working without the .default, it's because the code is more versatile than the code used by Nuxt. Nuxt already ships it's own builder and always uses ES6 modules.
On the other hand, vue-router does not know about how it will be built. So to be easier to use, vue-router automatically checks if the module is ES6 or not.
const resolve = once(resolvedDef => {
if (isESModule(resolvedDef)) {
resolvedDef = resolvedDef.default
}
// ...
}
Source: code of vue-router.
As you may already know, it's very uncommon to use a router.js file in a Nuxt project, because Nuxt already has its own routing system which splits your code into chunks for each page. Documentation here.

vue and webpack doesn't do lazy loading in components?

The lazy component in vue/webpack seem to be wrong or I miss confuse about the terms.
To do lazy loading of component I use the keyword import and webpack should split this component to sepeate bundle, and when I need to load this component webpack should take care of it and load the component.
but in fact, webpack does make sperate file, but it loaded anyway when the application is running. which is unexpected.
For example I just create a simple vue application (using the cli) and browse to localhost:8080/ and the about page should be loaded (by default) using the import keyword.
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
So This is by design? I load every time the file I do not need right now (the page about). and if I have 200 pages, when I'll fetch 200 files I dont need. how that ends up? that did I save here by using the import key?
In vuetify for example they uses import key, but the files are loaded anyway and not by demand.
You can also avoid component prefetch using one of the webpack "magic" comments (https://webpack.js.org/guides/code-splitting/#prefetchingpreloading-modules), eg.
components: {
MyComponent: () => import(/* webpackPrefetch: false */ './MyComponent.vue')
}
Feel free to read more about this Vue optimization below:
https://vueschool.io/articles/vuejs-tutorials/lazy-loading-individual-vue-components-and-prefetching/
If you're referring to the default app template from Vue CLI, then you're actually observing the prefetch of the About page, intended to reduce load times for pages the user will likely visit.
If you want to avoid this performance optimization, use this Vue config:
// vue.config.js
module.exports = {
chainWebpack: config => {
config.plugins.delete('prefetch')
config.plugins.delete('preload')
}
}
For troubleshooting reference, Chrome's Network panel includes an Initiator column, which provides a clickable link to the source file that triggered the network call. In this case of the about.js, the source file looks like this:
try using vue-lazyload maybe it can help and for <script> tags you can try async defer it helps in website speed optimizations

How to dynamically mock ES6 modules with SystemJS?

I have a single-page application written in ES6. The code in transpiled server-side into classic javascript by babelJs, then loaded by SystemJs.
Javascript present in my html file:
System.config({
baseURL: '/js',
meta: {
'/js/*': { format: 'cjs' }
}});
System.defaultJSExtensions = true;
System.import("index.js")
.catch(function (error) {
console.error(error)
});
index.js:
import f1 from 'file1';
import f2 from 'file2';
// code here ...
Everything works fine. index.js is loaded, and all import statements are correctly executed.
Now, I want to create some pages with mocked ES6 modules, for testing purpose. My goal is to display pages by replacing model classes (contained in ES6 modules) with other static test classes.
Let's say I have 3 files: real_model.js, fake_model.js and component.js. component.js import the real model (import Model from 'real_model';).
How can I replace the real model by the fake one (in the component) dynamically ?
It's been a while since this question was posted, but maybe this solution might still be of help to anyone else.
With SystemJS it is possible to create a module on-the-fly using System.newModule. Then you can use System.set to overwrite existing modules with the new one. In our tests we use the following helper function to mock existing modules:
function mockModule(name, value) {
const normalizedName = System.normalizeSync(name);
System.delete(normalizedName);
System.set(normalizedName, System.newModule(Object.assign({ default: value }, value)));
}
Then, e.g. inside the beforeEach callback, we assign the mock and then import the module to be tested using System.import:
let [component, fake_model] = [];
beforeEach(() => {
// define mock
fake_model = { foo: 'bar' };
// overwrite module with mock
mockModule('real_model', fake_model);
// delete and reimport module
System.delete(System.normalizeSync('component'));
return System.import('src/testing').then((m) => {
component = m.default;
}).catch(err => console.error(err));
});
// test your component here ...
A big advantage of this approach is that you don't need an additional mocking library and it works solely with SystemJS.