El framework Vue sólo se ocupa de la capa de vista de la aplicación pero su “ecosistema” como sus creadores le llaman, incluye multitud de herramientas para todo lo que podamos necesitar a la hora de realizar grandes proyectos.
Una de las librerías más utilizadas es la que permite realizar de forma sencilla peticiones Ajax a un servidor. Existen múltiples librerías para ello y la más utilizada es axios.
Aunque podríamos hacer las peticiones Ajax como vimos en Javascript (con fetch y async/await) es más sencillo con axios. Axios ya devuelve los datos transformados a JSON en una propiedad llamada data.
Como esta librería vamos a usarla en producción la instalaremos como dependencia del proyecto:
npm install axios -S
En el componente en que vayamos a usarla la importaremos:
import axios from 'axios'
Como es una dependencia incluida en el package.json no se indica su ruta (se buscará en node-modules).
Ya podemos hacer peticiones Ajax en el componente. Para ello axios incluye los métodos:
Estos métodos devuelven una promesa por lo que al hacer la petición deberemos anteponerle el await o bien indicaremos con .then y .catch la función que se ejecutará cuando responda el servidor si la petición se resuelve correctamente y la que se ejecutará si ocurre algún error respectivamente.
Lo que devuelve es un objeto que tiene, entre otras, las propiedades:
data: aquí tendremos los datos devueltos por el servidor ya procesadosstatus: obtendremos el código de la respuesta del servidor (200, 404, …)statusText: el texto de la respuesta del servidor (‘Ok’, ‘Not found’, …)message: mensaje del servidor en caso de producirse un errorheaders: las cabeceras HTTP de la respuestaLa sintaxis de una petición GET a axios usando async/await sería algo como:
try {
const response = await axios.get(url)
console.log(response.data)
} catch (response) {
console.error(response.message)
}
y usando promesas sería algo como:
axios.get(url)
.then(response => console.log(response.data))
.catch(response => console.error(response.message))
Vamos a seguir con la aplicación de la lista de tareas pero ahora los datos no serán un array estático sino que estarán en un servidor. Usaremos como servidor para probar la aplicación json-server por lo que las peticiones serán a la URL ‘localhost:3000’ que es el servidor web de json-server.
Los cambios que debemos hacer en nuestra aplicación son:
fetchTodos(): que hará una petición GET para obtener todos los datos, ya que al pricipio el array de tareas estará vacíotoggleDone(id, done): que hará una petición PATCH para modificar el campo done de una tarea concreta ya que ahora debe modificarse en el servidorPuedes descargar el código del repositorio de GitHub.
import axios from 'axios';
const SERVER_URL = 'http://localhost:3000';
const fetchTodos = async () => {
const response = await axios.get(`${SERVER_URL}/todos`);
return response.data;
};
const addTodo = async (todo) => {
const response = await axios.post(`${SERVER_URL}/todos`, todo);
return response.data;
};
const removeTodo = async (todoId) => {
await axios.delete(`${SERVER_URL}/todos/${todoId}`);
};
const toggleTodoDone = async (todoId, done) => {
const response = await axios.patch(`${SERVER_URL}/todos/${todoId}`, { done });
return response.data;
}
export { fetchTodos, addTodo, removeTodo, toggleTodoDone };
import { reactive } from "vue";
import * as api from "../services/api";
export const store = {
debug: true,
state: reactive({
todos: [],
}),
async fetchTodosAction() {
if (this.debug) console.log("fetchTodosAction triggered");
this.state.todos = await api.fetchTodos();
},
async addTodoAction(newTodo) {
if (this.debug) console.log("addTodoAction triggered with ", newTodo);
const addedTodo = await api.addTodo(newTodo);
this.state.todos.push(addedTodo);
},
async removeTodoAction(todoIdToRemove) {
if (this.debug)
console.log("removeTodoAction triggered with id ", todoIdToRemove);
await api.removeTodo(todoIdToRemove);
this.state.todos = this.state.todos.filter((todo) => todo.id !== todoIdToRemove);
},
async toggleDoneAction(todoId, done) {
if (this.debug)
console.log("toggleDoneAction triggered with id ", todoId, " done: ", done);
const updatedTodo = await api.toggleTodoDone(todoId, done);
const index = this.state.todos.findIndex((todo) => todo.id === todoId);
if (index !== -1) {
this.state.todos[index] = updatedTodo;
}
}
};
<script setup>
import { computed, onMounted } from "vue";
import { store } from "../store/index.js";
import TodoItem from "./TodoItem.vue";
onMounted(async () => {
try {
await store.fetchTodosAction();
} catch (error) {
alert("Error fetching todos: " + error.message);
}
});
const todos = computed(() => store.state.todos);
</script>
<template>
<ul v-if="todos.length">
<todo-item v-for="todo in todos" :key="todo.id" :item="todo" />
</ul>
<p v-else>No hay tareas que mostrar</p>
</template>
El hook onMounted() se ejecuta al montarse el componente. En sistaxis de Options API sería el hook mounted():
export default {
mounted() {
try {
await store.fetchTodosAction();
} catch (error) {
alert("Error fetching todos: " + error.message);
}
},
...
}
<script setup>
import { store } from "../store";
const props = defineProps({
item: Object,
});
const delTodo = () => {
store.removeTodoAction(props.item.id);
};
const toggleDone = () => {
store.toggleDoneAction(props.item.id, !props.item.done);
};
</script>
<template>
<li>
<label>
<del v-if="item.done">
{ { item.title }}
</del>
<span v-else>
{ { item.title }}
</span>
</label>
<button @click="toggleDone()">
{ { item.done ? "No Hecha" : "Hecha" }}
</button>
<button @click="delTodo()">Borrar</button>
</li>
</template>
El método toggleDone ya no cambia nada sino que llama a la acción del store que se encargará de hacer la petición al servidor y actualizar el array local. No es necesario poner el await porque no necesitamos esperar a que termine la petición (luego no se hace nada) y cuando cambien los datos en el store Vue se encargará de actualizar la vista.
Lo único que cambia es el método addTodo es que ahora conviene poner un await en la llamada del store para que no borre el newTodo si falla la petición.
Además tanto esta petición como las de borrar y cambiar el done del TodoItem.vue deberían estar dentro de un bloque try/catch para capturar posibles errores en la petición y mostrarlos al usuario.
En lugar de usar axios directamente podemos crear un axios personalizado donde definamos las opciones que necesitemos para todas las peticiones. De esta forma no tendremos que repetirlas en cada petición y si tenemos que cambiar algo (la ruta del servidor, añadir un token de autenticación, etc) lo haremos en un único sitio.
Algunas de las opciones que podemos definir son:
const apiClient = axios.create({
baseURL: 'http://localhost:3000',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: 'Bearer ' + localStorage.token
}
})
Y usaremos esta instancia para hacer las peticiones: apiClient.get('/todos'), apiClient.post('/todos', newTodo), etc.
Si trabajamos con varias tablas podemos hacer un fichero de repositorio para cada una de ellas o bien podemos escribir lo mismo de antes pero de forma más concisa:
import axios from 'axios'
const apiClient = axios.create({
baseURL: 'http://localhost:3000',
withCredentials: false,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
})
const todos = {
getAll: () => apiClient.get(`/todos`),
getOne: (id) => apiClient.get(`/todos/${id}`),
create: (item) => apiClient.post(`/todos`, item),
modify: (item) => apiClient.put(`/todos/${item.id}`, item),
delete: (id) => apiClient.delete(`/todos/${id}`),
toogleDone: (item) => apiClient.patch(`/todos/${item.id}`, { done: !item.done }),
}
const categories = {
getAll: () => apiClient.get(`/categories`),
getOne: (id) => apiClient.get(`/categories/${id}`),
create: (item) => apiClient.post(`/categories`, item),
modify: (item) => apiClient.put(`/categories/${item.id}`, item),
delete: (id) => apiClient.delete(`/categories/${id}`),
}
export default {
todos,
categories,
}
Y en los componentes donde queramos usarlo importamos el fichero y llamamos a las funciones que necesitemos, por ejemplo api.todos.getAll().
También podemos usar programación orientada a objetos para hacer nuestra Api y construir una clase que se ocupe de las peticiones a la API:
import axios from 'axios'
const apiClient = axios.create({
baseURL: 'http://localhost:3000',
withCredentials: false,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
})
export default class APIService{
constructor(){
}
getTodos() {
return apiClient.get('/todos')
}
delTodo(id){
return apiClient.delete('/todos/'+id)
},
...
}
Y en los componentes donde queramos usarlo importamos la clase y creamos una instancia de la misma:
import APIService from '../APIService'
const apiService = new APIService()
const todos = async () => {
await apiService.getTodos()
}
Se trata de un fichero donde guardar las configuraciones de la aplicación y la ruta del servidor es una constante que estaría mejor en este fichero que en el código como hemos hecho nosotros.
Vue por medio de Vite puede acceder a todas las variables de .env que comiencen por VITE_ por medio del objeto import.meta.env por lo que en nuestro código en vez de darle el valor a baseURL podríamos haber puesto:
const apiClient = axios.create({
baseURL: import.meta.env.VITE_RUTA_API,
...
})
Y en el fichero .env ponemos
VITE_RUTA_API=http://localhost:3000
Si usamos Vue con webpack las variables de .env deben comenzar por VUE_APP_ y accedemos a ellas por medio del objeto process.env por lo que en el fichero .env definiríamos la variable VUE_APP_RUTA_API=http://localhost:3000 y en nuestro código pondría:
const apiClient = axios.create({
baseURL: process.env.VUE_APP_RUTA_API,
...
})
El fichero .env por defecto se sube al repositorio por lo que no debemos poner información sensible (como usuarios o contraseñas). Para ello tenemos un fichero .env.local que no se sube, o bien debemos añadir al .gitignore dicho fichero. En cualquier caso, si el fichero con la configuración no lo subimos al repositorio es conveniente tener un fichero .env.exemple, que sí se sube, con valores predeterminados para las distintas variables que deberán cambiarse por los valores adecuados en producción. Además del .env y el .env.local también hay distintos ficheros que son usados en desarrollo (.env.development) y en producción (.env.production) y que pueden tener distintos datos según el entorno en que nos encontramos. Por ejemplo en el de desarrollo el valor de VUE_APP_RUTA_API podría ser “http://localhost:3000” si usamos json-server mientras que en el de producción tendríamos la ruta del servidor de producción de la API.
Podemos hacer que se ejecute código antes de cualquier petición a axios o tras recibir la respuesta del servidor usando los interceptores de axios. Es otra forma de enviar un token que nos autentifique ante una API sin tener que ponerlo en el código de cada petición, pero también nos permite hacer cualquier cosa que necesitemos.
Y podemos interceptar las respuestas para, por ejemplo, redireccionar a la página de login si el servidor nos devuelve un error 401 (no autorizado).
Para interceptar las peticiones que hacemos usaremos axios.interceptors.request.use( (config) => fnAEjecutar, (error) => fnAEjecutar) y para interceptar las respuestas del servidor axios.interceptors.response.use( (response) => fnAEjecutar, (error) => fnAEjecutar). Se les pasa como parámetro la función a ejecutar si todo es correcto y la que se ejecutará si ha habido algún error. El interceptor de peticiones recibe como parámetro un objeto con toda la configuración de la petición (incluyendo sus cabeceras) y el interceptor de respuestas recibe la respuesta del servidor.
Veamos un ejemplo en que queremos enviar en las cabeceras de cada petición el token que tenemos almacenado en el LocalStorage y queremos mostrar un alert siempre que el servidor devuelva en su respuesta un error que no sea de tipo 400. Además mostraremos por consola las peticiones y las respuestas si activamos el modo DEBUG:
import axios from 'axios'
const baseURL = 'http://localhost:3000'
const DEBUG = true
axios.interceptors.request.use((config) => {
if (DEBUG) {
console.info('Request: ', config)
}
const token = localStorage.token
if (token) {
config.headers['Authorization'] = 'Bearer ' + localStorage.token
}
return config
}, (error) => {
if (DEBUG) {
console.error('Request error: ', error)
}
return Promise.reject(error)
})
axios.interceptors.response.use((response) => {
if (DEBUG) {
console.info('Response: ', response)
}
return response
}, (error) => {
if (error.response && error.response.status !== 400) {
alert('Response error ' + error.response.status + '(' + error.response.statusText + ')')
}
if (DEBUG) {
console.info('Response error: ', error)
}
return Promise.reject(error)
})
const categories = {
getAll: () => axios.get(`${baseURL}/categories`),
getOne: (id) => axios.get(`${baseURL}/categories/${id}`),
create: (item) => axios.post(`${baseURL}/categories`, item),
modify: (item) => axios.put(`${baseURL}/categories/${item.id}`, item),
delete: (id) => axios.delete(`${baseURL}/categories/${id}`),
}
export default {
categories,
}