Menú de navegaciónMenú
Categorías

La mejor forma de Aprender Programación online y en español www.campusmvp.es

?id=b6012e33-b275-43b6-a483-4a64ffc20108

JavaScript: Cómo recibir y enviar archivos binarios con AJAX

Logo de JavaScript procesando una cola de archivos binarios

Los objetos XHR (XmlHttpRequest) del navegador son la base de AJAX, y sirven para realizar llamadas asíncronas a nuestra propia web o a webs externas (si los permisos lo permiten), aunque en la actualidad disponemos también del método fetch para lo mismo.

Estamos muy acostumbrados a ver este tipo de llamadas para obtener un valor de texto desde un servicio. Pero no es tan habitual usarlo para recibir otro tipo de datos binarios, por ejemplo archivos completos de imágenes o vídeo. Para lograrlo antiguamente, había que recurrir a técnicas complejas y tediosas de transformación de los datos recibidos: se recibían como texto con una codificación propia y se procesaba byte a byte para lograr obtener el archivo, obteniendo una cadena de texto con el contenido binario del archivo 🤯 Un "hack" en toda regla solo para cuando no quedaba más remedio.

Hoy en día todo es mucho más fácil puesto que ya hace muchos años se le añadieron al objeto XHR sendas propiedades para lograr procesar este tipo de información:

  • responseType: por defecto sigue siendo text, pero podemos especificar otros valores (que veremos ahora mismo) para recibir datos binarios de manera que podamos procesarlos directamente.
  • response: que contrasta con la tradicional propiedad responseText que devolvía tan solo texto, y permite recibir los datos binarios en el formato adecuado.

Cuando queremos recibir datos binarios desde una petición, básicamente tenemos dos maneras de poder convertirlos en información procesable directamente desde JavaScript:

  • ArrayBuffer: es un contenedor de datos (buffer) que podemos consumir en forma de matriz (array) tipada. Esto nos permite procesarlos directamente y de forma eficiente, y además tienen la capacidad de poder crearse tantas "vistas" de estos datos como necesitemos, en forma de arrays de bytes tipadas, que abarquen la parte de los datos que nos interese.
  • Blob: este es un viejo conocido si has utilizado la File API de HTML5, que te proporciona la capacidad de leer y escribir archivos, pero es más moderno. Un blob es un Binary Large OBject, y representa también un conjunto de datos binarios inmutables sin procesar (raw, tal cual están), y nos ofrece un método (slice) para leer bloques concretos de la información como Blobs independientes.

Nota: para responseType también existen los valores "json" para recibir datos ya en ese formato, o "document" para recibir el contenido en formato HTML o XML y poder procesarlo con facilidad.

Así, por ejemplo, podemos descargar un archivo binario mediante AJAX usando un código como este:

var xhr = new XMLHttpRequest();
xhr.open('GET', '/imgs/logo-empresa.png', true);
xhr.responseType = 'blob';

