Fetch API

Este codelab forma parte del curso de capacitación Developing Progressive Web Apps, desarrollado por el equipo de capacitación de Google Developers. Aprovecharás al máximo este curso si trabajas con los codelabs de forma secuencial.

Para obtener detalles completos sobre el curso, consulta la descripción general de Developing Progressive Web Apps.

Introducción

En este lab, se explica cómo usar la API de Fetch, una interfaz simple para recuperar recursos y una mejora con respecto a la API de XMLHttpRequest.

Qué aprenderás

  • Cómo usar la API de Fetch para solicitar recursos
  • Cómo realizar solicitudes GET, HEAD y POST con fetch
  • Cómo leer y establecer encabezados personalizados
  • Uso y limitaciones de CORS

Lo que debe saber

  • Conocimientos básicos de JavaScript y HTML
  • Conocimiento del concepto y la sintaxis básica de las promesas de ES2015

Lo que necesitarás

  • Una computadora con acceso a la terminal o al shell
  • Conexión a Internet
  • Un navegador que admita Fetch
  • Un editor de texto
  • Node y npm

Nota: Si bien la API de Fetch no es compatible con todos los navegadores en este momento, existe un polyfill.

Descarga o clona el repositorio de pwa-training-labs desde GitHub y, si es necesario, instala la versión LTS de Node.js.

Abre la línea de comandos de tu computadora. Navega al directorio fetch-api-lab/app/ y, luego, inicia un servidor de desarrollo local:

cd fetch-api-lab/app
npm install
node server.js

Puedes detener el servidor en cualquier momento con Ctrl-c.

Abre el navegador y navega a localhost:8081/. Deberías ver una página con botones para realizar solicitudes (aún no funcionarán).

Nota: Cancela el registro de todos los service workers y borra todas las memorias caché de service workers para localhost, de modo que no interfieran en el lab. En las Herramientas para desarrolladores de Chrome, puedes hacer clic en Borrar datos del sitio en la sección Borrar almacenamiento de la pestaña Aplicación.

Abre la carpeta fetch-api-lab/app/ en tu editor de texto preferido. La carpeta app/ es donde compilarás el lab.

Esta carpeta contiene lo siguiente:

  • echo-servers/ contiene archivos que se usan para ejecutar servidores de prueba.
  • examples/ contiene recursos de muestra que usamos para experimentar con la recuperación
  • js/main.js es el archivo JavaScript principal de la app, y es donde escribirás todo tu código.
  • index.html es la página HTML principal de nuestro sitio o aplicación de ejemplo
  • package-lock.json y package.json son archivos de configuración para nuestro servidor de desarrollo y las dependencias del servidor de eco.
  • server.js es un servidor de desarrollo de nodos

La API de Fetch tiene una interfaz relativamente simple. En esta sección, se explica cómo escribir una solicitud HTTP básica con la función fetch.

Cómo recuperar un archivo JSON

En js/main.js, el botón Fetch JSON de la app está adjunto a la función fetchJSON.

Actualiza la función fetchJSON para solicitar el archivo examples/animals.json y registrar la respuesta:

function fetchJSON() {
  fetch('examples/animals.json')
    .then(logResult)
    .catch(logError);
}

Guarda el código y actualiza la página. Haz clic en Fetch JSON. La consola debería registrar la respuesta de la recuperación.

Explicación

El método fetch acepta la ruta de acceso al recurso que queremos recuperar como parámetro, en este caso examples/animals.json. fetch devuelve una promesa que se resuelve en un objeto Response. Si la promesa se resuelve, la respuesta se pasa a la función logResult. Si la promesa se rechaza, catch toma el control y el error se pasa a la función logError.

Los objetos de respuesta representan la respuesta a una solicitud. Contienen el cuerpo de la respuesta y también propiedades y métodos útiles.

Prueba respuestas no válidas

Examina la respuesta registrada en la consola. Ten en cuenta los valores de las propiedades status, url y ok.

Reemplaza el recurso examples/animals.json en fetchJSON por examples/non-existent.json. La función fetchJSON actualizada ahora debería verse de la siguiente manera:

function fetchJSON() {
  fetch('examples/non-existent.json')
    .then(logResult)
    .catch(logError);
}

Guarda el código y actualiza la página. Haz clic en Fetch JSON de nuevo para intentar recuperar este recurso inexistente.

Observa que la recuperación se completó correctamente y no activó el bloqueo catch. Ahora busca las propiedades status, URL y ok de la nueva respuesta.

