Construcción y Consumo de APIs con Vue.js y Vuex
Clase 16 de 27 • Curso Avanzado de Vue.js 2
En esta vista vamos a trabajar con muchos componentes que a su vez tienen más componentes hijos. Tenemos los componentes divididos en 2 grandes bloques:
- Bloque Principal (MainBlock), que a su vez contiene:
- Top Héroes: los tres últimos héroes del juego con los que el usuario ha jugado
- Listado de héroes: listado restante de héroes.
- Progreso de actos: si hemos completado la historia o campaña del juego.
- Estadísticas generales: élites, nivel de leyenda, etc.
- Tiempo de jugado: porcentaje de tiempo jugado por héroe.
- Bloque Artesanos (ArtisansBlock): información de los artesanos.
En la mayoría de los casos no nos detendremos a comentar los componentes que vayamos creando, puesto que son bastante sencillos. Reciben unas props (que serán los datos de la API) y los pintaremos en pantalla. Habrán algunos casos específicos que sí comentaremos.
Si retomamos una de las lecturas anteriores, nos habíamos quedado en que al hacer el submit del formulario en la página principal, no nos hacía el cambio de ruta porque dicha ruta no existía. Ahora la ruta ya existe y si haces el envío del formulario, debería llevarte a la vista de Profile:
Los pasos a seguir son los siguientes:
- Recuperar los datos a través de la ruta (URL): región y battleTag.
- Llamar a la API oficial de Diablo III para que nos devuelva los datos del jugador de esa región
- Si hay error, redirigir a la página de error y mostrar el mensaje de error
- Si no hay error, mostrar los datos a través de los componentes que vamos a ir creando
Parámetros de Ruta
Si recuerdas, a nuestra ruta Profile la definimos de la siguiente forma:
{ path: '/region/:region/profile/:battleTag', name: 'Profile' },
En las Vue DevTools del navegador, hay una pestaña en la cual podemos ver la información de todas las rutas.
¿No te parece genial? Ahora, para ver los datos de la ruta (route object), no vas a tener que estar haciendo inspecciones usando console.log
en los componentes. Utilizando las Developer Tools te será mucho más cómodo inspeccionar las rutas y ver su configuración.
> 📗 Documentación oficial del objeto enrutador (route object), con el que vamos a trabajar ahora: https://router.vuejs.org/api/#the-route-object
En la segunda imagen, en el bloque de la derecha, vemos dicho route object, con los siguientes valores:
path: "/region/eu/profile/SuperRambo-2613" fullPath: "/region/eu/profile/SuperRambo-2613" params: Object battleTag: "SuperRambo-2613" region: "eu" name: "Profile" matched: Array[1] 0: Object path: "/region/:region/profile/:battleTag"
Nos vamos a fijar en el bloque params
, ahí es donde están definidos los parámetros que nos interesan de nuestra ruta.
Como bien dice la documentación, desde un componente cualquiera podemos acceder a este objeto de ruta a través de this.$route
.
El siguiente paso es hacer la llamada a la API del juego correspondiente pasándole los parámetros que acabamos de capturar del path de la ruta.
Para ello, en el hook created
de nuestro componente, vamos a crear una función que se encargue de llamar a la API y que nos devuelva una promesa, que en caso de éxito nos devuelva los datos correspondientes y en caso de error nos devuelva un mensaje, y, si es posible, el código de error.
El endpoint al que vamos a apuntar desde nuestra web para que nos devuelva los datos, lo encontramos aquí (https://develop.battle.net/documentation/diablo-3/community-apis) en el bloque de D3 Profile API / getApiAccount:
// views/Profile/Index.vue export default { name: 'ProfileView', created () { // this.$route.params -> { region: "eu", battleTag: "SuperRambo-2613" } this.fetchData() }, methods: { fetchData () { // Llamada API } } }
Para hacer la llamada a la API vamos a necesitar varias cosas:
- El token de acceso, que lo tenemos guardado en el Store de nuestra app.
- El BattleTag del usuario.
- La región en la cual se encuentra dicho usuario.
- El locale o lenguaje en el que queremos que nos devuelva los datos. Esto lo dejaremos fijo, es decir, cada región tendrá asociado un locale por defecto, a través de una funcionalidad (muy sencilla) que explicaremos más abajo.
La llamada a la API de Blizzard la vamos a hacer desde el fichero llamado search.js
, que hemos creado anteriormente dentro de /api
. Tendrá el siguiente contenido:
// /api/search.js // Axios para hacer la llamada HTTP import { get } from 'axios' // Store, donde tenemos almacenado nuestro token import store from '../store/index' // Útil de regiones, que nos devolverá el 'locale' por defecto correspondiente a cada región import { locales } from '../utils/regions' // API URL // https://{region}.api.blizzard.com, donde {region} puede ser 'us', 'eu', 'kr' o 'tw' const protocol = 'https://' const host = '.api.blizzard.com/'
> 📗 Puedes ver la lista de locales disponibles en este enlace (https://develop.battle.net/documentation/guides/regionality-and-apis), en el apartado de Region Host List.
Continuamos, ahora sí, con el proceso de creación de nuestra función encargada de llamar a la API. La llamaremos getApiAccount
, al igual que se la llama en la documentación para que haya correspondencia entre nuestras funciones y la documentación de la API.
// /api/search.js /** * Returns the specified account profile. * GET – /d3/profile/{account} * Los parámetros que hemos obtenido a través de la URL * - @param region {String} * - @param account {String} * @returns {Promise} */ function getApiAccount ({ region, account }) { // Recurso de la API al que queremos acceder const resource = `d3/profile/${account}/` // API URL completa const API_URL = `${protocol}${region}${host}${resource}` // Locale const locale = locales[region] // Parámetros: // - Token de acceso (recuperado desde Vuex) // - Locale const params = { access_token: store.state.oauth.accessToken, locale } // Retornamos el resultado de hacer la llamada a la API, es decir, una promesa // que controlaremos (éxito / error) desde el componente return get(API_URL, { params }) }
Lo último que tenemos que hacer para poder usar esta funcionalidad desde nuestro componente Vue, es exponerla para que pueda ser importada desde fuera. Esto lo hacemos de la siguiente forma:
export { getApiAccount }
El código completo de /api/search.js
se vería así:
import { get } from 'axios' import store from '../store/index' import { locales } from '../utils/regions' // https://{region}.api.blizzard.com, where {region} is one of us | eu | kr | tw const protocol = 'https://' const host = '.api.blizzard.com/' /** * Returns the specified account profile. * GET – /d3/profile/{account} * @param region {String} * @param account {String} * @returns {Promise} */ function getApiAccount ({ region, account }) { const resource = `d3/profile/${account}/` const API_URL = `${protocol}${region}${host}${resource}` const locale = locales[region] const params = { access_token: store.state.oauth.accessToken, locale } return get(API_URL, { params }) } export { getApiAccount }
Con esto creado, ya podemos hacer la llamada a la API desde nuestro componente y controlar si sale por error o por éxito.
Volvemos al componente principal de Profile en /views/Profile/Index.vue
y desde nuestro método fetchData
llamamos a esta función. Recuerda que, para usarla, tienes que importarla primero.
// /Profile/Index.vue import { getApiAccount } from '@/api/search' export default { name: 'ProfileView', created () { this.fetchData() }, methods: { fetchData () { // Desestructuración const { region, battleTag: account } = this.$route.params // Llamada a la API con los datos necesarios getApiAccount({ region, account }) .then() .catch() } } }
La llamada a getApiAccount
, que recibe como parámetro un objeto con region
y account
como claves, nos va a devolver una promesa que puede terminar exitosamente (then) o con error (catch):
- Éxito: guardamos los datos en una variable local al componente. Con esto ya podemos propagar los datos (props) por los componentes hijos que vayamos creando.
- Error: guardar respuesta de error en el Store, cambiar de ruta a vista de Error y recuperar los datos.
Mixins
Lo que vamos a hacer, en caso de que nuestra llamada a la API nos devuelva un error, es guardar dicho error en el Store y hacer un cambio de ruta a la página de Error.
Esta funcionalidad de guardar datos que se mostrarán en la pág. de error, la podemos usar cada vez que llamemos a una API y nos salte un error. Por lo tanto, en vez de duplicar código a través de varios componentes, lo que vamos a hacer es crear un mixin que contenga dicha funcionalidad. De esta forma, cuando queramos utilizar esta funcionalidad, estaremos reutilizando código en vez de duplicarlo.
> 📗 Documentación a mixins: https://es.vuejs.org/v2/guide/mixins.html
Para crear nuestro mixin nos vamos a la carpeta /mixins
de nuestro proyecto y creamos un nuevo fichero llamado, por ejemplo, setError.js
.
Este fichero va a interactuar con el Store de nuestra app, por lo que antes de continuar, necesitamos crear dicho código en el módulo Vuex correspondiente de nuestra aplicación.
Como vamos a establecer el objeto de error que nos devuelva la API, podemos crear el fichero con el nombre de error.js
, dentro de nuestro directorio de Vuex: /store/modules
. Deberías tener creado el fichero en esta ruta: /store/modules/error.js
.
El contenido que va a tener es muy simple, una variable error que por defecto sea null
y una función (mutación) que se encargue de mutar el valor de error.
// /store/modules/error.js export default { namespaced: true, state: { error: null }, mutations: { SET_ERROR (state, payload) { state.error = payload } } }
Cuando tengamos un error, llamamos a SET_ERROR
y le pasamos por parámetro el objeto. Cuando queramos limpiar el error, podemos pasarle null
como parámetro.
Ahora queda darlo de alta en el Store de nuestra app, y para ello nos vamos una carpeta atrás /store
, al fichero index.js
y le añadimos el módulo que acabamos de crear.
import Vue from 'vue' import Vuex from 'vuex' import oauth from './modules/oauth' import loading from './modules/loading' + import error from './modules/error' Vue.use(Vuex) export default new Vuex.Store({ modules: { oauth, loading, + error } })
Con esto, como ya sabes, lo tenemos disponible desde cualquier parte de nuestra app. Volvamos a retomar el tema de los mixins, en concreto nuestro archivo setError.js
.
Creo que el nombre del fichero te da una pista de que es lo que tenemos que hacer, ¿no?
// /mixins/setError.js import { mapMutations } from 'vuex' export default { methods: { ...mapMutations('error', { setError: 'SET_ERROR' }), /** * API response error. * @param params {Object || null} Error Object */ setApiErr (params) { this.setError(params) } } }
Para trabajar con las mutaciones de nuestro Store en nuestros componentes, Vuex nos ofrece un elegante método que nos permite mapear el nombre de nuestra mutación (del Store) con una función en nuestro componente.
> 📗 Documentación completa de mapMutations.
Para hacer uso de este método, lo primero que tenemos que hacer es importarlo desde Vuex.
Su uso es muy sencillo. En este ejemplo, mapMutations
recibe 2 argumentos:
- El primer argumento es el bloque al que hacemos referencia, en este caso, lleva por nombre
error
. - El segundo argumento es un objeto con las funciones que queremos mapear, es decir, le hemos dicho que nuestra mutación
SET_ERROR
, se convierta en un método de nombresetError
en nuestro componente. De esta forma podemos usarlo como un componente local con la sintaxis de siemprethis.setError()
.
Lo que hemos hecho es crear un mixin, es decir, código que puede ser reutilizado. Dentro de este código (que va a formar parte de un componente) estamos exponiendo un método (methods) que se llama setApiErr
, que recibe un argumento por parámetro.
Lo único que hace este método es llamar a nuestra función mapeada de Vuex, que en realidad es como si estuviéramos llamando la mutación del Store SET_ERROR
.
Es decir, desde un componente (en el cual tengamos importado el mixin) podemos llamar al método setApiErr()
, como si fuera un método local más de nuestro componente.
Ya tenemos implementado un mixin que se comunica con el Store de nuestra aplicación. Solo nos queda usarlo.
Ahora ya tenemos nuestra funcionalidad Vuex y nuestro mixin listo para usarse. Es hora de volver a nuestra vista Profile y retomar la llamada a la API. Nos quedaría algo así:
// /views/Profile/Index.vue // Traemos el mixin import setError from '@/mixins/setError' import { getApiAccount } from '@/api/search' export default { name: 'ProfileView', // Lo damos de alta mixins: [ setError ], data () { return { profileData: null } }, created () { // llamada a la API this.fetchData() }, methods: { fetchData () { const { region, battleTag: account } = this.$route.params getApiAccount({ region, account }) .then(({ data }) => { // Guardamos los datos en una variable local this.profileData = data }) .catch((err) => { this.profileData = null // Creamos el objeto error const errObj = { routeParams: this.$route.params, message: err.message } if (err.response) { errObj.data = err.response.data errObj.status = err.response.status } // Hacemos uso del Mixin // Guardamos el objeto en el Store this.setApiErr(errObj) // Navegamos a la ruta de nombre 'Error' this.$router.push({ name: 'Error' }) }) } } }
Si hacemos la prueba con el usuario SuperRambo#2613
y con la región EU
, hacemos el envío del formulario y vamos al navegador, deberíamos ver algo como esto:
Ya tenemos todo lo necesario para pintar nuestra pantalla de Profile. Vamos a ir creando los componentes necesarios y les iremos pasando la información necesaria en forma de props.
Dentro del bloque principal (MainBlock) vamos a posicionar nuestro contenido con CSS Grid Layout y con las clases de que nos proporciona Bootstrap.
> 📗 CSS Grid está fuera del alcance de este curso, pero como buen front-end developer deberías tener conocimientos para saber cómo funciona y poder entenderlo.
> Si no conoces Grid, esta guía te puede ser de ayuda: https://css-tricks.com/snippets/css/complete-guide-grid/
Al navegar a la ruta Profile estamos haciendo una llamada a una API. Sería lógico mostrar un mensaje de 'Loading' mientras obtenemos los datos.
Seguramente recuerdes que tenemos un componente Loading que nos encaja a la perfección en este momento. Vamos a traerlo y a usarlo.
Lo primero que tenemos que hacer es importarlo desde /components
. Lo segundo es darlo de alta dentro del bloque components
de nuestro componente vista Profile. Por último, solo nos queda usarlo.
import BaseLoading from '@/components/BaseLoading'
components: { BaseLoading }
<BaseLoading/>
Para controlar el componente BaseLoading vamos a crear una nueva variable llamada isLoading
dentro de nuestras variables. En el hook created
vamos a igualar esta variable a true
y cuando la llamada a la API termine, la igualaremos a false
.
Veamos como queda el código completo hasta ahora.
<template> <div> <BaseLoading v-if="isLoading"/> <h1>Profile View</h1> </div> </template> <script> import BaseLoading from '@/components/BaseLoading' import setError from '@/mixins/setError' import { getApiAccount } from '@/api/search' export default { name: 'ProfileView', mixins: [ setError ], components: { BaseLoading }, data () { return { isLoading: false, profileData: null } }, created () { this.isLoading = true const { region, battleTag: account } = this.$route.params this.fetchData(region, account) }, methods: { /** * Obtener los datos de la API * Guardarlos en 'profileData' */ fetchData (region, account) { getApiAccount({ region, account }) .then(({ data }) => { this.profileData = data }) .catch((err) => { this.profileData = null const errObj = { routeParams: this.$route.params, message: err.message } if (err.response) { errObj.data = err.response.data errObj.status = err.response.status } this.setApiErr(errObj) this.$router.push({ name: 'Error' }) }) .finally(() => { this.isLoading = false }) } } } </script>
Ya tenemos el loading funcionando. Es hora de crear los componentes que muestren por pantalla datos reales de la API. ¡Lo veremos en la siguiente lectura!