xhr.onload = function() {
  if (this.status == 200) {
    var blob = this.response;
	var img = document.createElement('img');
    img.onload = function() {
      window.URL.revokeObjectURL(img.src);
    };
    img.src = window.URL.createObjectURL(blob);
    document.body.appendChild(img);
};

xhr.send();

En este caso lo estamos descargando como Blob, para lo cual establecemos el responseType como 'blob'. Al asignar la respuesta a una variable, la obtenemos directamente como Blob al llamar a response, ya que el propio objeto XHR es el que se encarga de hacerlo (debido al responseType que le hemos indicado). Lo que hacemos con él en este caso es crear un nuevo objeto en memoria a partir del Blob con createObjectURL, el cual limpiamos mediante revokeObjectUrl tras haberlo asignado a una imagen (una vez haya cargado).

Nota: estos "URL de objeto" son direcciones especiales del navegador que apuntan a los objetos que hemos creado y que están en memoria. Suelen tener un formato muy sencillo de tipo: blob:https://midominio.com/dba53d8e9638-4421-878b-8d41-f5c2f1b7, siendo el GUID de la dirección el que identifica al objeto creado en memoria. En cuanto cerramos la pestaña del navegador en donde se han creado, se eliminan de memoria por lo que no vuelven a estar disponibles. Esto es útil para muchas cosas, como por ejemplo para mostrar imágenes que están previamente en disco o que recibimos desde otro servicio como en nuestro ejemplo.

Aunque podemos enviar y recibir datos usando tanto un ArrayBuffer como un Blob, tienen varias diferencias entre sí que conviene conocer.

ArrayBuffer

Un ArrayBuffer es el objeto binario más básico que podemos construir con JavaScript y es una referencia a un área de memoria determinada. O sea, es un puntero a un número determinado de posiciones de memoria (bytes) con los que podemos trabajar.

Para poder usarlo tenemos que crear una "vista" sobre estos datos, de modo que podamos manipularlos directamente. Para ello necesitaremos un array tipado de JavaScript que puede ser de varios tipos. Por ejemplo, para leer el contenido del ArrayBuffer byte a byte podemos usar un Uint8Array, así:

var xhr = new XMLHttpRequest();
xhr.open('GET', '/imgs/logo-empresa.png', true);
xhr.responseType = 'arraybuffer';

xhr.onload = function(e) {
    var arrBuff = this.response;
    var ui8Arr = new Uint8Array(arrBuff);
    ...
};

xhr.send();

Ahora podríamos manipular byte a byte los datos de esa imagen para hacer con ella lo que necesitásemos. Incluso podríamos tener "vistas" diferentes al mismo tiempo a partir del mismo ArrayBuffer para manipularlos de manera diferente según la "vista" que creemos.

Estos arrays tipados se parecen mucho a los arrays normales de JavaScript pero no soportan los métodos splice (no se pueden eliminar elementos) ni concat (no se pueden juntar), pero sí podemos iterar por ellos y modificarlos, usar map/reduce, extraer trozos con slice, localizarlos con find, etc...

Adicionalmente tienen dos métodos set y subarray para copiar y extraer elementos entre vistas tipadas. Disponen del método length que te dará su longitud como en cualquier array, pero también un método byteLength que siempre devuelve el tamaño en bytes de los datos (tú puedes estar usando una vista de 32 bits, y su longitud en bytes sería 4 veces mayor, claro).

También podemos usar un objeto especial llamado DataView que permite acceder a los contenidos del ArrayBuffer desde cualquier posición y longitud (new DataView(buffer, [byteOffset], [byteLength])), pudiendo además extraer los datos en varios tamaños (.getUint8(), .getUint16(), .getUint32()...) sin tener que crear vistas diferentes para cada caso, pudiendo escribirlos también .setUtin8(), .setUint16(), .setUint32().

Blobs

Un Blob, es un elemento inmutable de longitud fija que normalmente representa un archivo y que de hecho no tiene ni siquiera por qué contener datos representables en JavaScript. Puede estar en disco, en una caché, en memoria o incluso no estar disponible por completo hasta que lo utilicemos. Llevan asociado también un tipo MIME, como cualquier archivo o recurso.

Generalmente no manipulamos un Blob de ninguna manera, sino que lo pasamos directamente a algún método para que trabaje con él. Por ejemplo, lo podemos obtener a partir de un archivo en disco con la File API y pasarlo a un XHR para enviarlo al servidor.

Los blobs tienen un método slice() que permite extraer trozos del mismo en forma de un nuevo blob. Por ejemplo, este método se puede utilizar para trocear un blob muy grande e ir enviándolo al servidor por trozos, poco a poco, con lo que conseguimos mayor velocidad (porque pueden enviarse en paralelo, como hace GMail por ejemplo) y saltarnos posibles limitaciones de tamaño máximo de archivo recibido en el servidor:

document.querySelector('input[type="file"]').addEventListener('change', function(e) {
    var blob = this.files[0];
    var filename = this.files[0].name;

    const CHUNK_SIZE = 1024*1024; //Lo troceamos de 1Mb en 1Mb.

    var start = 0;
    var end = CHUNK_SIZE;
    var chunkNmbr = 1;	//Contará el trozo en el que vamos
    //El número de trozos en los que se va a dividir el archivo
    var numberOfChunks = Math.Floor(blob.size/CHUNK_SIZE);
    if (blob.size % CHUNK_SIZE > 0 ) numberOfChunks++;

    while(start < blob.size) {
      uploadChunk(filename, blob.slice(start, end), chunkNmbr);

      start = end;  //Comienzo del siguiente trozo
      end = start + CHUNK_SIZE; //Final del siguiente trozo
    }
});

function uploadChunk(filename, blobChunk, chunkNumber) {
    //Generamos los datos e enviar que llevan...
    var frmData = new FormData();
    //...el archivo con su nombre, y...
    frmData.append('file', blobchunk, filename);
    //...el número de trozo que se recibe, para guardarlo en el orden adecuado
    frmData.append('chunkNum', chunkNumber);

    var xhr = new XMLHttpRequest();
    xhr.open('POST', '/receiveFile', true);
    xhr.data
    xhr.send(frmData);
}

En este fragmento se localizan todos los campos de tipo file que hay, en los que el usuario puede elegir archivos para subir, y cuando elija alguno (evento change) lo que hace el código es trocearlos en fragmentos de 1Mb (máximo) e ir enviándolos uno a uno al servidor, indicando el nombre del archivo y el orden de cada trozo. Obviamente el servidor tiene que actuar en consecuencia y al recibir esos trozos juntarlos y almacenarlos donde proceda (lo cual dependerá del lenguaje de servidor que utilices).

Por supuesto, el código anterior se podría haber hecho con la API de fetch de manera muy parecida.

¿ArrayBuffer o Blob?

Salvo que necesitemos hacer un procesamiento extra con la información, por regla general lo que utilizaremos serán Blobs. Si quisiésemos transformar los datos de alguna manera tras haberlos recibido o antes de enviarlos, necesitaríamos un ArrayBuffer que permite su manipulación a través de un DataView o permite leerlo de diversas maneras con un array tipado como hemos visto.

Por otro lado, siempre podemos transformar un Blob en un ArrayBuffer y viceversa si lo necesitamos:

  • Los Blob tienen un método asíncrono arrayBuffer() que permite obtener un ArrayBuffer a partir del Blob.
  • Podemos obtener un Blob a partir de una vista de un ArrayBuffer simplemente instanciándolo: var arrBuf = new Blob(new Uint8Array(datosEnUnArrayBuffer));.

¡Espero que te resulte útil!

José Manuel Alarcón Fundador de campusMVP, es ingeniero industrial y especialista en consultoría de empresa. Ha escrito diversos libros, habiendo publicado hasta la fecha cientos de artículos sobre informática e ingeniería en publicaciones especializadas. Microsoft lo ha reconocido como MVP (Most Valuable Professional) en desarrollo web desde el año 2004 hasta la actualidad. Puedes seguirlo en Twitter en @jm_alarcon o leer sus blog técnico o personal. Ver todos los posts de José Manuel Alarcón
Archivado en: Desarrollo Web

¿Te ha gustado este post?
Pues espera a ver nuestro boletín...

Suscríbete a la newsletter

La mejor formación online para desarrolladores como tú

Agregar comentario

Los datos anteriores se utilizarán exclusivamente para permitirte hacer el comentario y, si lo seleccionas, notificarte de nuevos comentarios en este artículo, pero no se procesarán ni se utilizarán para ningún otro propósito. Lee nuestra política de privacidad.