Envoltura de Axios
En proyectos Vue.js, para la interacción con el backend y obtención de datos, generalmente utilizamos la biblioteca Axios. Esta es una biblioteca HTTP basada en promesas que puede ejecutarse tanto en el navegdaor como en Node.js. Posee excelentes características como interceptores de solicitud y respuesta, cancelación de solicitudes, conversión de JSON, defensa del cliente contra XSRF, entre otras. Por esta razón, el creador de Vue.js, Evan You, decidió dejar de mantener la biblioteca oficial vue-resource y recomendó directamente el uso de Axios.
Instalación
npm install axios; // Instalar Axios
Importación
Normalmente, en la carpeta src del proyecto, creamos una carpeta llamada request y dentro de esta, dos archivos: http.js y api.js. El archivo http.js se utiliza para encapsular Axios, mientras que api.js sirve para gestionar centraliz todas nuestras interfaces.
// Importar Axios en http.js
import axios from 'axios'; // Importar Axios
import QS from 'qs'; // Importar el módulo qs para serializar datos POST
// Componente de notificación Toast de Vant, puedes cambiarlo según tu biblioteca UI
import { Toast } from 'vant';
Cambio de Entorno
Nuestro proyecto puede tener diferentes entornos: desarrollo, pruebas y producción. Utilizamos las variables de entorno de Node.js para configurar el prefijo de URL de nuestras interfaces por defecto. La propiedad axios.defaults.baseURL nos permite establecer la URL base de las solicitudes.
// Cambio de entornos
if (process.env.NODE_ENV == 'development') {
axios.defaults.baseURL = 'https://www.example.com';}
else if (process.env.NODE_ENV == 'debug') {
axios.defaults.baseURL = 'https://test.example.com';
}
else if (process.env.NODE_ENV == 'production') {
axios.defaults.baseURL = 'https://api.example.com';
}
Configuración de Tiempo de Espera
Establecemos el tiempo de espera predeterminado de las solicitudes mediante axios.defaults.timeout. Si una solicitud supera este tiempo, se notificará al usuario que la solicitud ha expirado.
axios.defaults.timeout = 10000; // 10 segundos
Configuración de Encabezados POST
Para las solicitudes POST, necesitamos establecer un encabezado específico. Podemos configurarlo por defecto:
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
Interceptores de Solicitud
Podemos interceptar las solicitudes antes de enviarlas. Esto es útil para tareas como verificar si el usuario ha iniciado sesión antes de permitir el acceso a ciertas rutas, o para serializar datos en solicitudes POST.
// Importar Vuex para acceder al estado
import store from '@/store/index';
// Interceptor de solicitudes
axios.interceptors.request.use(
config => {
// Verificar si existe un token en Vuex
const token = store.state.token;
if (token) {
// Agregar el token al encabezado de la solicitud
config.headers.Authorization = token;
}
return config;
},
error => {
return Promise.reject(error);
});
Nota sobre el token: Generalmente, después de iniciar sesión, el token del usuario se almacena en localStorage o cookies. Cuando el usuario entra en la aplicación, se lee el token del almacenamiento local. Si existe, se actualiza el estado de token en Vuex. Cada solicitud a la API lleva el token en el encabezado, permitiendo al backend verificar si la sesión es válida. Si no se incluye el token, significa que el usuario no ha iniciado sesión.
Interceptores de Respuesta
// Interceptor de respuestas
axios.interceptors.response.use(
response => {
// Si el código de estado es 200, la solicitud fue exitosa
if (response.status === 200) {
return Promise.resolve(response);
} else {
return Promise.reject(response);
}
},
// Manejo de códigos de estado que no son 2xx
error => {
if (error.response.status) {
switch (error.response.status) {
// 401: No autenticado
case 401:
// Redirigir a la página de login
router.replace({
path: '/login',
query: {
redirect: router.currentRoute.fullPath
}
});
break;
// 403: Token expirado
case 403:
Toast({
message: 'La sesión ha expirado. Por favor, inicia sesión nuevamente',
duration: 1000,
forbidClick: true
});
// Limpiar token
localStorage.removeItem('token');
store.commit('loginSuccess', null);
// Redirigir a la página de login
setTimeout(() => {
router.replace({
path: '/login',
query: {
redirect: router.currentRoute.fullPath
}
});
}, 1000);
break;
// 404: Recurso no encontrado
case 404:
Toast({
message: 'El recurso solicitado no existe',
duration: 1500,
forbidClick: true
});
break;
// Otros errores
default:
Toast({
message: error.response.data.message,
duration: 1500,
forbidClick: true
});
}
return Promise.reject(error.response);
}
}
});
El interceptor de respuestas nos permite procesar los datos antes de que lleguen a nuestra aplicación. Por ejemplo, si el backend devuelve un código de estado 200, procesamos normalmente los datos; de lo contrario, manejamos los errores de manera uniforme.
Nota: El método Toast() utilizado es del componente de notificación de la biblioteca Vant. Deberás adaptarlo según la biblioteca UI que utilices en tu proyecto.
Métodos Encapsulados GET y POST
Los métodos comunes de AJAX son GET, POST, PUT, etc. Para simplificar nuestro código, encapsularemos estos métodos. A continuación, mostramos cómo encapsular GET y POST.
Método GET: Creamos una función get que toma dos parámetros: la URL de la solicitud y los parámetros opcionales. La función devuelve una promesa que se resuelve con los datos de la respuesta o se rechaza con el error.
/**
* Método GET para solicitudes GET
* @param {String} url - URL del endpoint
* @param {Object} params - Parámetros de la solicitud
*/
export function get(url, params){
return new Promise((resolve, reject) =>{
axios.get(url, {
params: params
}).then(res => {
resolve(res.data);
}).catch(err =>{
reject(err.data)
});
});
}
Método POST: Similar a GET, pero con la diferencia de que debemos serializar los datos enviados. Utilizamos el módulo qs para esta tarea.
/**
* Método POST para solicitudes POST
* @param {String} url - URL del endpoint
* @param {Object} params - Parámetros de la solicitud
*/
export function post(url, params) {
return new Promise((resolve, reject) => {
axios.post(url, QS.stringify(params))
.then(res => {
resolve(res.data);
})
.catch(err =>{
reject(err.data)
});
});
}
Nota importante: Existe una diferencia en cómo se pasan los parámetros entre axios.get() y axios.post(). En GET, el segundo parámetro es un objeto con una propiedad params, mientras que en POST, el segundo parámetro es directamente el objeto de parámetros.
Gestión Centralizada de API
Una API bien organizada es como un circuito impreso: incluso si es complejo, su estructura clara permite entenderlo fácilmente. Como mencionamos anteriormente, crearemos un archivo api.js para gestionar todas nuestras interfaces.
Primero, importamos los métodos encapsulados en api.js:
/**
* Gestión centralizada de API
*/
import { get, post } from './http'
Supongamos que tenemos un endpoint POST como este:
https://www.example.com/api/v1/users/my_address/address_edit_before
Podemos encapsularlo en api.js de la siguiente manera:
export const apiAddress = p => post('api/v1/users/my_address/address_edit_before', p);
Definimos un método apiAddress que toma un parámetro p (nuestros datos) y llama al método post que encapsulamos anteriormente. Finalmente, exportamos apiAddress.
En nuestros componentes, podemos llamar a esta API de esta manera:
import { apiAddress } from '@/request/api';
export default {
name: 'Direccion',
created () {
this.cargarDatos();
},
methods: {
cargarDatos() {
apiAddress({
tipo: 0,
orden: 1
}).then(res => {
// Operaciones con los datos recibidos
………………
})
}
}
}
Para otras interfaces, simplemente continuamos agregándolas en api.js. Importante: Asegúrate de documentar adecuadamente cada interfaz.
Ventajas de la gestión centralizada de API:
- Facilita la modificación de interfaces en un solo lugar
- Reduce el riesgo de errores al modificar código de negocio
- Mejora la organización del proyecto, especialmente en equipos grandes
Código Completo de la Envoltura de Axios
/**Envoltura de Axios
* Interceptores de solicitud y respuesta, manejo unificado de errores
*/
import axios from 'axios';
import QS from 'qs';
import { Toast } from 'vant';
import store from '../store/index';
// Configuración de entornos
if (process.env.NODE_ENV == 'development') {
axios.defaults.baseURL = '/api';
} else if (process.env.NODE_ENV == 'debug') {
axios.defaults.baseURL = '';
} else if (process.env.NODE_ENV == 'production') {
axios.defaults.baseURL = 'https://api.production.com/';
}
// Tiempo de espera de las solicitudes
axios.defaults.timeout = 10000;
// Encabezado para solicitudes POST
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
// Interceptor de solicitudes
axios.interceptors.request.use(
config => {
const token = store.state.token;
if (token) {
config.headers.Authorization = token;
}
return config;
},
error => {
return Promise.reject(error);
});
// Interceptor de respuestas
axios.interceptors.response.use(
response => {
if (response.status === 200) {
return Promise.resolve(response);
} else {
return Promise.reject(response);
}
},
error => {
if (error.response.status) {
switch (error.response.status) {
case 401:
router.replace({
path: '/login',
query: { redirect: router.currentRoute.fullPath }
});
break;
case 403:
Toast({
message: 'La sesión ha expirado. Por favor, inicia sesión nuevamente',
duration: 1000,
forbidClick: true
});
localStorage.removeItem('token');
store.commit('loginSuccess', null);
setTimeout(() => {
router.replace({
path: '/login',
query: { redirect: router.currentRoute.fullPath }
});
}, 1000);
break;
case 404:
Toast({
message: 'El recurso solicitado no existe',
duration: 1500,
forbidClick: true
});
break;
default:
Toast({
message: error.response.data.message,
duration: 1500,
forbidClick: true
});
}
return Promise.reject(error.response);
}
}
);
export default axios;
Mejoras Recientes en la Envoltura de Axios
Basado en diferentes necesidades, se han realizado las siguientes mejoras:
- Optimización de la envoltura de Axios: Se eliminaron los métodos GET y POST específicos, exportando directamente la instancia de Axios para mayor flexibilidad.
- Manejo de desconexión: Se agregó manejo para situaciones sin conexión a internet.
- Gestión más modular de API: Se reorganizó la estructura para facilitar el desarrollo en equipo.
- Múltiples dominios de API: Se permite configurar diferentes dominios para diferentes módulos.
- API en el prototipo de Vue: Se monta la API en Vue.prototype para evitar importaciones repetidas.
Estructura Mejorada del Proyecto
// api/index.js - Punto de salida centralizado de APIs
import articulos from './articulos';
import usuarios from './usuarios';
// ... otros módulos
export default {
articulos,
usuarios,
// ...
}
// api/base.js - Gestión de dominios de API
const dominios = {
principal: 'https://api.example.com/v1',
secundario: 'https://api2.example.com/v1'
}
export default dominios;
// api/articulos.js - APIs del módulo de artículos
import dominios from './base';
import http from '@/utils/http';
import qs from 'qs';
const articulos = {
lista() {
return http.get(`${dominios.principal}/articulos`);
},
detalle(id, params) {
return http.get(`${dominios.principal}/articulo/${id}`, {
params: params
});
},
crear(params) {
return http.post(`${dominios.principal}/articulos`, qs.stringify(params));
}
// ... otras APIs
}
export default articulos;
En main.js, montamos la API en el prototipo de Vue:
import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'
import api from './api'
Vue.prototype.$api = api;
Uso en componentes:
methods: {
cargarArticulo(id) {
this.$api.articulos.detalle(id, {
incluir: 'autor,comentarios'
}).then(response => {
// Procesar datos
})
}
}
Manejo de Desconexión
Para manejar situaciones de red, podemos crear un componente global que detecte la pérdida de conexión:
<template>
<div id="app">
<div v-if="!estaConectado">
<h3>No hay conexión a internet</h3>
<button @click="recargar">Recargar</button>
</div>
<router-view/>
</div>
</template>
<script>
import { mapState } from 'vuex';
export default {
name: 'App',
computed: {
...mapState(['estaConectado'])
},
methods: {
recargar() {
// Navegar a una página de recarga y volver
this.$router.replace('/recargar')
}
}
}
</script>
En el archivo de envoltura de Axios, actualizamos el estado de conexión cuando no hay internet:
// En el interceptor de errores
if (!window.navigator.onLine) {
store.commit('cambiarEstadoRed', false);
}
Y en el archivo recargar.vue:
beforeRouteEnter (to, from, next) {
next(vm => {
vm.$router.replace(from.fullPath)
})
}
Este enfoque proporciona una experiencia de usuario mejorada al notificar sobre problemas de red y permitir la recuperación sin recargar toda la aplicación.