Rutas Dinámicas y Lazy Loading en Vue.js
Clase 15 de 27 • Curso Avanzado de Vue.js 2
Hasta ahora teníamos 2 rutas en nuestra app, la ruta de Home
y la de About
.
Vamos a crear el resto de rutas (like a pro) que nos van a hacer falta para nuestra app de Diablo III. Las rutas que necesitaremos son las siguientes:
- Profile: muestra los datos generales del usuario (BattleTag) en una región determinada.
- Hero: muestra los datos específicos de un héroe (HeroId) de un usuario (BattleTag) en una región determinada.
- Error: en caso de error, redireccionamos y mostramos el mensaje que nos devuelva la API.
La ruta de Hero
es la más divertida. A través de las APIs del juego de Diablo III, traeremos los stats del héroe, los objetos (¡con imágenes!) y sus habilidades. Hay muchos más datos que nos proporcionan las APIs del juego, pero que no vamos a usar por no expandirnos mucho con el curso.
> Si te gusta este tema, siempre puedes animarte a mejorar el proyecto original con alguna feature que creas que pueda estar bien 😉.
Antes de tocar las rutas, vamos a crear los componentes vista vacíos para poder trabajar luego con el fichero de rutas. Tenemos que crear las siguientes carpetas dentro de /views
:
/Profile
/Hero
/Error
Ahora hay que crear, dentro de cada una, un nuevo fichero llamado Index.vue
. Dicho componente va a ser el que renderice nuestra vista, y de momento le vamos a dar un contenido muy simple. Hasta ahora deberías tener esto dentro de /views
, aparte de lo que ya tenías:
📂 /views ├──📂 /Profile │ └── Index.vue ├──📂 /Hero │ └── Index.vue └──📂 /Error └── Index.vue
Vamos a dotar de contenido a nuestros recién creados archivos, para diferenciar nuestras vistas. Con un h1
nos vale:
/Profile/Index.vue
<template> <div> <h1>Profile View</h1> </div> </template> <script> export default { name: 'ProfileView' } </script>
/Hero/Index.vue
<template> <div> <h1>Hero View</h1> </div> </template> <script> export default { name: 'HeroView' } </script>
/Error/Index.vue
<template> <div> <h1>Error View</h1> </div> </template> <script> export default { name: 'ErrorView' } </script>
Ahora ya podemos trabajar con nuestras rutas.
Tenemos que pensar, antes de empezar a picar código, qué es lo que necesitamos para poder trabajar con nuestras rutas. Tenemos varios escenarios:
- Ruta Home, About y Error: No necesitan parámetros en la URL. Renderizan contenido estático. En el caso de la pág. de error, dinámico, pero no require de parámetros en la URL.
- Ruta Profile: Necesitamos la región del usuario y el identificador del usuario en la URL.
- Ruta Hero: Necesitamos lo mismo que en Profile más el identificador de héroe en la URL.
Ya tenemos identificados los posibles escenarios que podemos tener en nuestras rutas. Vamos a editar en nuestro fichero principal de rutas, es decir, /router/index.js
, para ir creando lo que nos hace falta.
En el fichero hay que borrar algunas cosas y crear otras que vamos a ir comentando aquí abajo:
// Importamos vue y vue-router import Vue from 'vue' import VueRouter from 'vue-router'
Esto se queda tal cual, necesitamos Vue y Vue-Router.
Borramos los import de los componentes vista que teníamos.
En esta ocasión, solo teníamos la vista Home. La vamos a borrar (ahora verás el porqué) junto con la variable routes
.
Vamos a crear una nueva variable llamada routerOptions
que va a ser un array de rutas con las opciones. Tiene el siguiente contenido:
// ... const routerOptions = [ { path: '/', name: 'Home' }, { path: '/region/:region/profile/:battleTag', name: 'Profile' }, { path: '/region/:region/profile/:battleTag/hero/:heroId', name: 'Hero' }, { path: '/about', name: 'About' }, { path: '/error', name: 'Error' }, { path: '*', redirect: { name: 'Home' } } ]
Ya hemos nombrado nuestras rutas, tal y como habíamos definido antes, y les hemos dado un path. Como ya sabes, los dos puntos (:
) en el path
hacen referencia a un parámetro o variable en la ruta.
El path, en el caso de las rutas Profile y Hero, es a gusto del consumidor.
Existen múltiples opciones a la hora de afrontar este problema, esta es solo una de ellas. Puedes cambiar si crees que hay otra solución mejor.
Por ejemplo, puedes optar por omitir /region
del path de tu ruta, y dejarlo de esta forma: /:region/profile/:battleTag
. O incluso, omitir /profile
y dejar solo los parámetros: /:region/:battleTag
.
> 📗 Puedes leer más acerca de vue-router en este link: https://router.vuejs.org/guide/essentials/dynamic-matching.html
Necesitamos saber la región, el identificador del usuario (que lo hemos llamado BattleTag) y el identificador del héroe. Ya lo tenemos definido en la ruta y podemos controlarlo desde nuestras vistas.
Ahora vamos a crear otra variable llamada routes
, que va a ser el resultado de recorrer routerOptions
con un .map()
, junto con la propiedad component
que vamos a adjuntarle. Con el código lo entenderás mejor.
const routes = routerOptions.map(r => { return { ...r, component: () => import(`@/views/${r.name}/Index.vue`) } })
Expliquemos esto. La variable routes
es un array de objetos, que va a tener el mismo número de elementos que routerOptions
. El contenido de dichos elementos va a ser un objeto primitivo que va a tener path
, name
y component
:
...r
: es decir, lo que tenía el propio elementorouterOptions
.component
: que en vez de hacer referencia al componente, vamos a cargar el componente de manera lazy load usando los import dinámicos de Webpack. Esto quiere decir que dividiremos nuestro código en pequeñas partes, y solo se cargará la parte correspondiente a la ruta que estemos visitando.
> Al crear aplicaciones con Vue, puede darse el caso de que nuestro código JavaScript sea bastante grande y, por lo tanto, que afecte al tiempo de carga de la página. > Sería más eficiente dividir los componentes de cada ruta en un fragmento separado, y solo cargarlos cuando se visita la ruta.
Esto es lo que en inglés se conoce como Lazy Load. Justo lo que estamos haciendo aquí.
Y además de una manera elegante usando la función map()
, ya que tenemos todas nuestras vistas cargadas bajo el mismo patrón: el name
de routerOptions
es el mismo que tenemos para agrupar las vistas por directorios, es decir, la ruta de nombre Profile
tiene asignado el componente /views/Profile/Index.vue
. Lo mismo para el resto de rutas.
Lo último sería instanciar el Router de Vue.
const router = new Router({ routes })
El código completo se vería así:
import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) // Configuración rutas const routerOptions = [ { path: '/', name: 'Home' }, { path: '/region/:region/profile/:battleTag', name: 'Profile' }, { path: '/region/:region/profile/:battleTag/hero/:heroId', name: 'Hero' }, { path: '/about', name: 'About' }, { path: '/error', name: 'Error' }, { path: '*', redirect: { name: 'Home' } } ] // Rutas const routes = routerOptions.map(r => { return { ...r, // Lazy load component: () => import(`@/views/${r.name}/Index.vue`) } }) const router = new Router({ routes }) export default router
En component
, en vez de referenciar el componente vista que queremos cargar, estamos llamando a una función que es la que se va a encargar de traer el componente cuando sea necesario. Con esto estamos mejorando el rendimiento de nuestra web app.
Vamos a ver en acción el posible impacto en el rendimiento de nuestra app. Para probarlo podemos hacer uso del comando npm run build
. Esto nos permite ver los chunks (o fragmentos) que se generan de nuestra app. A medida que la aplicación vaya creciendo, estos fragmentos irán siendo más grandes. Hagamos la prueba.
Desde la terminal, en la carpeta raíz del proyecto, ejecuta el siguiente comando: npm run build
. Una vez que haya terminado verás algo parecido a esto:
Los chunks son los fragmentos que el build ha creado en vez de hacer un sólo paquete con todo el proyecto.
Por lo cual, en el momento de la carga de la app, se irán cargando los bloques que se vayan necesitando en vez de cargar todo el paquete completo.
Wow! Esto está bastante bien, pero... ¿Cómo saber a qué parte de la aplicación corresponde cada chunk? Webpack nos da un método para saberlo.
En la función donde hacemos el import, podemos poner un comentario JavaScript /* Comentario JS */
indicándole el nombre que queremos que tenga ese chunk. Veámoslo en marcha:
// router.js - Ejemplo simplificado import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) const routes = [ { path: '/', name: 'Home', component: () => import(/* webpackChunkName: "Home" */ '@/views/Home/Index.vue') }, { path: '/about', name: 'About', component: () => import(/* webpackChunkName: "About" */ '@/views/About/Index.vue') }, { path: '/error', name: 'Error', component: () => import(/* webpackChunkName: "Error" */ '@/views/Error/Index.vue') } ] const router = new Router({ routes }) export default router
Si te fijas, ahora los chunks tienen un nombre aparte del hash. Tenemos los vendors (de librerías externas) de JS y CSS, el fichero app y el resto, que son los chunks nombrados. Muy útil a la hora de desarrollar, para tener controlado el tamaño de nuestros fragmentos.
Una cosa más; esto también lo podemos ver desde las DevTools del navegador, en la pestaña network
. Para esto tienes que tener el servidor de desarrollo levantado, es decir, tienes que ejecutar npm run serve
(o si usas yarn, yarn serve
):
Error.js
, el archivo tiene ese nombre porque se lo hemos indicado nosotros, tú puedes poner lo que quieras.
Esto nos permite repartir el peso de la app en múltiples trozos, en vez de tener uno extremadamente grande. Lo malo de tener un solo chunk es que probablemente no llegas a utilizar todo, y, sin embargo, estás consumiendo recursos para que la app lo cargue todo de golpe.
Quédate con este concepto de Lazy Loading en rutas, pues más adelante lo veremos también en componentes.
Vamos a dejar el router.js como estaba antes de que le agregáramos los comentarios de webpackChunkName:
import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) const routerOptions = [ { path: '/', name: 'Home' }, { path: '/region/:region/profile/:battleTag', name: 'Profile' }, { path: '/region/:region/profile/:battleTag/hero/:heroId', name: 'Hero' }, { path: '/about', name: 'About' }, { path: '/error', name: 'Error' }, { path: '*', redirect: { name: 'Home' } } ] // Rutas const routes = routerOptions.map(r => { return { ...r, // Lazy load component: () => import(`@/views/${r.name}/Index.vue`) } }) const router = new Router({ routes }) export default router
Ahora ya sabes cómo hacer lazy loading de rutas en tus proyectos Vue. Ya podemos pasar a la siguiente lectura.