Los valores deben ser diferentes para los dos archivos (¿entiendes por qué?). Si recibiste algún error en la consola, ¿los valores coinciden con el contexto del error?

Explicación

¿Por qué una respuesta fallida no activó el bloque catch? Esta es una nota importante sobre las recuperaciones y las promesas: las respuestas incorrectas (como los errores 404) también se resuelven. Una promesa de recuperación solo se rechaza si la solicitud no se pudo completar, por lo que siempre debes verificar la validez de la respuesta. Validaremos las respuestas en la siguiente sección.

Más información

Verifica la validez de la respuesta

Debemos actualizar nuestro código para verificar la validez de las respuestas.

En main.js, agrega una función para validar las respuestas:

function validateResponse(response) {
  if (!response.ok) {
    throw Error(response.statusText);
  }
  return response;
}

Luego, reemplaza fetchJSON por el siguiente código:

function fetchJSON() {
  fetch('examples/non-existent.json')
    .then(validateResponse)
    .then(logResult)
    .catch(logError);
}

Guarda el código y actualiza la página. Haz clic en Fetch JSON. Revisa la consola. Ahora, la respuesta de examples/non-existent.json debería activar el bloque catch.

Reemplaza examples/non-existent.json en la función fetchJSON por el examples/animals.json original. La función actualizada ahora debería verse de la siguiente manera:

function fetchJSON() {
  fetch('examples/animals.json')
    .then(validateResponse)
    .then(logResult)
    .catch(logError);
}

Guarda el código y actualiza la página. Haz clic en Fetch JSON. Deberías ver que la respuesta se registra correctamente como antes.

Explicación

Ahora que agregamos la verificación validateResponse, las respuestas incorrectas (como los errores 404) arrojan un error y catch toma el control. Esto nos permite controlar las respuestas fallidas y evitar que las respuestas inesperadas se propaguen por la cadena de recuperación.

Lee la respuesta

Las respuestas de recuperación se representan como ReadableStreams (especificación de Streams) y se deben leer para acceder al cuerpo de la respuesta. Los objetos de respuesta tienen métodos para hacerlo.

En main.js, agrega una función readResponseAsJSON con el siguiente código:

function readResponseAsJSON(response) {
  return response.json();
}

Luego, reemplaza la función fetchJSON por el siguiente código:

function fetchJSON() {
  fetch('examples/animals.json') // 1
  .then(validateResponse) // 2
  .then(readResponseAsJSON) // 3
  .then(logResult) // 4
  .catch(logError);
}

Guarda el código y actualiza la página. Haz clic en Fetch JSON. Verifica la consola para asegurarte de que se esté registrando el JSON de examples/animals.json (en lugar del objeto Response).

Explicación

Revisemos lo que sucede.

Paso 1: Se llama a Fetch en un recurso, examples/animals.json. Fetch devuelve una promesa que se resuelve en un objeto Response. Cuando se resuelve la promesa, el objeto de respuesta se pasa a validateResponse.

Paso 2: validateResponse verifica si la respuesta es válida (¿es un 200?). De lo contrario, se arroja un error, se omite el resto de los bloques then y se activa el bloque catch. Esto es particularmente importante. Sin esta verificación, las respuestas incorrectas se pasan por la cadena y podrían interrumpir el código posterior que puede depender de la recepción de una respuesta válida. Si la respuesta es válida, se pasa a readResponseAsJSON.

Paso 3: readResponseAsJSON lee el cuerpo de la respuesta con el método Response.json(). Este método devuelve una promesa que se resuelve en JSON. Una vez que se resuelve esta promesa, los datos JSON se pasan a logResult. (Si la promesa de response.json() se rechaza, se activa el bloque catch).

Paso 4: Por último, logResult registra los datos JSON de la solicitud original a examples/animals.json.

Más información

La recuperación no se limita a JSON. En este ejemplo, recuperaremos una imagen y la agregaremos a la página.

En main.js, escribe una función showImage con el siguiente código:

function showImage(responseAsBlob) {
  const container = document.getElementById('img-container');
  const imgElem = document.createElement('img');
  container.appendChild(imgElem);
  const imgUrl = URL.createObjectURL(responseAsBlob);
  imgElem.src = imgUrl;
}

Luego, agrega una función readResponseAsBlob que lea las respuestas como un Blob:

function readResponseAsBlob(response) {
  return response.blob();
}

