Vue Router

15/27

Lectura

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 elemento routerOptions.
  • 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:

terminal-chunks

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
named-chunks

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):

home-chunks
Estamos en la vista Home, y aunque se han cargado las referencias de los tres chunks, realmente solo hemos cargado los datos de la vista Home. Si cambiamos de ruta y nos vamos a Error (http://localhost:8080/#/error) veremos lo siguiente:

error-chunks
Se ha cargado el fichero Error.js, el archivo tiene ese nombre porque se lo hemos indicado nosotros, tú puedes poner lo que quieras.

about-chunks
Si cambiamos de ruta a http://localhost:8080/#/about, verás que ahora ha cargado el último fragmento.

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

unnamed chunks
Mira cómo se ve en la pestaña network del navegador cuando no tienes los chunks nombrados. Es más difícil identificarlos.

Ahora ya sabes cómo hacer lazy loading de rutas en tus proyectos Vue. Ya podemos pasar a la siguiente lectura.

Aportes 11

Preguntas 0

Ordenar por:

¿Quieres ver más aportes, preguntas y respuestas de la comunidad? Crea una cuenta o inicia sesión.

De esta manera obtendrán el webpackChunkName de manera dinámica

// Rutas
const routes = routerOptions.map(r => {
  return {
    ...r,
    // Lazy load
    component: () => import(/* webpackChunkName: "[request]" */ `@/views/${r.name}/Index.vue`)
  }
})

Y al correr el comando yarn build quedará así:

File                                     Size             Gzipped

  dist/js/chunk-vendors.ad6a5d93.js        626.47 KiB       173.32 KiB
  dist/js/app.32173536.js                  9.89 KiB         3.93 KiB
  dist/js/Home-Index-vue.3792e385.js       1.04 KiB         0.56 KiB
  dist/js/Profile-Index-vue.9777fa54.js    0.50 KiB         0.33 KiB
  dist/js/Error-Index-vue.aae4ffcf.js      0.49 KiB         0.33 KiB
  dist/js/Hero-Index-vue.87ee5a10.js       0.48 KiB         0.33 KiB
  dist/js/About-Index-vue.bfa332ee.js      0.46 KiB         0.32 KiB
  dist/css/chunk-vendors.73152c73.css      237.67 KiB       31.77 KiB
  dist/css/app.e8df6455.css                3.95 KiB         0.99 KiB

Si tienes la versión actual de babel

"babel-eslint": "^10.1.0" 

Y te sale este error

Cannot read property 'range' of null

configura los rules en el package.json

"rules": {
      "template-curly-spacing": "off",
      "indent": [
        "error",
        2,
        {
          "ignoredNodes": [
            "TemplateLiteral"
          ]
        }
      ]
    },

de lo contrario tienes que cambiar a la versión que recomendaron al inicio.

Si alguno le aparece un error “TypeError: Cannot read property ‘range’ of null” es porque no ha hecho el la actualización de babel-eslint que se hace en la clase número 2.

“Actualiza la versión que tengas de babel-eslint a la 7.2.3. En mi caso, he pasado de la versión ^10.0.3 a la 7.2.3”

Es decir en el package json cambias la versión de babel y luego en la consola ejecutas el comando npm install

  "devDependencies": {
    /* muchas dependencias ....*/	
    "babel-eslint": "7.2.3", /* antes tenia la versión ^10.0.3*/
    /* y más dependencias ....*/	
  },

Muy pro esto de los imports dinámicos!!

Wow Wow man, este apartado me gustó, excelente, aprendí nuevas cosas… “Eres el puto amo, de las calles y garitos. Aquí todo es infinito cada palpito es un grito. Cada hazaña un hito cada creador un mito.”

El mes pasado escribí un artículo en CSS tricks, donde hablo de esto también: https://css-tricks.com/lazy-load-routes-in-vue-with-webpack-dynamic-comments/

🤯🤯 que buena clase, demasiado lo de los chunks y los import gracias🤯🤯

Que buen tema esto del lazy load y los chunks, aprendi varias cosas como el nombrar los chunks, siempre que veia ese comentario en los componentes pense que solo era para documentacion del codigo pero no sabia pera lo que realmente se utilizaba.

🤯🤯🤯🤯🤯
wow
Siempre me pregunté cómo optimizar el build. Gracias!!

Increíble, esta forma de cargar los componentes de las vistas me gustó porque practicamente te obliga a seguir el estandar de poner en una carpeta tu componente con nombre Index y que el nombre del componente debe ser el mismo que las rutas. Una desventaja obviamente es que no podemos ponerle nombre a los chunks de esta forma (¿O si?..)

Como sea, me gustó esta clase:D!

Aqui les dejo el import con el chuck name de todos los componentes

const routes = [
  {
    path: "/",
    name: "Home",
    component: () =>
      import(/* webpackChunkName: "Home" */ `@/views/Home/index.vue`),
  },
  {
    path: "/region/:region/profile/:battleTag",
    name: "Profile",
    component: () =>
      import(/* webpackChunkName: "Profile" */ `@/views/Profile/index.vue`),
  },
  {
    path: "/region/:region/profile/:battleTag/hero/:heroId",
    name: "Hero",
    component: () =>
      import(/* webpackChunkName: "Hero" */ `@/views/Hero/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`),
  },
  {
    path: "*",
    redirect: {
      name: "Home",
      component: () =>
        import(/* webpackChunkName: "Home" */ `@/views/Home/index.vue`),
    },
  },
];


En este caso cambie el nombre de routeroptions routes para dejar un solo array con toda la informacion