Nuovi trucchi in XMLHttpRequest2

Introduzione

Uno degli eroi non celebrati nell'universo HTML5 è XMLHttpRequest. A rigore, XHR2 non è HTML5. Tuttavia, tali miglioramenti rientrano nei miglioramenti incrementali apportati dai fornitori di browser alla piattaforma di base. Includo XHR2 nella nostra nuova serie di vantaggi perché è parte integrante delle complesse app web odierne.

Abbiamo scoperto che il nostro vecchio amico ha cambiato look, ma molte persone non sono consapevoli delle sue nuove funzionalità. XMLHttpRequest Level 2 introduce una serie di nuove funzionalità che pongono fine a complicati attacchi nelle nostre app web, come le richieste multiorigine, gli eventi di avanzamento del caricamento e il supporto per il caricamento/download dei dati binari. Questi consentono a AJAX di funzionare insieme a molte delle API HTML5 al limite, come l'API File System, l'API Web Audio e WebGL.

Questo tutorial mette in evidenza alcune delle nuove funzionalità di XMLHttpRequest, in particolare quelle che possono essere utilizzate per lavorare con i file.

Recupero dati

Il recupero di un file come BLOB binario è stato complicato con XHR. Tecnicamente, non era nemmeno possibile. Un trucco ben documentato è quello di sostituire il tipo MIME con un set di caratteri definito dall'utente, come mostrato di seguito.

Il metodo precedente per recuperare un'immagine:

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();

Anche se funziona, ciò che in realtà viene restituito nel responseText non è un blob binario. È una stringa binaria che rappresenta il file immagine. Stiamo inducendo con l'inganno il server a ritrasmettere i dati, non elaborati. Anche se questa piccola gemma funziona, la chiamerò magia nera e la consiglierò. Ogni volta che ricorri a compromissioni del codice dei caratteri e manipolazione delle stringhe per forzare i dati in un formato auspicabile,

Specificare un formato di risposta

Nell'esempio precedente, abbiamo scaricato l'immagine come "file" binario, sostituendo il tipo MIME del server ed elaborando il testo della risposta come stringa binaria. Utilizziamo invece le nuove proprietà responseType e response di XMLHttpRequest per informare il browser in quale formato devono essere restituiti i dati.

xhr.responseType
Prima di inviare una richiesta, imposta xhr.responseType su "text", "arraybuffer", "blob" o "document", a seconda delle tue esigenze di dati. Nota: se imposti xhr.responseType = '' (o omettilo), la risposta predefinita sarà "testo".
xhr.response
Dopo una richiesta andata a buon fine, la proprietà di risposta della xhr conterrà i dati richiesti come DOMString, ArrayBuffer, Blob o Document (a seconda di quanto impostato per responseType).

Grazie a questa nuova meraviglia, possiamo rielaborare l'esempio precedente, ma questa volta recupera l'immagine come Blob anziché come stringa:

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'blob';

xhr.onload = function(e) {
  if (this.status == 200) {
    // Note: .response instead of .responseText
    var blob = new Blob([this.response], {type: 'image/png'});
    ...
  }
};

xhr.send();

Molto meglio!

Risposte Arraybu

Un ArrayBuffer è un container generico a lunghezza fissa per dati binari. Sono molto utili se hai bisogno di un buffer generalizzato dei dati non elaborati, ma la vera potenza di questi ragazzi è che puoi creare "viste" dei dati sottostanti utilizzando array di tipo JavaScript. Infatti, è possibile creare più visualizzazioni da un'unica sorgente ArrayBuffer. Ad esempio, puoi creare un array di numeri interi a 8 bit che condivida lo stesso ArrayBuffer di un array di numeri interi a 32 bit esistente dagli stessi dati. I dati sottostanti rimangono gli stessi, ne creiamo solo rappresentazioni diverse.

Ad esempio, quanto riportato di seguito recupera la nostra stessa immagine come ArrayBuffer, ma questa volta crea un array di numeri interi a 8 bit senza firma dal buffer dei dati:

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'arraybuffer';

xhr.onload = function(e) {
  var uInt8Array = new Uint8Array(this.response); // this.response == uInt8Array.buffer
  // var byte3 = uInt8Array[4]; // byte at offset 4
  ...
};

xhr.send();

Risposte BLOB

Se vuoi lavorare direttamente con un elemento Blob e/o non hai bisogno di modificare i byte del file, utilizza 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();

Un Blob può essere utilizzato in diversi modi, ad esempio salvandolo in indexedDB, scrivendolo nel file system HTML5 o creando un URL BLOB, come mostrato in questo esempio.

Invio di dati

Essere in grado di scaricare dati in diversi formati è ottimo, ma non ci arriva da nessuna parte se non riusciamo a inviare questi formati avanzati alla home page (il server). XMLHttpRequest ci ha limitato all'invio di dati DOMString o Document (XML) per un certo periodo di tempo. Ora non più. È stato eseguito l'override di un metodo send() aggiornato per accettare i seguenti tipi: DOMString, Document, FormData, Blob, File, ArrayBuffer. Gli esempi nella parte restante di questa sezione mostrano l'invio di dati utilizzando ciascun tipo.

Invio dei dati della stringa: xhr.send(DOMString)

function sendText(txt) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) {
    if (this.status == 200) {
      console.log(this.responseText);
    }
  };

  xhr.send(txt);
}

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);
}

