Capítulo 17 XMLHttpRequest2

XMLHttpRequest forma parte de las mejoras incrementales que los creadores de navegadores están implementando a la plataforma base.

XMLHttpRequest de nivel 2 introduce una gran cantidad de nuevas funciones que ponen fin a los problemas de nuestras aplicaciones web, como solicitudes de origen cruzado, eventos de progreso de subidas y compatibilidad con subida/bajada de datos binarios. Esto permite a AJAX trabajar en coordinación con muchas de las API HTML5 más punteras, como API de FileSystem, el API de Web Audio y WebGL.

17.1 Recuperación de archivos

Recuperar archivos como blob binario era muy complicado con XHR. Técnicamente, no era ni siquiera posible. Un truco que se ha documentado mucho implicaba anular el tipo mime con un conjunto de caracteres definido por el usuario, como se muestra a continuación.

La antigua forma de recuperar una imagen:

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
// Hack to pass bytes through unprocessed.
xhr.overrideMimeType('text/plain; charset=x-user-defined');
xhr.onreadystatechange = function(e) {
    if (this.readyState == 4 && this.status == 200) {
        var binStr = this.responseText;
            for (var i = 0, len = binStr.length; i < len; ++i) {
                var c = binStr.charCodeAt(i);
                //String.fromCharCode(c & 0xff);
                var byte = c & 0xff;  // byte at offset i
            }
        }
    };
xhr.send();

Aunque funciona, lo que se obtiene realmente en responseText no es un blob binario, sino una cadena binaria que representa el archivo de imagen. Estamos engañando al servidor para que devuelva los datos sin procesar.

17.2 Especificación de un formato de respuesta

En lugar de realizar la acción del ejemplo anterior, vamos a aprovechar las nuevas propiedades de XMLHttpRequest (responseType y response) para indicar al navegador el formato en el que queremos que nos devuelva los datos.

  • xhr.responseType: antes de enviar una solicitud, establece xhr.responseType en text, arraybuffer, blob o document, en función de los datos que necesites. Ten en cuenta que si se establece xhr.responseType = '' (o si se omite), se utilizará la respuesta predeterminada text.
  • xhr.response: después de una solicitud correcta, la propiedad response de xhr contendrá los datos solicitados como DOMString, ArrayBuffer, Blob o Document (en función del valor establecido para responseType).

Con esto, podemos recuperar la imagen como ArrayBuffer en lugar de como una cadena. Al transferir el búfer al API BlobBuilder se crea un Blob:

BlobBuilder = window.MozBlobBuilder || window.WebKitBlobBuilder || window.BlobBuilder;
var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'arraybuffer';
xhr.onload = function(e) {
    if (this.status == 200) {
        var bb = new BlobBuilder();
        bb.append(this.response); // Note: not xhr.responseText
        var blob = bb.getBlob('image/png');
        ...
    }
};
xhr.send();

Si se quiere trabajar directamente con un Blob o no se necesita manipular ni un solo byte del archivo, utilizaremos xhr:

responseType='blob':
window.URL = window.URL || window.webkitURL;  // Take care of vendor prefixes.
var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'blob';
xhr.onload = function(e) {
    if (this.status == 200) {
        var blob = this.response;
        var img = document.createElement('img');
        img.onload = function(e) {
          window.URL.revokeObjectURL(img.src); // Clean up after yourself.
        };
        img.src = window.URL.createObjectURL(blob);
        document.body.appendChild(img);
        ...
    }
};
xhr.send();

17.3 Envío de datos

Durante algún tiempo, XMLHttpRequest nos ha limitado a enviar datos DOMString o Document (XML). Pero eso se acabó. Se ha anulado un método send() y rediseñado para aceptar todos estos tipos: DOMString, Document, FormData, Blob, File y ArrayBuffer.

  • Envío de la cadena de datos: xhr.send(domstring)
sendText('test string'); function sendTextNew(txt) {
    var xhr = new XMLHttpRequest();
    xhr.open('POST', '/server', true);
    xhr.responseType = 'text';
    xhr.onload = function(e) {
        if (this.status == 200) {
            console.log(this.response);
        }
    };
    xhr.send(txt);
}
sendText2('test string');
  • Envío de formularios: xhr.send(formdata)
function sendForm() {
    var formData = new FormData();
    formData.append('username', 'johndoe');
    formData.append('id', 123456);
    var xhr = new XMLHttpRequest();
    xhr.open('POST', '/server', true);
    xhr.onload = function(e) { ... };
    xhr.send(formData);
}

Por supuesto, no es necesario crear un objeto <form> desde cero. Los objetos FormData se pueden inicializar a partir de un elemento HTMLFormElement de la página. Por ejemplo:

<form id="myform" name="myform" action="/server">
    <input type="text" name="username" value="johndoe">
    <input type="number" name="id" value="123456">
    <input type="submit" onclick="return sendForm(this.form);">
