Capítulo 4. Solicitudes de red

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Introducción

Te resultaría difícil encontrar hoy en día una aplicación web que no envíe peticiones a la red, en . Desde los albores de la Web 2.0 y el novedoso enfoque conocido como Ajax (Asynchronous JavaScript and XML), las aplicaciones web han estado enviando peticiones asíncronas para obtener nuevos datos sin recargar toda la página. La API XMLHttpRequest inició una nueva era de aplicaciones JavaScript interactivas. A pesar de su nombre, XMLHttpRequest (o XHR, como se conoce a veces) también puede funcionar con JSON y cargas útiles de datos de formulario.

XMLHttpRequest supuso un cambio en las reglas del juego, pero puede resultar complicado trabajar con la API. Con el tiempo, bibliotecas de terceros, como Axios y jQuery, añadieron API más ágiles que envolvían la API XHR central.

En 2015, una nueva API basada en Promise llamada Fetch se convirtió en un nuevo estándar, y los navegadores empezaron gradualmente a añadir soporte para ella. Hoy en día, Fetch es la forma estándar de realizar peticiones asíncronas desde tus aplicaciones web.

Este capítulo explora XHR y Fetch, así como algunas otras API para lacomunicación en red:

Balizas

Una simple petición POST unidireccional ideal para enviar datos analíticos

Eventos enviados por el servidor

Una conexión persistente unidireccional con un servidor para recibir eventos en tiempo real

WebSockets

Una conexión bidireccional persistente para la comunicación bidireccional

Enviar una petición con XMLHttpRequest

Problema

Quieres enviar una petición GET a una API pública, y quieres dar soporte a navegadores antiguos que no implementan la API Fetch.

Solución

Utiliza la API XMLHttpRequest. XMLHttpRequest es una API asíncrona, basada en eventos, para hacer peticiones a la red. El uso general de XMLHttpRequest es el siguiente:

  1. Crea un nuevo objeto XMLHttpRequest.

  2. Añade un receptor para el evento load, que recibe los datos de la respuesta.

  3. Llama a open sobre la solicitud, pasando el método HTTP y la URL.

  4. Por último, llama a send sobre la petición. Esto activa el envío de la solicitud HTTP.

El Ejemplo 4-1 muestra un ejemplo sencillo de cómo trabajar con datos JSON utilizando un XHR.

Ejemplo 4-1. Hacer una petición GET con XMLHttpRequest
/**
 * Loads user data from the URL /api/users, then prints them
 * to the console
 */
function getUsers() {
  const request = new XMLHttpRequest();

  request.addEventListener('load', event => {
    // The event target is the XHR itself; it contains a
    // responseText property that we can use to create a JavaScript object from
    // the JSON text.
    const users = JSON.parse(event.target.responseText);
    console.log('Got users:', users);
  });

  // Handle any potential errors with the request.
  // This only handles network errors. If the request
  // returns an error status like 404, the 'load' event still fires
  // where you can inspect the status code.
  request.addEventListener('error', err => {
    console.log('Error!', err);
  });

  request.open('GET', '/api/users');
  request.send();
}

Debate

La API XMLHttpRequest es una API basada en eventos. Cuando se recibe la respuesta, se activa un evento load. En el Ejemplo 4-1, el controlador de eventosload pasa el texto sin procesar de la respuesta a JSON.parse. Espera que el cuerpo de la respuesta sea JSON y utiliza JSON.parse para convertir la cadena JSON en un objeto.

Si se produce un error al cargar los datos, se activa el evento error. Esto gestiona los errores de conexión o de red, pero un código de estado HTTP que se considere un "error", como 404 o 500, no desencadena este evento. En su lugar, también desencadena el evento load.

Para protegerte de tales errores, debes examinar la propiedad status de la respuesta para determinar si existe tal situación de error. Puedes acceder a ella haciendo referencia a event.target.status.

Fetch es compatible desde hace mucho tiempo, así que, a menos que tengas que dar soporte a navegadores realmente antiguos, lo más probable es que no necesites utilizar XMLHttpRequest. La mayoría de las veces -si no todas- utilizarás la API Fetch.