sendTextNew('test string');

Non c'è niente di nuovo qui, anche se lo snippet destro è leggermente diverso. Imposta responseType='text' per il confronto. Anche in questo caso, l'omissione di questa linea produce gli stessi risultati.

Invio dei moduli: xhr.send(FormData)

Molte persone sono probabilmente abituate a utilizzare i plug-in jQuery o altre librerie per gestire gli invii di moduli AJAX. Possiamo invece utilizzare FormData, un altro nuovo tipo di dati concepito per XHR2. FormData è un'opzione comoda per creare <form> HTML all'istante, in JavaScript. Il modulo può essere inviato utilizzando AJAX:

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);
}

Essenzialmente, stiamo solo creando dinamicamente un <form> e applicando i valori <input> al suo interno chiamando il metodo di aggiunta.

Naturalmente, non è necessario creare un <form> da zero. Gli oggetti FormData possono essere inizializzati dalla pagina HTMLFormElement ed esistenti. Ad esempio:

<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'); // Append extra data before send.

  var xhr = new XMLHttpRequest();
  xhr.open('POST', form.action, true);
  xhr.onload = function(e) { ... };

  xhr.send(formData);

  return false; // Prevent page from submitting.
}

Un modulo HTML può includere caricamenti di file (ad es. <input type="file">) e FormData può gestire anche questo tipo di caricamenti. Aggiungi semplicemente i file e il browser creerà una richiesta multipart/form-data quando verrà chiamato 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);

Caricamento di un file o di un blob: xhr.send(Blob)

Possiamo anche inviare i dati File o Blob utilizzando l'XHR. Tieni presente che tutti gli File sono Blob, quindi funzionano entrambi qui.

In questo esempio, viene creato un nuovo file di testo da zero utilizzando il costruttore Blob() e viene caricato Blob sul server. Il codice imposta anche un gestore per informare l'utente dell'avanzamento del caricamento:

<progress min="0" max="100" value="0">0% complete</progress>
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);
}

upload(new Blob(['hello world'], {type: 'text/plain'}));

Caricamento di un blocco di byte: xhr.send(ArrayBuffer)

Ultimo ma non meno importante, possiamo inviare ArrayBuffer come payload dell'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);
}

Condivisione delle risorse tra origini (CORS)

CORS consente alle applicazioni web su un dominio di effettuare richieste AJAX tra domini a un altro dominio. È molto semplice da attivare, richiede che il server invii una sola intestazione di risposta.

Abilitazione delle richieste CORS

Supponiamo che la tua applicazione sia su example.com e che tu voglia eseguire il pull dei dati da www.example2.com. Normalmente, se provassi a effettuare questo tipo di chiamata AJAX, la richiesta non andrebbe a buon fine e il browser restituirà un errore di mancata corrispondenza dell'origine. Con CORS, www.example2.com può scegliere di consentire le richieste provenienti da example.com semplicemente aggiungendo un'intestazione:

Access-Control-Allow-Origin: http://example.com

Puoi aggiungere Access-Control-Allow-Origin a una singola risorsa all'interno di un sito o nell'intero dominio. Per consentire a qualsiasi dominio di inviarti una richiesta, imposta:

Access-Control-Allow-Origin: *

Infatti, questo sito (html5rocks.com) ha abilitato CORS su tutte le sue pagine. Avvia gli Strumenti per sviluppatori e vedrai Access-Control-Allow-Origin nella nostra risposta:

Intestazione Access-Control-Allow-Origin su html5rocks.com
Intestazione "Access-Control-Allow-Origin" su html5rocks.com

Abilitare le richieste multiorigine è facile, quindi attiva CORS se i tuoi dati sono pubblici.

Effettuare una richiesta interdominio

Se l'endpoint del server ha abilitato CORS, l'esecuzione di una richiesta multiorigine non è diversa da una normale richiesta XMLHttpRequest. Ad esempio, ecco una richiesta che example.com ora può fare a www.example2.com:

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://www.example2.com/hello.json');
xhr.onload = function(e) {
  var data = JSON.parse(this.response);
  ...
}
xhr.send();

Esempi pratici

Scaricare e salvare i file nel file system HTML5

Supponiamo che tu abbia una galleria di immagini e voglia recuperare una serie di immagini per poi salvarle localmente utilizzando il file system HTML5. Un modo per farlo è richiedere le immagini come Blob e scriverle utilizzando 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 = 'blob';

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 blob = new Blob([xhr.response], {type: 'image/png'});

        writer.write(blob);

      }, onError);
    }, onError);
  }, onError);
};

xhr.send();

Scomporre un file e caricare ogni parte

Utilizzando le API di file, possiamo ridurre al minimo il lavoro necessario per caricare file di grandi dimensioni. La tecnica prevede di suddividere il caricamento in più blocchi, generare un XHR per ogni parte e mettere insieme il file sul server. È un modo simile a come Gmail carica rapidamente allegati di grandi dimensioni. Questa tecnica può essere utilizzata anche per aggirare il limite di richieste HTTP di 32 MB di Google App Engine.

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) {
    upload(blob.slice(start, end));

    start = end;
    end = start + BYTES_PER_CHUNK;
  }
}, false);

})();

Qui non viene mostrato il codice per ricostruire il file sul server.

Riferimenti