</form>
function sendForm(form) {
    var formData = new FormData(form);
    formData.append('secret_token', '1234567890');
    var xhr = new XMLHttpRequest();
    xhr.open('POST', form.action, true);
    xhr.onload = function(e) { ... };
    xhr.send(formData);
    return false; // Prevent page from submitting.
}

Un formulario HTML puede incluir subidas de archivos (como <input type="file">) y FormData también. Simplemente se añade el archivo o los archivos y el navegador construirá una solicitud multipart/form-data cuando se ejecute send():

function uploadFiles(url, files) {
    var formData = new FormData();
    for (var i = 0, file; file = files[i]; ++i) {
    formData.append(file.name, file);
    }
    var xhr = new XMLHttpRequest();
    xhr.open('POST', url, true);
    xhr.onload = function(e) { ... };
    xhr.send(formData);  // multipart/form-data
}
document.querySelector('input[type="file"]').addEventListener('change', function(e) {
    uploadFiles('/server', this.files);
}, false);

17.4 Subida de archivos o Blob

También podemos enviar datos de File o Blob con XHR.

En este ejemplo se crea un texto nuevo desde cero con el API lobBuilder y se sube eseBlob` al servidor. El código también configura un controlador para informar al usuario sobre el progreso de la subida:

 
function upload(blobOrFile) {
    var xhr = new XMLHttpRequest();
    xhr.open('POST', '/server', true);
    xhr.onload = function(e) { ... };
    // Listen to the upload progress.
    var progressBar = document.querySelector('progress');
    xhr.upload.onprogress = function(e) {
        if (e.lengthComputable) {
            progressBar.value = (e.loaded / e.total) * 100;
            progressBar.textContent = progressBar.value; // Fallback for unsupported browsers.
        }
    };
    xhr.send(blobOrFile);
}

// Take care of vendor prefixes. BlobBuilder = window.MozBlobBuilder || window.WebKitBlobBuilder || window.BlobBuilder;

    var bb = new BlobBuilder();
    bb.append('hello world');
    upload(bb.getBlob('text/plain'));

17.5 Subida de un fragmento de bytes

Y por último, podemos enviar ArrayBuffer como la carga de XHR.

function sendArrayBuffer() {
    var xhr = new XMLHttpRequest();
    xhr.open('POST', '/server', true);
    xhr.onload = function(e) { ... };
    var uInt8Array = new Uint8Array([1, 2, 3]);
    xhr.send(uInt8Array.buffer);
}

17.6 Ejemplos prácticos

17.6.1 Cómo descargar y guardar archivos en el sistema de archivos HTML5

Supongamos que tienes una galería de imágenes y quieres recuperar un grupo de imágenes para, a continuación, guardarlas localmente con el sistema de archivos HTML5. Una forma de conseguir esto sería solicitar imágenes como ArrayBuffer, crear un Blob a partir de los datos y escribir el blob con FileWriter:

window.requestFileSystem  = window.requestFileSystem || window.webkitRequestFileSystem;
function onError(e) {
    console.log('Error', e);
}
var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'arraybuffer';
xhr.onload = function(e) {
    window.requestFileSystem(TEMPORARY, 1024 * 1024, function(fs) {
        fs.root.getFile('image.png', {create: true}, function(fileEntry) {
            fileEntry.createWriter(function(writer) {
            writer.onwrite = function(e) { ... };
            writer.onerror = function(e) { ... };
            var bb = new BlobBuilder();
            bb.append(xhr.response);
            writer.write(bb.getBlob('image/png'));
            }, onError);
        }, onError);
    }, onError);
};
xhr.send();

17.6.2 Cómo dividir un archivo y subir cada fragmento

Con las API de archivo, podemos minimizar el trabajo necesario para subir un archivo de gran tamaño. La técnica es dividir el archivo que se va a subir en varios fragmentos, crear un XHR para cada parte y unir los fragmentos en el servidor. Es similar a la forma en que Gmail sube archivos adjuntos tan grandes en tan poco tiempo.

window.BlobBuilder = window.MozBlobBuilder || window.WebKitBlobBuilder ||
                    window.BlobBuilder;
function upload(blobOrFile) {
    var xhr = new XMLHttpRequest();
    xhr.open('POST', '/server', true);
    xhr.onload = function(e) { ... };
    xhr.send(blobOrFile);
}
document.querySelector('input[type="file"]').addEventListener('change', function(e) {
    var blob = this.files[0];
    const BYTES_PER_CHUNK = 1024 * 1024; // 1MB chunk sizes.
    const SIZE = blob.size;
    var start = 0;
    var end = BYTES_PER_CHUNK;
    while(start < SIZE) {
        if ('mozSlice' in blob) {
            var chunk = blob.mozSlice(start, end);
        } else {
            var chunk = blob.webkitSlice(start, end);
        }
        upload(chunk);
        start = end;
        end = start + BYTES_PER_CHUNK;
    }
}, false);
})();