Enviar una solicitud GET con la API Fetch

Problema

Quieres enviar una solicitud GET a una API pública utilizando un navegador moderno.

Solución

Utiliza la API Fetch. Fetch es una API de petición más reciente que utiliza Promises. Es muy flexible y puede enviar todo tipo de datos, pero el Ejemplo 4-2 envía una petición GET básica a una API.

Ejemplo 4-2. Enviar una solicitud GET con la API Fetch
/**
 * Loads users by calling the /api/users API, and parses the
 * response JSON.
 * @returns a Promise that resolves to an array of users returned by the API
 */
function loadUsers() {
  // Make the request.
  return fetch('/api/users')
    // Parse the response body to an object.
    .then(response => response.json())
    // Handle errors, including network and JSON parsing errors.
    .catch(error => console.error('Unable to fetch:', error.message));
}

loadUsers().then(users => {
  console.log('Got users:', users);
});

Debate

La API Fetch es más concisa. Devuelve un Promise que resuelve a un objeto que representa la respuesta HTTP. El objeto response contiene datos como el código de estado, las cabeceras y el cuerpo.

Para obtener el cuerpo de la respuesta JSON, tienes que llamar al método json de la respuesta. Este método lee el cuerpo del flujo y devuelve un Promise que resuelve el cuerpo JSON analizado como un objeto. Si el cuerpo de la respuesta no es JSON válido, se rechaza el Promise.

La respuesta también tiene métodos para leer el cuerpo en otros formatos, como FormData o una cadena de texto sin formato.

Como Fetch funciona con Promises, también puedes utilizar await, como se muestra en el Ejemplo 4-3.

Ejemplo 4-3. Utilizar Fetch con async/await
async function loadUsers() {
  try {
    const response = await fetch('/api/users');
    return response.json();
  } catch (error) {
    console.error('Error loading users:', error);
  }
}

async function printUsers() {
  const users = await loadUsers();
  console.log('Got users:', users);
}
Nota

Recuerda que antes de utilizar await en una función, esa función debe tener la palabra clave async .

Enviar una solicitud POST con la API Fetch

Problema

Quieres enviar una solicitud POST a una API que espera un cuerpo de solicitud JSON.

Solución

Utiliza la API Fetch, especificando el método (POST), y el cuerpo y tipo de contenido JSON (ver Ejemplo 4-4).

Ejemplo 4-4. Envío de carga útil JSON mediante POST con la API Fetch
/**
 * Creates a new user by sending a POST request to /api/users.
 * @param firstName The user's first name
 * @param lastName The user's last name
 * @param department The user's department
 * @returns a Promise that resolves to the API response body
 */
function createUser(firstName, lastName, department) {
  return fetch('/api/users', {
    method: 'POST',
    body: JSON.stringify({ firstName, lastName, department }),
    headers: {
      'Content-Type': 'application/json'
    }
  })
    .then(response => response.json());
}

createUser('John', 'Doe', 'Engineering')
  .then(() => console.log('Created user!'))
  .catch(error => console.error('Error creating user:', error));

Debate

El ejemplo 4-4 envía algunos datos JSON en una petición POST. Llamar a JSON.stringify sobre el objeto usuario lo convierte en una cadena JSON, lo que es necesario para enviarlo como cuerpo con fetch. También necesitas establecer la cabecera Content-Type para que el servidor sepa cómo interpretar el cuerpo.

Fetch también te permite enviar otros tipos de contenido como cuerpo. El Ejemplo 4-5 muestra cómo enviarías una petición a POST con algunos datos de formulario.

Ejemplo 4-5. Enviar datos de formulario en una petición POST
fetch('/login', {
  method: 'POST',
  body: 'username=sysadmin&password=password',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
  }
})
  .then(response => response.json())
  .then(data => console.log('Logged in!', data))
  .catch(error => console.error('Request failed:', error));

Cargar un archivo con la API Fetch

Problema

Quieres subir datos de archivo con una petición POST, utilizando la API Fetch.

Solución

Utiliza un elemento <input type="file">, y envía el contenido del archivo como cuerpo de la petición (consulta el Ejemplo 4-6).

