Rutas de carga diferida con Vue Router con una barra de progreso

En este artículo, exploraremos cómo cargar rutas de forma diferida utilizando Vue Router para mejorar el rendimiento de la página, así como implementar una barra de progreso para visualizar el progreso de la carga.

Introducción

De forma predeterminada, al escribir una aplicación de página única (SPA) de Vue.js, todos los activos necesarios, como los archivos JavaScript y CSS, se cargan juntos cuando se carga la página. Cuando se trata de archivos de gran tamaño, esto puede conducir a una experiencia de usuario insatisfactoria.

Con la ayuda de Webpack, es posible cargar páginas bajo demanda en Vue.js usando la función import() en lugar de la palabra clave import.

¿Por qué cargar según demanda? {#por qué cargar bajo demanda}

Un SPA típico en Vue.js funciona al tener toda la funcionalidad y los activos empaquetados y entregados juntos para permitir que los usuarios usen la aplicación sin la necesidad de actualizar las páginas. Si no ha diseñado explícitamente la aplicación para cargar páginas bajo demanda, todas las páginas se cargarán a la vez, o se cargarán/recargarán previamente, utilizando un ancho de banda innecesario y ralentizando la carga de la página.

Esto lo hace especialmente malo para grandes SPA con muchas páginas. Las personas con una conexión a Internet lenta o dispositivos de gama baja, como teléfonos móviles, tendrían una mala experiencia de usuario. Al cargar a pedido, los usuarios nunca necesitarán descargar más de lo que necesitan.

Vue.js no viene con ningún indicador de carga para módulos dinámicos. Incluso con precarga y precarga, ningún indicador visual les permite a los usuarios saber cómo va la carga. También agregaremos una barra de progreso para mejorar la experiencia del usuario.

Preparando el Proyecto

Primero, necesitamos una forma de que nuestra barra de progreso se comunique con el enrutador Vue. Para ello, utilizaremos el Patrón de bus de eventos.

El bus de eventos es básicamente una instancia de Vue singleton. Dado que todas las instancias de Vue tienen un sistema de eventos que usa $on y $emit, podemos usarlo para pasar eventos en cualquier lugar de nuestra aplicación.

Vamos a crear un nuevo archivo, eventHub.js en el directorio components:

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

Ahora, configuraremos Webpack para deshabilitar la precarga y la precarga. Podemos hacer esto individualmente para cada función o deshabilitarlo globalmente. Cree un archivo vue.config.js en la carpeta raíz y agregue la configuración para deshabilitar la precarga y la precarga:

1
2
3
4
5
6
7
module.exports = {
    chainWebpack: (config) => {
        // Disable prefetching and preloading
        config.plugins.delete('prefetch')
        config.plugins.delete('preload')
    },
}

Adición de rutas y páginas

Usaremos Vue Router. Con ese fin, usaremos npx para instalarlo:

1
$ npx vue add router

Ahora, editemos nuestro archivo de enrutador, normalmente ubicado en router/index.js y actualicemos nuestras rutas para usar la función import(), en lugar de la instrucción import:

Esta es la configuración por defecto:

1
2
3
4
5
6
import About from '../views/About.vue'
{
    path: '/about',
    name: 'About',
    component: About
},

Lo hemos cambiado a:

1
2
3
4
5
{
    path: '/about',
    name: 'About',
    component: () => import('../views/About.vue')
},

Si prefiere seleccionar qué páginas cargar a pedido en lugar de deshabilitar la captación previa y la carga previa globalmente, use los comentarios especiales de Webpack en lugar de configurar Webpack en vue.config.js:

1
2
3
4
5
import(
    /* webpackPrefetch: true */
    /* webpackPreload: true */
    '../views/About.vue'
)

La principal diferencia entre import() e import es que los módulos ES cargados con import() se cargan en tiempo de ejecución, mientras que los cargados con import se cargan durante el tiempo de compilación. Esto significa que podemos diferir la carga de módulos con import() y cargar solo cuando sea necesario.

Implementación de la barra de progreso

Dado que es imposible estimar con precisión cuándo se cargará la página (o si se cargará), no podemos realmente hacer una barra de progreso. Tampoco hay forma de verificar cuánto se ha cargado la página. Lo que podemos hacer es crear una barra de progreso que termine cuando se cargue la página.

Todo lo que hay en el medio no refleja realmente el progreso, por lo que, en la mayoría de los casos, el progreso representado son solo saltos aleatorios.

Instalemos lodash.random primero, ya que usaremos ese paquete para seleccionar algunos números aleatorios durante la generación de la barra de progreso:

1
$ npm i lodash.random

Luego, creemos un componente Vue - components/ProgressBar.vue:

1
2
3
4
5
6
7
8
<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>

Ahora, a ese componente, agregaremos un script. Dentro de ese script, primero importaremos random y $eventHub, ya que los usaremos:

1
2
3
4
<script>
import random from 'lodash.random'
import $eventHub from '../components/eventHub'
</script>

Ahora, después de las importaciones, en el mismo script, podemos definir algunas variables que usaremos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Assume that loading will complete under this amount of time.
const defaultDuration = 8000 
// How frequently to update
const defaultInterval = 1000 
// 0 - 1. Add some variation to how much the bar will grow at each interval
const variation = 0.5 
// 0 - 100. Where the progress bar should start from.
const startingPoint = 0 
// Limiting how far the progress bar will get to before loading is complete
const endingPoint = 90 

Con eso en su lugar, escribamos la lógica para cargar los componentes de forma asíncrona:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
export default {
    name: 'ProgressBar',
    
    data: () => ({
        isLoading: true, // Once loading is done, start fading away
        isVisible: false, // Once animate finish, 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)
        },
    },
}

En la función mounted() verá que estamos haciendo uso del bus de eventos para escuchar la carga asíncrona de componentes. Comenzará la animación de carga una vez que el enrutador nos indique que hemos navegado a una página que aún no se ha cargado.

Y finalmente, agreguemos algo de estilo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<style scoped>
.loading-container {
    font-size: 0; /* remove space */
    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>

Ahora, agreguemos nuestra ProgressBar a nuestra App.vue o un componente de diseño siempre que esté en el mismo componente que la vista del enrutador. Queremos que esté disponible durante todo el ciclo de vida de la aplicación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<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>

Todo esto da como resultado una elegante barra de progreso, que se ve así:

progress bar

Activar barra de progreso para páginas con carga diferida

Nuestra ProgressBar está escuchando en el bus de eventos el evento de carga del componente asíncrono. Cuando algo se carga de esta manera, querremos activar la animación. Agreguemos un protector de ruta al enrutador para detectar estos eventos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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()
})

Para detectar si la página tiene carga diferida, debemos verificar si el componente está definido como una importación dinámica, es decir, component: () => import('...') en lugar de component: MyComponent.

Esto se hace con typeof to.matched[0]?.components.default === 'function'. Los componentes que se cargaron con la instrucción import no se clasificarán como funciones.

Conclusión

En este artículo, hemos explorado la necesidad de cargar de forma diferida ciertas páginas. Deshabilitamos la precarga y la precarga en nuestra aplicación Vue y creamos un componente de barra de progreso que aparece para simular el progreso real que se realiza al cargar una página.