Actualiza la función fetchImage con el siguiente código:

function fetchImage() {
  fetch('examples/fetching.jpg')
    .then(validateResponse)
    .then(readResponseAsBlob)
    .then(showImage)
    .catch(logError);
}

Guarda el código y actualiza la página. Haz clic en Fetch image. Deberías ver un perro adorable buscando un palo en la página (¡es un chiste de búsqueda!).

Explicación

En este ejemplo, se recupera una imagen, examples/fetching.jpg. Al igual que en el ejercicio anterior, la respuesta se valida con validateResponse. Luego, la respuesta se lee como un Blob (en lugar de JSON, como en la sección anterior). Se crea un elemento de imagen y se agrega a la página, y el atributo src de la imagen se establece en una URL de datos que representa el Blob.

Nota: El método createObjectURL() del objeto URL se usa para generar una URL de datos que representa el Blob. Es importante tener en cuenta esto. No puedes establecer la fuente de una imagen directamente en un Blob. El objeto Blob se debe convertir en una URL de datos.

Más información

Esta sección es un desafío opcional.

Actualiza la función fetchText de la siguiente manera:

  1. fetch /examples/words.txt
  2. Valida la respuesta con validateResponse.
  3. leer la respuesta como texto (pista: consulta Response.text())
  4. y mostrar el texto en la página

Puedes usar esta función showText como ayuda para mostrar el texto final:

function showText(responseAsText) {
  const message = document.getElementById('message');
  message.textContent = responseAsText;
}

Guarda el código y actualiza la página. Haz clic en Fetch text. Si implementaste fetchText correctamente, deberías ver texto agregado en la página.

Nota: Si bien puede ser tentador recuperar HTML y agregarlo con el atributo innerHTML, ten cuidado. Esto puede exponer tu sitio a ataques de secuencias de comandos entre sitios.

Más información

De forma predeterminada, la recuperación usa el método GET, que recupera un recurso específico. Sin embargo, la recuperación también puede usar otros métodos HTTP.

Cómo realizar una solicitud HEAD

Reemplaza la función headRequest por el siguiente código:

function headRequest() {
  fetch('examples/words.txt', {
    method: 'HEAD'
  })
  .then(validateResponse)
  .then(readResponseAsText)
  .then(logResult)
  .catch(logError);
}

Guarda el código y actualiza la página. Haz clic en Solicitud HEAD. Observa que el contenido de texto registrado está vacío.

Explicación

El método fetch puede recibir un segundo parámetro opcional, init. Este parámetro permite configurar la solicitud de recuperación, como el método de solicitud, el modo de caché, las credenciales y mucho más.

En este ejemplo, configuramos el método de solicitud de recuperación en HEAD con el parámetro init. Las solicitudes HEAD son como las solicitudes GET, excepto que el cuerpo de la respuesta está vacío. Este tipo de solicitud se puede usar cuando solo necesitas metadatos sobre un archivo, pero no necesitas transportar todos los datos del archivo.

Opcional: Busca el tamaño de un recurso

Veamos los encabezados de la respuesta de recuperación para examples/words.txt y determinar el tamaño del archivo.

Actualiza la función headRequest para registrar la propiedad content-length de la respuesta headers (pista: consulta la documentación de encabezados y el método get).

Después de actualizar el código, guarda el archivo y actualiza la página. Haz clic en Solicitud HEAD. La consola debe registrar el tamaño (en bytes) de examples/words.txt.

Explicación

En este ejemplo, se usa el método HEAD para solicitar el tamaño (en bytes) de un recurso (representado en el encabezado content-length) sin cargar el recurso en sí. En la práctica, esto se podría usar para determinar si se debe solicitar el recurso completo (o incluso cómo solicitarlo).

Opcional: Averigua el tamaño de examples/words.txt con otro método y confirma que coincida con el valor del encabezado de respuesta (puedes buscar cómo hacerlo para tu sistema operativo específico; obtendrás puntos adicionales si usas la línea de comandos).

Más información

Fetch también puede enviar datos con solicitudes POST.

Configura un servidor echo

Para este ejemplo, debes ejecutar un servidor de eco. Desde el directorio fetch-api-lab/app/, ejecuta el siguiente comando (si el servidor localhost:8081 bloquea la línea de comandos, abre una nueva ventana o pestaña de línea de comandos):

node echo-servers/cors-server.js

