Add a progress bar to Vue's lazy loading

Add a progress bar to Vue's lazy loading

Front-end Pioneer

Introduction


Usually when writing a single page application (SPA) with Vue.js, when the page is loaded, all necessary resources (such as JavaScript and CSS files) will be loaded together. This may cause a poor user experience when dealing with large files.

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

Why load on demand?


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

This is very detrimental to large SPAs with many pages, and will result in a poor user experience using low-end mobile phones and low Internet speeds. If it is loaded on demand, users will not need to download resources that they do not currently need.

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

Prepare the project


First, we need a way to let the progress bar communicate with Vue Router. The event bus mode is more 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.js in the components directory:


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

Then configure Webpack to disable prefetching and preloading, so you can perform such operations for each function individually, of course, you can also disable it globally. Create a vue.config.js file in the root folder and add related configurations to disable prefetching and preloading:


module.exports = {
    chainWebpack: (config) => {
        // 禁用预取和预加载
        config.plugins.delete('prefetch')
        config.plugins.delete('preload')
    },
}

Add routes and pages

Install Vue router with npx and use:


$ npx vue add router

Edit the router file located under router/index.js and update the route so that the import statement can be replaced with the import() function:

The following default configuration:


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

Change it to:


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

If you want to be able to choose to load certain pages on demand, instead of globally disabling prefetching and preloading, you can use special Webpack annotations and do not configure Webpack in vue.config.js:


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

The main difference between import() and import is that ES modules loaded by import() are loaded at runtime, and ES modules loaded by import are loaded at compile time. This means that import() can be used to delay the loading of modules and load them only when necessary.

Implementation progress bar


Since the page load time (or full load) cannot be accurately estimated, we cannot really create a progress bar. There is also no way to check how much the page has loaded. But you can create a progress bar and make it complete when the page loads.

Since the progress cannot be truly reflected, the progress depicted is just a random jump.

Install lodash.random first, because this package will be used to generate some random numbers during 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. First import random and $eventHub in the script, you will use it 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:


// 假设加载将在此时间内完成。
const defaultDuration = 8000 
// 更新频率
const defaultInterval = 1000 
// 取值范围 0 - 1. 每个时间间隔进度增长多少
const variation = 0.5 
// 0 - 100. 进度条应该从多少开始。
const startingPoint = 0 
// 限制进度条到达加载完成之前的距离
const endingPoint = 90

Then code to implement the logic of asynchronously loading components:


export default {
    name: 'ProgressBar',

    data: () => ({
        isLoading: true, // 加载完成后,开始逐渐消失
        isVisible: false, // 完成动画后,设置 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, use the event bus to listen to 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 to load the animation.

Finally it adds 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 the ProgressBar to App.vue or the layout component, as long as it is in the same component as the routing view, and it is available throughout the life cycle of the application:


<template>
    <div>
        <progress-bar></progress-bar>
        <router-view></router-view>
        <!--- 你的其它组件 -->
    </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:
Add a progress bar to Vue's lazy loading

Progress bar at the top of the page

Trigger a progress bar for delayed loading


Now ProgressBar is listening for asynchronous component loading events on the event bus. Animations should be triggered when certain resources are loaded in this way. Now add a router daemon to the router 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) // 启动进度条
    }
    next()
})

router.beforeResolve((to, from, next) => {
    $eventHub.$emit('asyncComponentLoaded') // 停止进度条
    next()
})

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

This is done by typeof to.matched[0]?.components.default ==='function'. Components with import statements will not be classified as functions.

to sum up


In this article, we disabled the prefetching and preloading functions in the Vue application, and created a progress bar component, which can be displayed to simulate the actual progress when the page is loaded.

Guess you like

Origin blog.51cto.com/15077562/2609636