Add a progress bar for Vue's lazy loading

Author: Stack Abuse

Crazy technology house

https://stackabuse.com/lazy-l...

Reprinting without permission is strictly prohibited

brief introduction

Usually Vue JS when writing a single page application (SPA), when loading the page, all necessary resources (such as JavaScript and CSS files) will be loaded together. This can lead to a poor user experience when dealing with large files.

With Webpack, you can use the import() function instead of the import keyword in Vue JS to load pages on demand.

Why load on demand?

Vue. The typical working mode of SPA in JS is to package and deliver all functions and resources together, so that users can use your application without refreshing the page. If you don't have a clear design for your application in order to load pages on demand, all pages will be loaded immediately, or use a lot of memory for unnecessary preloading in advance.

This is very unfavorable for large spas with many pages, which will lead to poor user experience with low-end mobile phones and low network speed. If you load on demand, users will not need to download resources they don't currently need.

Vue.js does not provide any load indicator related controls for dynamic modules. Even if prefetching and preloading are carried out, there is no corresponding space for users to know the loading process, so it is also necessary to improve the user experience by adding a progress bar.

Preparation project

First, you need a way for the progress bar to communicate with Vue Router. The event bus mode is appropriate.

The event bus is a singleton of a Vue instance. Since all Vue instances have an event system using $on and $emit, it can be used to deliver events anywhere in the application.

First, create a new file eventhub. Com in the components directory js:

import Vue from 'vue'
export default new Vue()

Then configure Webpack to disable prefetching and preloading, so that such operations can be performed separately for each function. Of course, you can disable it globally. Create a Vue in the root folder config. JS file and add relevant configurations to disable prefetching and preloading:

module.exports = {
    chainWebpack: (config) => {
        // Disable prefetching and preloading
        config.plugins.delete('prefetch')
        config.plugins.delete('preload')
    },
}

Add routes and pages

Install Vue router with npx and use:

$ npx vue add router

The edit is located in router / index JS and update the route so that the import() function can be used instead of the import statement:

The following default configurations:

import About from '../views/About.vue'
{
    path: '/about',
    name: 'About',
    component: About
},

Replace it with:

{
    path: '/about',
    name: 'About',
    component: () => import('../views/About.vue')
},

If you want to choose to load some pages on demand instead of disabling prefetching and preloading globally, you can use special Webpack comments, not in Vue config. Configure Webpack in JS:

import(
    /* webpackPrefetch: true */
    /* webpackPreload: true */
    '../views/About.vue'
)

The main difference between import() and import is that the ES module loaded by import() is loaded at runtime and the ES module loaded by import is loaded at compile time. This means that you can delay the loading of the module with import() and load it only if necessary.

Implementation progress bar

Because we can't accurately estimate the loading time (or full loading) of the page, we can't really create a progress bar. There is no way to check how many pages have been loaded. However, you can create a progress bar and make it complete when the page loads.

Because it can't really reflect the progress, the progress depicted is just a random jump.

Install lodash first Random, because this package will be used to generate some random numbers in the process of generating the progress bar:

$ npm i lodash.random

Then, create a Vue component components / ProgressBar vue:

<template>
    <div :class="{'loading-container': true, loading: isLoading, visible: isVisible}">
        <div class="loader" :style="{ width: progress + '%' }">
            <div class="light"></div>
        </div>
        <div class="glow"></div>
    </div>
</template>

Next, add a script to the component. In the script, first import random and $eventHub, which will be used later:

<script>
import random from 'lodash.random'
import $eventHub from '../components/eventHub'
</script>

After importing, define some variables to be used later in the script:

// It is assumed that the loading will be completed within this time.
const defaultDuration = 8000 
// update frequency
const defaultInterval = 1000 
// Value range: 0 - 1 How much progress will be increased in each time interval
const variation = 0.5 
// 0 - 100.  How much should the progress bar start.
const startingPoint = 0 
// Limit the distance from the progress bar until the loading is complete
const endingPoint = 90 

Then code the logic of asynchronous loading components:

export default {
    name: 'ProgressBar',
    
    data: () => ({
        isLoading: true, // After loading, it begins to disappear gradually
        isVisible: false, // After the animation is complete, set display: none
        progress: startingPoint,
        timeoutId: undefined,
    }),

    mounted() {
        $eventHub.$on('asyncComponentLoading', this.start)
        $eventHub.$on('asyncComponentLoaded', this.stop)
    },

    methods: {
        start() {
            this.isLoading = true
            this.isVisible = true
            this.progress = startingPoint
            this.loop()
        },

        loop() {
            if (this.timeoutId) {
                clearTimeout(this.timeoutId)
            }
            if (this.progress >= endingPoint) {
                return
            }
            const size = (endingPoint - startingPoint) / (defaultDuration / defaultInterval)
            const p = Math.round(this.progress + random(size * (1 - variation), size * (1 + variation)))
            this.progress = Math.min(p, endingPoint)
            this.timeoutId = setTimeout(
                this.loop,
                random(defaultInterval * (1 - variation), defaultInterval * (1 + variation))
            )
        },

        stop() {
            this.isLoading = false
            this.progress = 100
            clearTimeout(this.timeoutId)
            const self = this
            setTimeout(() => {
                if (!self.isLoading) {
                    self.isVisible = false
                }
            }, 200)
        },
    },
}

In the mounted() function, the event bus is used to listen for the loading of asynchronous components. Once the route tells us that we have navigated to a page that has not yet been loaded, it will start loading the animation.

Finally, add some styles:

<style scoped>
.loading-container {
    font-size: 0;
    position: fixed;
    top: 0;
    left: 0;
    height: 5px;
    width: 100%;
    opacity: 0;
    display: none;
    z-index: 100;
    transition: opacity 200;
}

.loading-container.visible {
    display: block;
}
.loading-container.loading {
    opacity: 1;
}

.loader {
    background: #23d6d6;
    display: inline-block;
    height: 100%;
    width: 50%;
    overflow: hidden;
    border-radius: 0 0 5px 0;
    transition: 200 width ease-out;
}

.loader > .light {
    float: right;
    height: 100%;
    width: 20%;
    background-image: linear-gradient(to right, #23d6d6, #29ffff, #23d6d6);
    animation: loading-animation 2s ease-in infinite;
}

.glow {
    display: inline-block;
    height: 100%;
    width: 30px;
    margin-left: -30px;
    border-radius: 0 0 5px 0;
    box-shadow: 0 0 10px #23d6d6;
}

@keyframes loading-animation {
    0% {
        margin-right: 100%;
    }
    50% {
        margin-right: 100%;
    }
    100% {
        margin-right: -10%;
    }
}
</style>

Finally, add ProgressBar to app Vue or layout component, as long as it is in the same component as the routing view, it can be used in the whole life cycle of the application:

<template>
    <div>
        <progress-bar></progress-bar>
        <router-view></router-view>
        <!--- Your other components -->
    </div>
</template>

<script>
import ProgressBar from './components/ProgressBar.vue'
export default {
       components: { ProgressBar },
}
</script>

Then you can see a smooth progress bar at the top of the page:

Trigger progress bar for delayed loading

The ProgressBar is now listening for asynchronous component loading events on the event bus. Animation should be triggered when some resources are loaded in this way. Now add a route daemon to the route to receive the following events:

import $eventHub from '../components/eventHub'

router.beforeEach((to, from, next) => {
    if (typeof to.matched[0]?.components.default === 'function') {
        $eventHub.$emit('asyncComponentLoading', to) // Start progress bar
    }
    next()
})

router.beforeResolve((to, from, next) => {
    $eventHub.$emit('asyncComponentLoaded') // Stop progress bar
    next()
})

In order to detect whether the page is loaded late, you need to check whether the component is defined as dynamic import, that is, it should be component: () = > Import ('...') instead of component:MyComponent.

This is through typeof to matched[0]?. components. Default = = = 'function' completed. Components with import statements are not classified as functions.

summary

In this article, we disabled prefetching and preloading in Vue applications and created a progress bar component that can be displayed to simulate the actual progress when loading pages.

The first wechat official account of this article: front-end pioneer

Welcome to scan the QR code to follow the official account, and push you fresh front-end technical articles every day

Welcome to continue reading other high praise articles in this column:

Tags: Javascript Front-end Vue.js

Posted by benrussell on Tue, 10 May 2022 17:02:05 +0300