Este comando inicia un servidor simple en localhost:5000/ que devuelve las solicitudes que se le envían.

Puedes detener este servidor en cualquier momento con ctrl+c.

Realiza una solicitud POST

Reemplaza la función postRequest por el siguiente código (asegúrate de haber definido la función showText de la sección 4 si no completaste la sección):

function postRequest() {
  fetch('http://localhost:5000/', {
    method: 'POST',
    body: 'name=david&message=hello'
  })
    .then(validateResponse)
    .then(readResponseAsText)
    .then(showText)
    .catch(logError);
}

Guarda el código y actualiza la página. Haz clic en Solicitud POST. Observa la solicitud enviada que se muestra en la página. Debe contener el nombre y el mensaje (ten en cuenta que aún no recibimos datos del formulario).

Explicación

Para realizar una solicitud POST con fetch, usamos el parámetro init para especificar el método (de manera similar a como establecimos el método HEAD en la sección anterior). Aquí también configuramos el cuerpo de la solicitud, que, en este caso, es una cadena simple. El cuerpo son los datos que queremos enviar.

Nota: En producción, recuerda siempre encriptar los datos sensibles del usuario.

Cuando los datos se envían como una solicitud POST a localhost:5000/, la solicitud se devuelve como respuesta. Luego, la respuesta se valida con validateResponse, se lee como texto y se muestra en la página.

En la práctica, este servidor representaría una API de terceros.

Opcional: Usa la interfaz FormData

Puedes usar la interfaz FormData para obtener datos de formularios con facilidad.

En la función postRequest, crea una instancia de un objeto FormData nuevo a partir del elemento de formulario msg-form:

const formData = new FormData(document.getElementById('msg-form'));

Luego, reemplaza el valor del parámetro body por la variable formData.

Guarda el código y actualiza la página. Completa el formulario (los campos Nombre y Mensaje) en la página y, luego, haz clic en la solicitud POST. Observa el contenido del formulario que se muestra en la página.

Explicación

El constructor FormData puede recibir un form HTML y crear un objeto FormData. Este objeto se completa con las claves y los valores del formulario.

Más información

Inicia un servidor echo sin CORS

Detén el servidor echo anterior (presionando ctrl+c desde la línea de comandos) y, luego, inicia un nuevo servidor echo desde el directorio fetch-lab-api/app/ ejecutando el siguiente comando:

node echo-servers/no-cors-server.js

Este comando configura otro servidor de eco simple, esta vez en localhost:5001/. Sin embargo, este servidor no está configurado para aceptar solicitudes de origen cruzado.

Recupera datos del nuevo servidor

Ahora que el nuevo servidor se ejecuta en localhost:5001/, podemos enviarle una solicitud de recuperación.

Actualiza la función postRequest para recuperar datos de localhost:5001/ en lugar de localhost:5000/. Después de actualizar el código, guarda el archivo, actualiza la página y, luego, haz clic en POST Request.

Deberías recibir un error en la consola que indique que la solicitud de origen cruzado está bloqueada porque falta el encabezado Access-Control-Allow-Origin de CORS.

Actualiza el fetch en la función postRequest con el siguiente código, que usa el modo no-cors (como sugiere el registro de errores) y quita las llamadas a validateResponse y readResponseAsText (consulta la explicación a continuación):

function postRequest() {
  const formData = new FormData(document.getElementById('msg-form'));
  fetch('http://localhost:5001/', {
    method: 'POST',
    body: formData,
    mode: 'no-cors'
  })
    .then(logResult)
    .catch(logError);
}

Guarda el código y actualiza la página. Luego, completa el formulario de mensaje y haz clic en POST Request.

Observa el objeto de respuesta registrado en la consola.

Explicación

