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ónjs/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 ejemplopackage-lock.json
ypackage.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:
- fetch
/examples/words.txt
- Valida la respuesta con
validateResponse
. - leer la respuesta como texto (pista: consulta Response.text())
- 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.