Ejemplo 4-6. Enviar datos de archivos con la API Fetch
/**
 * Given a form with a 'file' input, sends a POST request containing
 * the file data in its body.
 * @param form the form object (should have a file input with the name 'file')
 * @returns a Promise that resolves when the response JSON is received
 */
function uploadFile(form) {
  const formData = new FormData(form);
  const fileData = formData.get('file');
  return fetch('https://httpbin.org/post', {
    method: 'POST',
    body: fileData
  })
    .then(response => response.json());
}

Debate

No hay muchos pasos para subir un archivo utilizando las APIs de los navegadores modernos. La página <input type="file"> proporciona los datos del archivo a través de la API FormData y se incluyen en el cuerpo de la solicitud POST. El navegador se encarga del resto.

Enviar una baliza

Problema

Quieres enviar una solicitud rápida sin esperar respuesta, por ejemplo, para enviar datos analíticos.

Solución

Utiliza la Beacon API para enviar datos en una solicitud POST. Una solicitud POST normal con la API Fetch puede no completarse a tiempo antes de que se descargue la página. Utilizar una baliza tiene más probabilidades de éxito (ver Ejemplo 4-7). El navegador no espera una respuesta, y es más probable que la solicitud tenga éxito si se envía cuando el usuario está abandonando tu sitio.

Ejemplo 4-7. Enviar una baliza
const currentUser = {
  username: 'sysadmin'
};

// Some analytics data we want to capture
const data = {
  user: currentUser.username,
  lastVisited: new Date()
};

// Send the data before unload.
document.addEventListener('visibilitychange', () => {
  // If the visibility state is 'hidden', that means the page just became hidden.
  if (document.visibilityState === 'hidden') {
    navigator.sendBeacon('/api/analytics', data);
  }
});

Debate

Con una llamada a XMLHttpRequest o fetch, el navegador espera la respuesta y la devuelve (con un evento o Promise). En general, no necesitas esperar la respuesta para peticiones unidireccionales, como el envío de datos analíticos.

En lugar de un Promise, navigator.sendBeacon devuelve un valor booleano que indica si se programó la operación de envío. No hay más eventos ni notificaciones.

navigator.sendBeacon siempre envía una petición POST. Si quieres enviar varios conjuntos de datos analíticos, como una colección de interacciones de interfaz de usuario, puedes recopilarlos en una matriz a medida que el usuario interactúa con tu página, y luego enviar la matriz como cuerpo de POST con la baliza .

Escucha de Eventos Remotos con Eventos Enviados por el Servidor

Problema

Quieres recibir notificaciones de tu servidor backend sin sondeos repetidos.

Solución

Utiliza la API EventSource para recibir eventos enviados por el servidor (SSE).

Para empezar a escuchar SSE, crea una nueva instancia de EventSource, pasando la URL como primer argumento (ver Ejemplo 4-8).

Ejemplo 4-8. Abrir una conexión SSE
const events = new EventSource('https://example.com/events');

// Fired once connected
events.addEventListener('open', () => {
  console.log('Connection is open');
});

// Fired if a connection error occurs
events.addEventListener('error', event => {
  console.log('An error occurred:', event);
});

// Fired when receiving an event with a type of 'heartbeat'
events.addEventListener('heartbeat', event => {
  console.log('got heartbeat:', event.data);
});

// Fired when receiving an event with a type of 'notice'
events.addEventListener('notice', event => {
  console.log('got notice:', event.data);
})

// The EventSource leaves the connection open. If we want to close the connection,
// we need to call close on the EventSource object.
function cleanup() {
  events.close();
}

Debate

Un EventSource debe conectarse a un punto final HTTP especial que deja la conexión abierta con una cabecera Content-Type de text/event-stream. Cada vez que se produce un evento, el servidor puede enviar un nuevo mensaje a través de la conexión abierta.

Nota

Como señala MDN, es muy recomendable utilizar HTTP/2 con SSE. De lo contrario, los navegadores imponen un límite estricto al número de conexiones EventSource por dominio. En este caso, sólo puede haber hasta seis conexiones.

Este límite no es por pestaña; se impone en todas las pestañas del navegador de un dominio determinado.