Fetch (y XMLHttpRequest) siguen la política del mismo origen. Esto significa que los navegadores restringen las solicitudes HTTP de origen cruzado desde las secuencias de comandos. Una solicitud multiorigen se produce cuando un dominio (por ejemplo, http://foo.com/) solicita un recurso de un dominio independiente (por ejemplo, http://bar.com/).

Nota: Las restricciones de solicitudes de origen cruzado suelen ser un punto de confusión. Muchos recursos, como imágenes, hojas de estilo y secuencias de comandos, se recuperan en varios dominios (es decir, de origen cruzado). Sin embargo, estas son excepciones a la política del mismo origen. Las solicitudes de origen cruzado siguen restringidas dentro de las secuencias de comandos.

Dado que el servidor de nuestra app tiene un número de puerto diferente al de los dos servidores de eco, las solicitudes a cualquiera de los servidores de eco se consideran de origen cruzado. Sin embargo, el primer servidor de eco, que se ejecuta en localhost:5000/, está configurado para admitir CORS (puedes abrir echo-servers/cors-server.js y examinar la configuración). El nuevo servidor de eco, que se ejecuta en localhost:5001/, no lo está (por eso recibimos un error).

El uso de mode: no-cors permite recuperar una respuesta opaca. Esto permite obtener una respuesta, pero impide acceder a ella con JavaScript (por eso no podemos usar validateResponse, readResponseAsText ni showResponse). Otras APIs pueden consumir la respuesta, o bien un service worker puede almacenarla en caché.

Cómo modificar encabezados de solicitud

Fetch también admite la modificación de los encabezados de solicitud. Detén el servidor de eco localhost:5001 (sin CORS) y reinicia el servidor de eco localhost:5000 (con CORS) de la sección 6:

node echo-servers/cors-server.js

Restablece la versión anterior de la función postRequest que recupera datos de localhost:5000/:

function postRequest() {
  const formData = new FormData(document.getElementById('msg-form'));
  fetch('http://localhost:5000/', {
    method: 'POST',
    body: formData
  })
    .then(validateResponse)
    .then(readResponseAsText)
    .then(showText)
    .catch(logError);
}

Ahora usa la interfaz de encabezado para crear un objeto Headers dentro de la función postRequest llamada messageHeaders con el encabezado Content-Type igual a application/json.

Luego, establece la propiedad headers del objeto init para que sea la variable messageHeaders.

Actualiza la propiedad body para que sea un objeto JSON convertido en cadena, como el siguiente:

JSON.stringify({ lab: 'fetch', status: 'fun' })

Después de actualizar el código, guarda el archivo y actualiza la página. Luego, haz clic en POST Request.

Observa que la solicitud repetida ahora tiene un Content-Type de application/json (en lugar de multipart/form-data como tenía antes).

Ahora, agrega un encabezado Content-Length personalizado al objeto messageHeaders y asígnale un tamaño arbitrario a la solicitud.

Después de actualizar el código, guarda el archivo, actualiza la página y haz clic en POST Request. Observa que este encabezado no se modifica en la solicitud repetida.

Explicación

La interfaz Header permite crear y modificar objetos Headers. Algunos encabezados, como Content-Type, se pueden modificar con la recuperación. Otros, como Content-Length, están protegidos y no se pueden modificar (por motivos de seguridad).

Cómo establecer encabezados de solicitud personalizados

Fetch admite la configuración de encabezados personalizados.

Quita el encabezado Content-Length del objeto messageHeaders en la función postRequest. Agrega el encabezado personalizado X-Custom con un valor arbitrario (por ejemplo, "X-CUSTOM': 'hello world'").

Guarda la secuencia de comandos, actualiza la página y, luego, haz clic en POST Request.

Deberías ver que la solicitud repetida tiene la propiedad X-Custom que agregaste.

Ahora, agrega un encabezado Y-Custom al objeto Headers. Guarda la secuencia de comandos, actualiza la página y haz clic en POST Request.

Deberías ver un error similar a este en la consola:

Fetch API cannot load http://localhost:5000/. Request header field y-custom is not allowed by Access-Control-Allow-Headers in preflight response.

Explicación

Al igual que las solicitudes de origen cruzado, los encabezados personalizados deben ser compatibles con el servidor desde el que se solicita el recurso. En este ejemplo, nuestro servidor de eco está configurado para aceptar el encabezado X-Custom, pero no el encabezado Y-Custom (puedes abrir echo-servers/cors-server.js y buscar Access-Control-Allow-Headers para comprobarlo por tu cuenta). Cada vez que se establece un encabezado personalizado, el navegador realiza una verificación de solicitud preliminar. Esto significa que el navegador primero envía una solicitud OPTIONS al servidor para determinar qué métodos y encabezados HTTP permite el servidor. Si el servidor está configurado para aceptar el método y los encabezados de la solicitud original, se envía. De lo contrario, se arroja un error.

Más información

Código de solución

Para obtener una copia del código que funciona, navega a la carpeta solution.

Ahora sabes cómo usar la API de Fetch.

Recursos

Para ver todos los codelabs del curso de capacitación sobre APW, consulta el codelab de bienvenida del curso.