UD5 - 3. Comunicación asíncrona¶
Promesas¶
Las promesas son un concepto para resolver el problema de asincronía de una forma mucho más elegante y práctica que, por ejemplo, utilizando funciones callback directamente.
Las promesas pueden tener varios estados:
- Resuelta. La promesa se cumple
- Rechazada. La promesa no se cumple
- Pendiente. La promesa se queda en un estado incierto indefinidamente
Con estas sencillas bases, podemos entender el funcionamiento de una promesa en Javascript. Antes de empezar, también debemos tener claro que existen dos partes importantes de las promesas:
- Consumirlas
- Crearlas, preparar una función para que use promesas y se puedan consumir.
Las promesas en Javascript se representan a través de un objeto, y cada promesa estará en un estado concreto: pendiente, resuelta o rechazada. Además, cada promesa tiene los siguientes métodos, que podremos utilizar para utilizarla:
.then(resolve): ejecuta la función callbackresolvecuando la promesa se cumple..catch(reject): ejecuta la función callbackrejectcuando la promesa se rechaza..then(resolve, reject): Método equivalente a los dos anteriores contenido en un solo método..finally(): ejecuta la función callbackfinallycuando la promesa se cumple o si se rechaza.
Consumir promesas¶
La forma general de consumir una promesa es utilizando el .then() con un sólo parámetro, puesto que muchas veces lo único que nos interesa es realizar una acción cuando la promesa se cumpla:
fetch("/datos.json").then(function(response) {
/* Código a realizar cuando se cumpla la promesa */
});
Lo que vemos en el ejemplo anterior es el uso de la función fetch(), la cuál devuelve una promesa que se cumple cuando obtiene respuesta de la petición realizada. De esta forma, estaríamos preparando (de una forma legible) la forma de actuar de nuestro código a la respuesta de la petición realizada, todo ello de forma asíncrona.
Recuerda que podemos hacer uso del método .catch() para actuar cuando se rechaza una promesa:
fetch("/datos.json")
.then(function(response) {
/* Código a realizar cuando se cumpla la promesa */
})
.catch(function(error) {
/* Código a realizar cuando se rechaza la promesa */
});
Observa como hemos indentado los métodos .then() y .catch(), ya que se suele hacer así para que sea mucho más legible. Además, se pueden encadenar varios .then() si se siguen generando promesas y se devuelven con un return:
fetch("/datos.json")
.then(response => {
return response.text(); // Devuelve una promesa
})
.then(data => {
console.log(data);
})
.catch(error => { /* Código a realizar cuando se rechaza la promesa */ });
Usando arrow functions se puede mejorar aún más la legibilidad de este código, recordando que cuando sólo tenemos una sentencia en el cuerpo de la arrow function hay un return implícito:
fetch("/datos.json")
.then(response => response.text())
.then(data => console.log(data))
.finally(() => console.log("Terminado."))
.catch(error => console.error(data));
Se añade el método .finally() para añadir una función callback que se ejecutará tanto si la promesa se cumple o se rechaza, lo que nos ahorrará tener que repetir la función en el .then() como en el .catch().
Código asíncrono¶
Algo muy importante, pero que quizás hemos pasado por alto es que el código que ejecutamos en el interior de un .then() es código asíncrono no bloqueante:
- Asíncrono: Porque no se ejecuterá de inmediato, sino que tardará en ejecutarse.
- No bloqueante: Porque mientras espera ser ejecutado, no bloquea el resto del programa.
Cuando llegamos a un .then(), el sistema no se bloquea, sino que deja la función «pendiente» hasta que se cumpla la promesa, pero mientras, continua procesando el resto del programa.
Observa el siguiente ejemplo:
fetch("/datos.json")
.then(response => response.text())
.then(data => {
console.log("Código asíncrono");
});
console.log("Código síncrono")
Aunque el console.log("Código asíncrono") figure unas líneas antes del console.log("Código síncrono"), se mostrará más tarde. Esto ocurre porque el console.log() del interior del .then() no ocurre inmediatamente, y al no ser bloqueante, se continua con el resto del programa hasta que se ejecute, que lo retomará.
Crear promesas¶
Para crear una promesa se utiliza el objeto Promise, de la siguiente forma new Promise((resolve, reject) => { }) se le pasa por parámetro una función anónima con dos parámetros de callback:
resolve. Lo utilizaremos cuando se cumpla la promesa.reject. Lo utilizaremos cuando se rechace la promesa.
Ejemplo de creación de una promesa:
// Ejemplo sencillo donde se va llenando un array con números aleatorios
// se aparece un 6 se rechaza la promesa
const doTask = (iterations) => {
return new Promise((resolve, reject) => {
const numbers = [];
for (let i = 0; i < iterations; i++) {
const number = 1 + Math.floor(Math.random() * 6);
numbers.push(number);
if (number === 6) {
reject({
error: true,
message: "Se ha sacado un 6"
});
}
}
resolve({
error: false,
value: numbers
});
}); // new Promise
}; // doTask
Como ves, se trata de una implementación muy similar a los callbacks que vimos en el apartado anterior, pero observa que se devuelve una que envuelve toda la función, permitiendo así consumirla cómodamente más tarde:
doTask(10)
.then(result => console.log("Tiradas correctas: ", result.value))
.catch(err => console.error("Ha ocurrido algo: ", err.message));
Imagina el caso de que cada lanzamiento del dado (la parte donde genera el número aleatorio) fuera un proceso más costoso que tardara un tiempo considerable, quizás de esa forma se vea más clara la necesidad de una tarea asíncrona, controlada con promesas.
Async / Await¶
En ES2017 se introducen las palabras clave async/await, que no son más que una forma para gestionar las promesas.
Con async/await seguimos manejando promesas, sin embargo, hay ciertos cambios importantes:
- El código se vuelve más legible, ya que se parece más a código síncrono.
- Se puede utilizar
try/catchpara gestionar los errores de una forma más cómoda. - Se puede utilizar
awaitpara esperar a que se cumpla una promesa, y así evitar el uso de.then().
Await¶
La palabra clave await se utiliza para esperar a que se cumpla una promesa, y así evitar el uso de .then().
const response = await fetch("datos.txt");
const data = await response.text();
console.log(data);
console.log("Código síncrono.");
Lo que hace await es detener la ejecución y no continuar. Se espera a que se resuelva la promesa, y hasta que no lo haga, no continua. A diferencia del fetch(), tenemos un código bloqueante.
Lo normal es que se utilice await dentro de una función. Por ejemplo:
function request() {
const response = await fetch("datos.txt");
const data = await response.text();
return data;
}
request();
Sin embargo, aquí tenemos un problema. Estamos utilizando await (asíncrono) dentro de request() (síncrono), por lo que antes de ejecutarla, al intentarla definir, nos aparecerá el siguiente error:
Para solucionarlo, debemos indicar que la función request() es asíncrona, utilizando la palabra clave async
Async¶
Para resolver el problema anterior y poder utilizar el await dentro de nuestra función, sólo tenemos que definir nuestra función como función asíncrona y al llamarla utilizar nuevamente el await:
async function request() {
const response = await fetch("datos.txt");
const data = await response.text();
return data;
}
await request();
Sin embargo, vamos a pararnos un poco a pensar esto desde las bases. Definamos dos funciones básicas exactamente iguales, ambas devuelven lo mismo, pero una es síncrona y otra asíncrona:
function sincrona() { return 42; }
async function asincrona() { return 42; }
sincrona(); // 42
asincrona(); // Promise <fulfilled>: 42
En el caso de la función sincrona() devuelve directamente el valor, sin embargo, en el caso de la función asincrona() devuelve una promesa que se ha cumplido inmediatamente, con el valor 42.
Si queremos reescribirlas como arrow function, se definiría como vemos a continuación, colocando el async justo antes de los parámetros de la arrow function:
Await/Async + .then()¶
En algunos casos, como al usar un fetch(), donde tenemos que manejar dos promesas, es posible que nos interese utilizar .then() para la primera promesa y await para la segunda. De esta forma podemos manejarlo todo directamente, sin tener que guardarlo en constantes o variables temporales que no utilizaremos sino una sola vez:
async function request() {
return await fetch("datos.txt")
.then(response => response.text());
}
await request();
En este caso, observa que el fetch() devuelve una primera Promise que es manejada por el .then(). La segunda Promise, devuelta por el método response.text() se devuelve hacia fuera y es manejada por el await, que espera a que se cumpla, y una vez cumplida, se devuelve como valor de la función request().
Asincronía en async/await¶
Volvamos al ejemplo de las tiradas de dados. La función doTask() realiza 10 lanzamientos de un dado y nos devuelve los resultados obtenidos o detiene la tarea si se obtiene un 6. La implementación de la función sufre algunos cambios, simplificándose considerablemente.
- En primer lugar, añadimos la palabra clave
asyncantes de los parámetros de la arrow function. - En segundo lugar, desaparece cualquier mención a promesas, se devuelven directamente los objetos, ya que al ser una función
asyncse devolverá todo envuelto en una Promise:
const doTask = async (iterations) => {
const numbers = [];
for (let i = 0; i < iterations; i++) {
const number = 1 + Math.floor(Math.random() * 6);
numbers.push(number);
if (number === 6) {
return {
error: true,
message: "Se ha sacado un 6"
};
}
}
return {
error: false,
value: numbers
};
}
Pero donde se introducen cambios considerables es a la hora de consumir las promesas con async/await. No tendríamos que utilizar .then(), sino que podemos simplemente utilizar await para esperar la resolución de la promesa, obteniendo el valor directamente:
Observa que el await se utiliza dentro de una función async, por lo que la función que lo contenga debe ser asíncrona:
async function consume() {
const result = await doTask(10);
if (result.error) {
console.log("Error: ", result.message);
} else {
console.log("Los números son: ", result.value);
}
}
ACTIVIDAD 4: 📂 UD5/act04/
- Crea un fichero
index.htmlcon un botón que al pulsarlo llame a la funcióngetRandomMessage(). - Crea un fichero
script.jscon la funcióngetRandomMessage()y la funcióngetMessages(). - Descarga el archivo
jokes.txty guárdalo en la carpetaUD5/act04/. - La función
getMessages()hará unfetch()ajokes.txty devolverá una promesa con el texto. - La función
getRandomMessage()debe llamar agetMessages()dividir el texto en líneas (.split('\n')) y devolver una línea aleatoria que aparecerá en unalert().
Peticiones Ajax¶
AJAX es el acrónimo de Asynchronous Javascript And XML (Javascript asíncrono y XML) y es lo que usamos para hacer peticiones asíncronas al servidor desde Javascript. Cuando hacemos una petición al servidor no nos responde inmediatamente (la petición tiene que llegar al servidor, procesarse allí y enviarse la respuesta que llegará al cliente).
Lo que significa asíncrono es que la página no permanecerá bloqueada esperando esa respuesta sino que continuará ejecutando su código e interactuando con el usuario, y en el momento en que llegue la respuesta del servidor se ejecutará la función que indicamos al hacer la llamada Ajax. Respecto a XML, es el formato en que se intercambia la información entre el servidor y el cliente, aunque actualmente el formato más usado es JSON que es más simple y legible.
Básicamente Ajax nos permite poder mostrar nuevos datos enviados por el servidor sin tener que recargar la página, que continuará disponible mientras se reciben y procesan los datos enviados por el servidor en segundo plano.
Sin Ajax cada vez que necesitamos nuevos datos del servidor la página deja de estar disponible para el usuario hasta que se recarga con lo que envía el servidor. Con Ajax la página está siempre disponible para el usuario y simplemente se modifica (cambiando el DOM) cuando llegan los datos del servidor:
Fuente Uniwebsidad
Métodos HTTP¶
Las peticiones Ajax usan el protocolo HTTP (el mismo que utiliza el navegador para cargar una página). Este protocolo envía al servidor unas cabeceras HTTP (con información como el userAgent del navegador, el idioma, etc), el tipo de petición y, opcionalmente, datos o parámetros (por ejemplo en la petición que procesa un formulario se envían los datos del mismo).
Hay diferentes tipos de petición que podemos hacer:
GET: suele usarse para obtener datos sin modificar nada (equivale a unSELECTen SQL). Si enviamos datos (ej. laIDdel registro a obtener) suelen ir en la url de la petición (formato URIEncoded). Ej.:locahost/users/3, https://jsonplaceholder.typicode.com/usersPOST: suele usarse para añadir un dato en el servidor (equivalente a unINSERTde SQL). Los datos enviados van en el cuerpo de la petición HTTP (igual que sucede al enviar desde el navegador un formulario por POST)PUT: es similar al POST pero suele usarse para actualizar datos del servidor (como unUPDATEde SQL). Los datos se envían en el cuerpo de la petición (como en el POST) y la información para identificar el objeto a modificar en la url (como en elGET). El servidor hará unUPDATEsustituyendo el objeto actual por el que se le pasa como parámetroPATCH: es similar al PUT pero la diferencia es que en el PUT hay que pasar todos los campos del objeto a modificar (los campos no pasados se eliminan del objeto) mientras que en el PATCH sólo se pasan los campos que se quieren cambiar y en resto permanecen como están.DELETE: se usa para eliminar un dato del servidor (como unDELETEde SQL). La información para identificar el objeto a eliminar se envía en la url (como en el GET)
El servidor acepta la petición, la procesa y le envía una respuesta al cliente con el recurso solicitado y además unas cabeceras de respuesta (con el tipo de contenido enviado, el idioma, etc) y el código de estado. Los códigos de estado más comunes son:
2xx: son peticiones procesadas correctamente. Las más usuales son200(ok) o201(created, como respuesta a una petición POST satisfactoria)3xx: son códigos de redirección que indican que la petición se redirecciona a otro recurso del servidor, como301(el recurso se ha movido permanentemente a otra URL) o304(el recurso no ha cambiado desde la última petición por lo que se puede recuperar desde la caché)4xx: indican un error por parte del cliente, como404(Not found, no existe el recurso solicitado) o401(Not authorized, el cliente no está autorizado a acceder al recurso solicitado)5xx: indican un error por parte del servidor, como500(error interno del servidor) o504(timeout, el servidor no responde).
En cuanto a la información enviada por el servidor al cliente normalmente serán datos en formato JSON o XML (cada vez menos usado) que el cliente procesará y mostrará en la página al usuario. También podría ser HTML, texto plano, etc.
El formato JSON es una forma de convertir un objeto Javascript en una cadena de texto para poderla enviar, por ejemplo el objeto
se transformaría en la cadena de texto
y el array
let alumnos = [
{
id: 5,
nombre: "Marta",
apellidos: "Pérez Rodríguez"
},
{
id: 7,
nombre: "Joan",
apellidos: "Reig Peris"
},
]
en la cadena:
[{ "id": 5, "nombre": Marta, "apellidos": Pérez Rodríguez }, { "id": 7, "nombre": "Joan", "apellidos": "Reig Peris" }]
Para convertir objetos en cadenas de texto JSON y viceversa Javascript proporciona 2 funciones:
- JSON.stringify(objeto): recibe un objeto JS y devuelve la cadena de texto correspondiente. Ej.:
const cadenaAlumnos = JSON.stringify(alumnos) - JSON.parse(cadena): realiza el proceso inverso, convirtiendo una cadena de texto en un objeto. Ej.:
const alumnos = JSON.parse(cadenaAlumnos)
API Fetch¶
La API Fetch permite realizar una petición Ajax genérica que directamente devuelve en forma de promesa.
La API Fetch proporciona una interfaz JavaScript para acceder y manipular partes del canal HTTP, tales como peticiones y respuestas. También provee un método global fetch() que proporciona una forma fácil y lógica de obtener recursos de forma asíncrona por la red.
-
fetchdevuelve los datos "en crudo" por lo que si la respuesta está en formato JSON habrá con convertirlos. Para ello dispone del método.json()que hace elJSON.parse. Este método devuelve una nueva promesa a la que nos suscribimos con un nuevo.then(). Ejemplo.:fetch('https://jsonplaceholder.typicode.com/posts?userId=' + idUser) .then(response => response.json()) // los datos son una cadena JSON .then(myData => { // ya tenemos los datos en _myData_ como un objeto o array // Aquí procesamos los datos console.log(myData) }) .catch(err => console.error(err)); -
fetchllama aresolvesiempre que el servidor conteste, sin comprobar si la respuesta es de éxito (200,201, etc.) o de error (4xx,5xx). Por tanto siempre se ejecutará elthenexcepto si se trata de un error de red y el servidor no responde.
Propiedades y métodos de la respuesta¶
La respuesta devuelta por fetch() tiene las siguientes propiedades y métodos:
status: el código de estado devuelto por el servidor (200,404, etc.)statusText: el texto correspondiente a ese código (Ok,Not found, etc.)ok: booleano que valetruesi el status está entre200y299yfalseen caso contrariojson(): devuelve una promesa que se resolverá con los datos de la respuesta convertidos a un objeto (les hace un JSON.parse())- otros métodos para convertir los datos según el formato que tengan:
text(),blob(),formData(), etc. Todos devuelven una promesa con los datos de distintos formatos convertidos.
Ejemplo de usando fetch() para obtener los posts de un usuario y presentarlos en una tabla:
Pero este ejemplo fallaría si hubiéramos puesto mal la url ya que contestaría con un 404 pero se ejecutaría el .then() igualmente.
Gestión de errores con fetch¶
Según MDN la promesa devuelta por la API fetch sólo es rechazada en el caso de un error de red, es decir, el .catch() sólo saltará si no hemos recibido respuesta del servidor; en caso contrario la promesa siempre es resuelta.
Por tanto para saber si se ha resuelto satisfactoriamente o no debemos comprobar la propiedad .ok de la respuesta. El código correcto del ejemplo anterior gestionando los posibles errores del servidor sería:
En este caso si la respuesta del servidor no es ok lanzamos un error que es interceptado por nuestro propio catch
Otros métodos de petición¶
Los ejemplos anteriores hacen peticiones GET al servidor. Para peticiones que no sean GET la función fetch() admite un segundo parámetro con un objeto con la información a enviar en la petición HTTP. Ej.:
fetch(url, {
method: 'POST', // o 'PUT', 'GET', 'DELETE'
body: JSON.stringify(data), // los datos que enviamos al servidor en el 'send'
headers:{
'Content-Type': 'application/json'
}
}).then
Ejemplo de una petición para añadir datos:
fetch(url, {
method: 'POST',
body: JSON.stringify(data), // los datos que enviamos al servidor en el 'send'
headers:{
'Content-Type': 'application/json'
}
})
.then(response => {
if (!response.ok) {
throw `Error ${response.status} de la BBDD: ${response.statusText}`
}
return response.json()
})
.then(datos => {
alert('Datos recibidos')
console.log(datos)
})
.catch(err => {
alert('Error en la petición HTTP: ' + err.message);
})
Podéis ver mś ejemplos en MDN web docs y otras páginas.
Peticiones con async / await¶
Estas nuevas instrucciones introducidas en ES2017 nos permiten escribir el código de peticiones asíncronas como si fueran síncronas lo que facilita su comprensión.
Se puede llamar a cualquier función asíncrona (por ejemplo una promesa como fetch()) anteponiendo la palabra await a la llamada. Esto provocará que la ejecución se "espere" a que se resuelva la promesa devuelta por esa función. Así nuestro código se asemeja a código síncrono ya que no continúan ejecutándose las instrucciones que hay después de un await hasta que esa petición se ha resuelto.
Cualquier función que realice un await pasa a ser asíncrona ya que no se ejecuta en ese momento sino que se espera un tiempo. Y para indicarlo debemos anteponer la palabra async a su declaración function. Al hacerlo automáticamente se "envuelve" esa función en una promesa (o sea que esa función pasa a devolver una promesa, a la que podríamos ponerle un await o un .then()).
Siguiendo con el ejemplo anterior:
async function getUserPosts() {
const response = await fetch('https://jsonplaceholder.typicode.com/posts?userId=' + idUser);
if (!response.ok) {
throw `Error ${response.status} de la BBDD: ${response.statusText}`
}
const userPosts = await response.json(); // recordad que .json() tb es una promesa
return userPosts;
}
...
// Y llamaremos a esa función con
const userPosts = await getUserPosts();
Diferencia entre async/await y promesas¶
La diferencia entre usar async/await y promesas es que con async/await no tenemos que usar .then() para obtener el valor devuelto por la promesa sino que lo obtenemos directamente en la variable.
-
Por ejemplo, si hacemos una petición
fetchy queremos obtener los posts de un usuario conididUserpodemos hacerlo con promesas así:y en
responsetendremos una promesa que se resolverá cuando llegue la respuesta del servidor. Para obtener los datos de la respuesta debemos suscribirnos a esa promesa con un.then():y en el
thentendremos los datos de la respuesta convertidos a un objeto o array. Si queremos obtenerlos en una variable debemos hacer: -
Con async/await podemos hacer:
const response = await fetch('https://jsonplaceholder.typicode.com/posts?userId=' + idUser); const userPosts = await response.json(); // aquí ya podemos usar los datosEn este caso
responsees una promesa que se resolverá cuando llegue la respuesta del servidor yuserPostses una promesa que se resolverá cuando se conviertan los datos de la respuesta a un objeto o array. Pero como hemos puestoawaitlo que obtenemos enresponsees ya el valor devuelto por la promesa cuando se resuelve.Con esto conseguimos que llamadas asíncronas se comporten como instrucciones síncronas lo que aporta claridad al código.
Podéis ver algunos ejemplos del uso de async / await en la página de MDN.
El ejemplo de los posts quedaría:
En este código estamos tratando los posibles errores que se pueden producir. Con async/await los errores se tratan como en las excepciones, con try ... catch:
También podemos tratarlos sin usar try...catch porque como una función asíncrona devuelve una promesa podemos suscribirnos directamente a su .catch()
Hacer varias peticiones simultáneamente. Promise.all()¶
En ocasiones necesitamos hacer más de una petición al servidor. Por ejemplo para obtener los productos y sus categorías podríamos hacer:
Pero si para renderizar los productos necesitamos tener las categorías este código no nos lo garantiza ya que el servidor podría devolver antes los productos aunque los pedimos después.
Una solución sería no pedir los productos hasta tener las categorías:
pero esto hará más lento nuestro código al no hacer las dos peticiones simultáneamente.
La solución es usar el método Promise.all() al que se le pasa un array de promesas a hacer y devuelve una promesa que:
- se resuelve en el momento en que todas las promesas se han resuelto satisfactoriamente o
- se rechaza en el momento en que alguna de las promesas es rechazada
El código anterior de forma correcta sería:
| Promise.all() | |
|---|---|
Lo mismo pasa si en vez de promesas usamos async/await. Si hacemos:
tenemos el problema de que no comienza la petición de los productos hasta que se reciben las categorías.
La solución con Promise.all() sería:
| Promise.all() con async/await | |
|---|---|
Realizar peticiones Ajax¶
Json Server¶
Las peticiones Ajax se hacen a un servidor que proporcione una API. Como ahora no tenemos ninguno podemos utilizar Json Server que es un servidor API-REST que funciona bajo Node.js (que ya tenemos instalado para usar NPM) y que utiliza un fichero JSON como contenedor de los datos en lugar de una base de datos.
Para instalarlo en nuestra máquina (lo instalaremos global para poderlo usar en todas nuestras prácticas) ejecutamos:
Para que sirva un fichero datos.json, dentro del fichero package.json añadimos la línea:
Le podemos poner la opción --watch ( o -w) para que actualice los datos si se modifica el fichero .json externamente (si lo editamos).
El fichero datos.json será un fichero que contenga un objeto JSON con una propiedad para cada "tabla" de nuestra BBDD. Por ejemplo, si queremos simular una BBDD con las tablas users y posts vacías el contenido del fichero será:
La API escucha en el puerto 3000 y servirá los diferentes objetos definidos en el fichero .json. Por ejemplo:
- http://localhost:3000/users: devuelve un array con todos los elementos de la tabla users del fichero .json
- http://localhost:3000/users/5: devuelve un objeto con el elemento de la tabla users cuya propiedad id valga 5
También pueden hacerse peticiones más complejas como:
- http://localhost:3000/users?rol=3: devuelve un array con todos los elementos de users cuya propiedad rol valga 3
Para más información: https://github.com/typicode/json-server.
Si queremos acceder a la API desde otro equipo (no desde localhost) tenemos que indicar la IP de la máquina que ejecuta json-server y que se usará para acceder, por ejemplo si vamos a ejecutarlo en la máquina 192.168.0.10 pondremos:
Y la ruta para acceder a la API sería http://192.168.0.10:3000.
liveServer
Si utilizamos liveServer en vsCode, cada vez que se actualice el fichero datos.json recargará la página, ignorando el .preventDefault() del formulario. Para evitarlo podemos desactivar la recarga automática de liveServer añadiendo la siguiente línea al fichero .vscode/settings.json de vsCode:
REST client¶
Para probar las peticiones GET podemos poner la URL en la barra de direcciones del navegador pero para probar el resto de peticiones debemos instalar en nuestro navegador una extensión que nos permita realizar las peticiones indicando el método a usar, las cabeceras a enviar y los datos que enviaremos a servidor, además de la URL.
Existen multitud de aplicaciones para realizar peticiones HTTP, como Advanced REST client. Cada navegador tiene sus propias extensiones para hacer esto, como Advanced Rest Client para Chrome o RestClient para Firefox.
Ejemplos de envío de datos¶
Para poder añadir datos a la BBDD necesitamos hacer peticiones POST al servidor.
Vamos a ver un ejemplo de creación de un nuevo usuario. Supondremos que tenemos una página con un formulario para dar de alta nuevos usuarios:
En este ejemplo se usa el método fetch() para hacer la petición POST al servidor. En el objeto de opciones que le pasamos como segundo parámetro indicamos:
method: 'POST': para indicar que es una peticiónPOSTbody: JSON.stringify(userData): para convertir el objetouserDataen una cadena de texto JSON que se enviará en el cuerpo de la peticiónheaders: { 'Content-type': 'application/json' }: para indicar que el formato en el que se envía la información es JSON
ACTIVIDAD 5: 📂 UD5/act05/
Crear una pequeña aplicación para gestionar una lista de deseos de Navidad, mediante peticiones Ajax a un servidor local json-server.
- Crea dos carpetas
client/yserver/. - Dentro de
server/realiza los siguientes pasos:- Crea un fichero
datos.jsoncon el siguiente contenidos: - Instala json-server como dependencia de desarrollo, ejecuta en la consola:
- Edita
package.jsony añade la siguiente línea en el apartado "scripts": Permitirá poner en marcha el servicio con un pequeño retardo de 400ms para simular retraso en la respuesta del servidor. - Inicia el servicio con:
- Crea un fichero
- Dentro de
client/crea un ficheroindex.htmlcon el siguiente contenido:- En el título de la página:
Xmas Wishlist. - Un formulario para buscar productos por nombre:
- Un campo de texto para introducir el nombre del producto
- Un botón para enviar el formulario
- Un formulario para crear nuevos productos:
- Nombre del producto
- Precio
- Botón para enviar el formulario
- Un formulario para borrar un producto por su
id:- Un campo de texto para introducir el
iddel producto - Un botón para enviar el formulario
- Un campo de texto para introducir el
- Un
divpara mostrar los resultados de las peticiones - Un
divpara mostrar mensajes de error - Un
loaderpara mostrar mientras se realizan las peticiones, puedes utilizar uno de los que creamos en las actividades anteriores.
- En el título de la página:
- Dentro de
client/crea un ficheromain.jscon el siguiente contenido:- Añade un eventListener al formulario de búsqueda para que cuando se envíe haga una petición
GETal servidor con el nombre del producto a buscar y muestre los resultados en la página. - Añade un eventListener al formulario de añadir productos para que cuando se envíe haga una petición
POSTal servidor con los datos del producto a añadir y muestre los resultados en la página. - Añade un eventListener al formulario de borrar productos para que cuando se envíe haga una petición
DELETEal servidor con eliddel producto a borrar y muestre los resultados en la página. - Crea un componente
product-infoque muestre la información de un producto individual, con su id, nombre y precio. Este componente se usará para mostrar los resultados de las peticiones. - El código de las peticiones debe estar en funciones independientes.
- Se debe mostrar un mensaje de error si la petición no se ha podido realizar y ocultarlo si se realiza correctamente.
- Se debe mostrar un loader mientras se realizan las peticiones y ocultarlo cuando se han terminado, tanto si se han realizado correctamente como si no.
- Se debe ocultar el
divde resultados al realizar cualquier petición.
- Añade un eventListener al formulario de búsqueda para que cuando se envíe haga una petición
Estructura de carpetas:
Single Page Application¶
Ajax es la base para construir SPAs que permiten al usuario interactuar con una aplicación web como si se tratara de una aplicación de escritorio (sin "esperas" que dejen la página en blanco o no funcional mientras se recarga desde el servidor).
En una SPA sólo se carga la página de inicio (es la única página que existe) que se va modificando y cambiando sus datos como respuesta a la interacción del usuario. Para obtener los nuevos datos se realizan peticiones al servidor (normalmente Ajax). La respuesta son datos (JSON, XML, etc.) que se muestran al usuario modificando mediante DOM la página mostrada (o podrían ser trozos de HTML que se cargan en determinadas partes de la página, o ...).
Resumen de llamadas asíncronas¶
Una llamada Ajax es un tipo de llamada asíncrona que podemos hacer en Javascript aunque hay muchas más, como un setTimeout() o las funciones manejadoras de eventos. Como hemos visto, para la gestión de las llamadas asíncronas tenemos varios métodos y los más comunes son:
- funciones callback
- promesas
- async / await
Cuando se produce una llamada asíncrona el orden de ejecución del código no es el que vemos en el programa ya que el código de respuesta de la llamada no se ejecutará hasta completarse esta. Podemos ver un ejemplo de esto extraído de todoJS usando funciones callback.
Además, si hacemos varias llamadas tampoco sabemos el qué orden se ejecutarán sus respuestas ya que depende de cuándo finalice cada una como podemos ver en este otro ejemplo.
Si usamos funciones callback y necesitamos que cada función no se ejecute hasta que haya terminado la anterior debemos llamarla en la respuesta a la función anterior lo que provoca un tipo de código difícil de leer llamado callback hell.
Para evitar esto surgieron las promesas que permiten evitar las funciones callback tan difíciles de leer. Podemos ver el primer ejemplo usando promesas. Y si necesitamos ejecutar secuencialmente las funciones evitaremos la pirámide de llamadas callback como vemos en este ejemplo.
Aún así el código no es muy claro. Para mejorarlo tenemos async y await como vemos en este ejemplo. Estas funciones forman parte del estándar ES2017 por lo que no están soportadas por navegadores muy antiguos (aunque siempre podemos transpilar con Babel).
Fuente: todoJs: Controlar la ejecución asíncrona
CORS¶
Cross-Origin Resource Sharing (CORS) es un mecanismo de seguridad que incluyen los navegadores y que por defecto impiden que se pueden realizar peticiones Ajax desde un navegador a un servidor con un dominio diferente al de la página cargada originalmente.
Si necesitamos hacer este tipo de peticiones necesitamos que el servidor al que hacemos la petición añada en su respuesta la cabecera Access-Control-Allow-Origin donde indiquemos el dominio desde el que se pueden hacer peticiones (o * para permitirlas desde cualquier dominio).
El navegador comprobará las cabeceras de respuesta y si el dominio indicado por ella coincide con el dominio desde el que se hizo la petición, esta se permitirá.
Como en desarrollo normalmente no estamos en el dominio de producción (para el que se permitirán las peticiones) podemos instalar en el navegador la extensión allow CORS que al activarla deshabilita la seguridad CORS en el navegador.
Podéis ampliar la información en numerosas páginas web como "Entendiendo CORS y aplicando soluciones".