Cuando EventSource recibe un evento a través de una conexión persistente, es texto sin formato. Puedes acceder al texto del evento desde la propiedad data del objeto de evento recibido. Aquí tienes un ejemplo de un evento del tipo notice:

event: notice
data: Connection established at 10:51 PM, 2023-04-22
id: 3

Para escuchar este evento, llama a addEventListener('notice') en el objeto EventSource. El objeto evento tiene una propiedad data, cuyo valor es cualquier valor de cadena prefijado con data: en el evento.

Si un evento no tiene un tipo de evento, puedes escuchar el evento genérico message para recibirlo .

Intercambiar datos en tiempo real con WebSockets

Problema

Quieres enviar y recibir datos en tiempo real sin tener que sondear repetidamente al servidor con peticiones Fetch.

Solución

Utiliza la API WebSocket para abrir una conexión persistente con tu servidor backend (consulta el Ejemplo 4-9).

Ejemplo 4-9. Crear una conexión WebSocket
// Open the WebSocket connection (the URL scheme should be ws: or wss:).
const socket = new WebSocket(url);

socket.addEventListener('open', onSocketOpened);
socket.addEventListener('message', handleMessage);
socket.addEventListener('error', handleError);
socket.addEventListener('close', onSocketClosed);

function onSocketOpened() {
  console.log('Socket ready for messages');
}

function handleMessage(event) {
  console.log('Received message:', event.data);
}

function handleError(event) {
  console.log('Socket error:', event);
}

function onSocketClosed() {
  console.log('Connection was closed');
}
Nota

Para utilizar WebSockets, tu servidor debe tener un punto final habilitado para WebSocket al que puedas conectarte. MDN tiene un buen artículo sobre cómo crear un servidor WebSocket.

Una vez que el socket dispara el evento open, puedes empezar a enviar mensajes, como se muestra en el Ejemplo 4-10.

Ejemplo 4-10. Envío de mensajes WebSocket
// Messages are simple strings.
socket.send('Hello');

// The socket needs the data as a string, so you can use
// JSON.stringify to serialize objects to be sent.
socket.send(JSON.stringify({
  username: 'sysadmin',
  password: 'password'
}));

Una conexión WebSocket es una conexión bidireccional. Los datos recibidos del servidor disparan un evento message. Puedes manejarlos como necesites o incluso enviar una respuesta (ver Ejemplo 4-11).

Ejemplo 4-11. Responder a un mensaje WebSocket
socket.addEventListener('message', event => {
  socket.send('ACKNOWLEDGED');
});

Por último, para limpiar cuando hayas terminado, puedes cerrar la conexión llamando a close en el objeto WebSocket.

Debate

Los WebSockets son muy adecuados para aplicaciones que requieren capacidades en tiempo real, como un sistema de chat o de monitoreo de eventos. Los puntos finales de WebSocket tienen un esquema ws:// o wss://. Son análogos a http:// y https://-uno es inseguro y el otro utilizaencriptación-.

Para iniciar una conexión WebSocket, el navegador envía primero una solicitud GET al punto final WebSocket. La carga útil de la solicitud de la URL wss://example.com/websocket tiene el siguiente aspecto:

GET /websocket HTTP/1.1
Host: example.com
Sec-WebSocket-Key: aSBjYW4gaGFzIHdzIHBsej8/
Sec-WebSocket-Version: 13
Connection: Upgrade
Upgrade: websocket

Esto inicia un intercambio WebSocket. Si tiene éxito, el servidor responde con un código de estado 101 (Cambio de protocolo):

HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: bm8gcGVla2luZywgcGxlYXNlIQ==

El protocolo WebSocket especifica un algoritmo para generar una cabecera Sec-Websocket-Accept basada en el Sec-WebSocket-Key de la solicitud. El cliente verifica este valor, y en ese momento la conexión WebSocket bidireccional está activa y el socket dispara el evento open.

Una vez abierta la conexión, puedes escuchar mensajes con el evento message y enviar mensajes llamando a send sobre el objeto socket. Después, puedes terminar la sesión WebSocket llamando a close sobre el objeto socket.

Get Libro de recetas de la API web now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.