XMLHttpRequest es un objeto nativo del navegador que permite hacer solicitudes HTTP desde JavaScript.
A pesar de tener la palabra âXMLâ en su nombre, se puede operar sobre cualquier dato, no solo en formato XML. Podemos cargar y descargar archivos, dar seguimiento y mucho más.
Ahora hay un método más moderno fetch que en algún sentido hace obsoleto a XMLHttpRequest.
En el desarrollo web moderno XMLHttpRequest se usa por tres razones:
- Razones históricas: necesitamos soportar scripts existentes con
XMLHttpRequest. - Necesitamos soportar navegadores viejos, y no queremos
polyfills(p.ej. para mantener los scripts pequeños). - Necesitamos hacer algo que
fetchno puede todavÃa, ej. rastrear el progreso de subida.
¿Te suena familiar? Si es asÃ, está bien, adelante con XMLHttpRequest. De otra forma, por favor, dirÃgete a Fetch.
Lo básico
XMLHttpRequest tiene dos modos de operación: sincrónica y asÃncrona.
Veamos primero la asÃncrona, ya que es utilizada en la mayorÃa de los casos.
Para hacer la petición, necesitamos seguir 3 pasos:
-
Crear el objeto
XMLHttpRequest:let xhr = new XMLHttpRequest();El constructor no tiene argumentos.
-
Inicializarlo, usualmente justo después de
new XMLHttpRequest:xhr.open(method, URL, [async, user, password])Este método especifica los parámetros principales para la petición:
methodâ método HTTP. Usualmente"GET"o"POST".URLâ la URL a solicitar, una cadena, puede ser un objeto URL.asyncâ si se asigna explÃcitamente afalse, entonces la petición será asincrónica. Cubriremos esto un poco más adelante.user,passwordâ usuario y contraseña para autenticación HTTP básica (si se requiere).
Por favor, toma en cuenta que la llamada a
open, contrario a su nombre, no abre la conexión. Solo configura la solicitud, pero la actividad de red solo empieza con la llamada del métodosend. -
Enviar.
xhr.send([body])Este método abre la conexión y envÃa ka solicitud al servidor. El parámetro adicional
bodycontiene el cuerpo de la solicitud.Algunos métodos como
GETno tienen un cuerpo. Y otros comoPOSTusan el parámetrobodypara enviar datos al servidor. Vamos a ver unos ejemplos de eso más tarde. -
Escuchar los eventos de respuesta
xhr.Estos son los tres eventos más comúnmente utilizados:
loadâ cuando la solicitud está; completa (incluso si el estado HTTP es 400 o 500), y la respuesta se descargó por completo.errorâ cuando la solicitud no pudo ser realizada satisfactoriamente, ej. red caÃda o una URL inválida.progressâ se dispara periódicamente mientras la respuesta está siendo descargada, reporta cuánto se ha descargado.
xhr.onload = function() { alert(`Cargado: ${xhr.status} ${xhr.response}`); }; xhr.onerror = function() { // solo se activa si la solicitud no se puede realizar alert(`Error de red`); }; xhr.onprogress = function(event) { // se dispara periódicamente // event.loaded - cuántos bytes se han descargado // event.lengthComputable = devuelve true si el servidor envÃa la cabecera Content-Length (longitud del contenido) // event.total - número total de bytes (si `lengthComputable` es `true`) alert(`Recibido ${event.loaded} of ${event.total}`); };
Aquà un ejemplo completo. El siguiente código carga la URL en /article/xmlhttprequest/example/load desde el servidor e imprime el progreso:
// 1. Crea un nuevo objeto XMLHttpRequest
let xhr = new XMLHttpRequest();
// 2. Configuración: solicitud GET para la URL /article/.../load
xhr.open('GET', '/article/xmlhttprequest/example/load');
// 3. EnvÃa la solicitud a la red
xhr.send();
// 4. Esto se llamará después de que la respuesta se reciba
xhr.onload = function() {
if (xhr.status != 200) { // analiza el estado HTTP de la respuesta
alert(`Error ${xhr.status}: ${xhr.statusText}`); // ej. 404: No encontrado
} else { // muestra el resultado
alert(`Hecho, obtenidos ${xhr.response.length} bytes`); // Respuesta del servidor
}
};
xhr.onprogress = function(event) {
if (event.lengthComputable) {
alert(`Recibidos ${event.loaded} de ${event.total} bytes`);
} else {
alert(`Recibidos ${event.loaded} bytes`); // sin Content-Length
}
};
xhr.onerror = function() {
alert("Solicitud fallida");
};
Una vez el servidor haya respondido, podemos recibir el resultado en las siguientes propiedades de xhr:
status- Código del estado HTTP (un número):
200,404,403y asà por el estilo, puede ser0en caso de una falla no HTTP. statusText- Mensaje del estado HTTP (una cadena): usualmente
OKpara200,Not Foundpara404,Forbiddenpara403y asà por el estilo. response(scripts antiguos deben usarresponseText)- El cuerpo de la respuesta del servidor.
También podemos especificar un tiempo lÃmite usando la propiedad correspondiente:
xhr.timeout = 10000; // lÃmite de tiempo en milisegundos, 10 segundos
Si la solicitud no es realizada con éxito dentro del tiempo dado, se cancela y el evento timeout se activa.
Para agregar los parámetros a la URL, como ?nombre=valor, y asegurar la codificación adecuada, podemos utilizar un objeto URL:
let url = new URL('https://google.com/search');
url.searchParams.set('q', 'pruébame!');
// el parámetro 'q' está codificado
xhr.open('GET', url); // https://google.com/search?q=test+me%21
Tipo de respuesta
Podemos usar la propiedad xhr.responseType para asignar el formato de la respuesta:
""(default) â obtiene una cadena,"text"â obtiene una cadena,"arraybuffer"â obtiene unArrayBuffer(para datos binarios, ve el capÃtulo ArrayBuffer, arrays binarios),"blob"â obtiene unBlob(para datos binarios, ver el capÃtulo Blob),"document"â obtiene un documento XML (puede usar XPath y otros métodos XML) o un documento HTML (en base al tipo MIME del dato recibido),"json"â obtiene un JSON (automáticamente analizado).
Por ejemplo, obtengamos una respuesta como JSON:
let xhr = new XMLHttpRequest();
xhr.open('GET', '/article/xmlhttprequest/example/json');
xhr.responseType = 'json';
xhr.send();
// la respuesta es {"message": "Hola, Mundo!"}
xhr.onload = function() {
let responseObj = xhr.response;
alert(responseObj.message); // Hola, Mundo!
};
En los scripts antiguos puedes encontrar también las propiedades xhr.responseText e incluso xhr.responseXML.
Existen por razones históricas, para obtener ya sea una cadena o un documento XML. Hoy en dÃa, debemos seleccionar el formato en xhr.responseType y obtener xhr.response como se demuestra debajo.
Estados
XMLHttpRequest cambia entre estados a medida que avanza. El estado actual es accesible como xhr.readyState.
Todos los estados, como en la especificación:
UNSENT = 0; // estado inicial
OPENED = 1; // llamada abierta
HEADERS_RECEIVED = 2; // cabeceras de respuesta recibidas
LOADING = 3; // la respuesta está cargando (un paquete de datos es recibido)
DONE = 4; // solicitud completa
Un objeto XMLHttpRequest escala en orden 0 â 1 â 2 â 3 â ⦠â 3 â 4. El estado 3 se repite cada vez que un paquete de datos se recibe a través de la red.
Podemos seguirlos usando el evento readystatechange:
xhr.onreadystatechange = function() {
if (xhr.readyState == 3) {
// cargando
}
if (xhr.readyState == 4) {
// solicitud finalizada
}
};
Puedes encontrar oyentes del evento readystatechange en código realmente viejo, está ahà por razones históricas, habÃa un tiempo cuando no existÃan load y otros eventos. Hoy en dÃa los manipuladores load/error/progress lo hacen obsoleto.
Abortando solicitudes
Podemos terminar la solicitud en cualquier momento. La llamada a xhr.abort() hace eso:
xhr.abort(); // termina la solicitud
Este dispara el evento abort, y el xhr.status se convierte en 0.
Solicitudes sincrónicas
Si en el método open el tercer parámetro async se asigna como false, la solicitud se hace sincrónicamente.
En otras palabras, la ejecución de JavaScript se pausa en el send() y se reanuda cuando la respuesta es recibida. Algo como los comandos alert o prompt.
Aquà está el ejemplo reescrito, el tercer parámetro de open es false:
let xhr = new XMLHttpRequest();
xhr.open('GET', '/article/xmlhttprequest/hello.txt', false);
try {
xhr.send();
if (xhr.status != 200) {
alert(`Error ${xhr.status}: ${xhr.statusText}`);
} else {
alert(xhr.response);
}
} catch(err) { // en lugar de onerror
alert("Solicitud fallida");
}
Puede verse bien, pero las llamadas sincrónicas son rara vez utilizadas porque bloquean todo el JavaScript de la página hasta que la carga está completa. En algunos navegadores se hace imposible hacer scroll. Si una llamada sÃncrona toma mucho tiempo, el navegador puede sugerir cerrar el sitio web âcolgadoâ.
Algunas capacidades avanzadas de XMLHttpRequest, como solicitar desde otro dominio o especificar un tiempo lÃmite, no están disponibles para solicitudes sÃncronas. Tampoco, como puedes ver, la indicación de progreso.
La razón de esto es que las solicitudes sincrónicas son utilizadas muy escasamente, casi nunca. No hablaremos más sobre ellas.
Cabeceras HTTP
XMLHttpRequest permite tanto enviar cabeceras personalizadas como leer cabeceras de la respuesta.
Existen 3 métodos para las cabeceras HTTP:
setRequestHeader(name, value)-
Asigna la cabecera de la solicitud con los valores
nameyvalueprovistos.Por ejemplo:
xhr.setRequestHeader('Content-Type', 'application/json');Limitaciones de cabecerasMuchas cabeceras se administran exclusivamente por el navegador, ej.
RefereryHost. La lista completa está en la especificación.XMLHttpRequestno está permitido cambiarlos, por motivos de seguridad del usuario y la exactitud de la solicitud.No se pueden eliminar cabecerasOtra peculiaridad de
XMLHttpRequestes que no puede deshacer unsetRequestHeader.Una vez que una cabecera es asignada, ya está asignada. Llamadas adicionales agregan información a la cabecera, no la sobreescriben.
Por ejemplo:
xhr.setRequestHeader('X-Auth', '123'); xhr.setRequestHeader('X-Auth', '456'); // la cabecera será: // X-Auth: 123, 456 getResponseHeader(name)-
Obtiene la cabecera de la respuesta con el
namedado (exceptoSet-CookieySet-Cookie2).Por ejemplo:
xhr.getResponseHeader('Content-Type') getAllResponseHeaders()-
Devuelve todas las cabeceras de la respuesta, excepto por
Set-CookieySet-Cookie2.Las cabeceras se devuelven como una sola lÃnea, ej.:
Cache-Control: max-age=31536000 Content-Length: 4260 Content-Type: image/png Date: Sat, 08 Sep 2012 16:53:16 GMTEl salto de lÃnea entre las cabeceras siempre es un
"\r\n"(independiente del SO), asà podemos dividirlas en cabeceras individuales. El separador entre el nombre y el valor siempre es dos puntos seguido de un espacio": ". Eso quedó establecido en la especificación.AsÃ, si queremos obtener un objeto con pares nombre/valor, necesitamos tratarlas con un poco de JS.
Como esto (asumiendo que si dos cabeceras tienen el mismo nombre, entonces el último sobreescribe al primero):
let headers = xhr .getAllResponseHeaders() .split('\r\n') .reduce((result, current) => { let [name, value] = current.split(': '); result[name] = value; return result; }, {}); // headers['Content-Type'] = 'image/png'
POST, Formularios
Para hacer una solicitud POST, podemos utilizar el objeto FormData nativo.
La sintaxis:
let formData = new FormData([form]); // crea un objeto, opcionalmente se completa con un <form>
formData.append(name, value); // añade un campo
Lo creamos, opcionalmente lleno desde un formulario, append (agrega) más campos si se necesitan, y entonces:
xhr.open('POST', ...)â se utiliza el métodoPOST.xhr.send(formData)para enviar el formulario al servidor.
Por ejemplo:
<form name="person">
<input name="name" value="John">
<input name="surname" value="Smith">
</form>
<script>
// pre llenado del objeto FormData desde el formulario
let formData = new FormData(document.forms.person);
// agrega un campo más
formData.append("middle", "Lee");
// lo enviamos
let xhr = new XMLHttpRequest();
xhr.open("POST", "/article/xmlhttprequest/post/user");
xhr.send(formData);
xhr.onload = () => alert(xhr.response);
</script>
El formulario fue enviado con codificación multipart/form-data.
O, si nos gusta más JSON, entonces, un JSON.stringify y lo enviamos como un string.
Solo no te olvides de asignar la cabecera Content-Type: application/json, muchos frameworks del lado del servidor decodifican automáticamente JSON con este:
let xhr = new XMLHttpRequest();
let json = JSON.stringify({
name: "John",
surname: "Smith"
});
xhr.open("POST", '/submit')
xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');
xhr.send(json);
El método .send(body) es bastante omnÃvoro. Puede enviar casi cualquier body, incluyendo objetos Blob y BufferSource.
Progreso de carga
El evento progress se dispara solo en la fase de descarga.
Esto es: si hacemos un POST de algo, XMLHttpRequest primero sube nuestros datos (el cuerpo de la respuesta), entonces descarga la respuesta.
Si estamos subiendo algo grande, entonces seguramente estaremos interesados en rastrear el progreso de nuestra carga. Pero xhr.onprogress no ayuda aquÃ.
Hay otro objeto, sin métodos, exclusivamente para rastrear los eventos de subida: xhr.upload.
Este genera eventos similares a xhr, pero xhr.upload se dispara solo en las subidas:
loadstartâ carga iniciada.progressâ se dispara periódicamente durante la subida.abortâ carga abortada.errorâ error no HTTP.loadâ carga finalizada con éxito.timeoutâ carga caducada (si la propiedadtimeoutestá asignada).loadendâ carga finalizada con éxito o error.
Ejemplos de manejadores:
xhr.upload.onprogress = function(event) {
alert(`Uploaded ${event.loaded} of ${event.total} bytes`);
};
xhr.upload.onload = function() {
alert(`Upload finished successfully.`);
};
xhr.upload.onerror = function() {
alert(`Error durante la carga: ${xhr.status}`);
};
Aquà un ejemplo de la vida real: indicación del progreso de subida de un archivo:
<input type="file" onchange="upload(this.files[0])">
<script>
function upload(file) {
let xhr = new XMLHttpRequest();
// rastrea el progreso de la subida
xhr.upload.onprogress = function(event) {
console.log(`Uploaded ${event.loaded} of ${event.total}`);
};
// seguimiento completado: sea satisfactorio o no
xhr.onloadend = function() {
if (xhr.status == 200) {
console.log("Logrado");
} else {
console.log("error " + this.status);
}
};
xhr.open("POST", "/article/xmlhttprequest/post/upload");
xhr.send(file);
}
</script>
Solicitudes de origen cruzado (Cross-origin)
XMLHttpRequest puede hacer solicitudes de origen cruzado, utilizando la misma polÃtica CORS que se solicita.
Tal como fetch, no envÃa cookies ni autorización HTTP a otro origen por omisión. Para activarlas, asigna xhr.withCredentials como true:
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open('POST', 'http://anywhere.com/request');
...
Ve el capÃtulo Fetch: Cross-Origin Requests para detalles sobre las cabeceras de origen cruzado.
Resumen
Codificación tÃpica de la solicitud GET con XMLHttpRequest:
let xhr = new XMLHttpRequest();
xhr.open('GET', '/my/url');
xhr.send();
xhr.onload = function() {
if (xhr.status != 200) { // error HTTP?
// maneja el error
alert( 'Error: ' + xhr.status);
return;
}
// obtiene la respuesta de xhr.response
};
xhr.onprogress = function(event) {
// reporta progreso
alert(`Loaded ${event.loaded} of ${event.total}`);
};
xhr.onerror = function() {
// manejo de un error no HTTP (ej. red caÃda)
};
De hecho hay más eventos, la especificación moderna los lista (en el orden del ciclo de vida):
loadstartâ la solicitud ha empezado.progressâ un paquete de datos de la respuesta ha llegado, el cuerpo completo de la respuesta al momento está enresponse.abortâ la solicitud ha sido cancelada por la llamada dexhr.abort().errorâ un error de conexión ha ocurrido, ej. nombre de dominio incorrecto. No pasa con errores HTTP como 404.loadâ la solicitud se ha completado satisfactoriamente.timeoutâ la solicitud fue cancelada debido a que caducó (solo pasa si fue configurado).loadendâ se dispara después deload,error,timeoutoabort.
Los eventos error, abort, timeout, y load son mutuamente exclusivos. Solo uno de ellos puede pasar.
Los eventos más usados son la carga terminada (load), falla de carga (error), o podemos usar un solo manejador loadend y comprobar las propiedades del objeto solicitado xhr para ver qué ha pasado.
Ya hemos visto otro evento: readystatechange. Históricamente, apareció hace mucho tiempo, antes de que la especificación fuera publicada. Hoy en dÃa no es necesario usarlo; podemos reemplazarlo con eventos más nuevos, pero puede ser encontrado a menudo en scripts viejos.
Si necesitamos rastrear especÃficamente, entonces debemos escuchar a los mismos eventos en el objeto xhr.upload.
Comentarios
<code>, para varias lÃneas â envolverlas en la etiqueta<pre>, para más de 10 lÃneas â utilice una entorno controlado (sandbox) (plnkr, jsbin, codepenâ¦)