Nuovi trucchi in XMLHttpRequest2

Introduzione

Uno degli eroi non celebrati nell'universo HTML5 è XMLHttpRequest. Tecnicamente, XHR2 non è HTML5. Tuttavia, fa parte dei miglioramenti incrementali che i fornitori di browser apportano alla piattaforma di base. Ho incluso XHR2 nella nostra nuova gamma di funzionalità perché svolge un ruolo fondamentale nelle complesse app web di oggi.

A quanto pare, il nostro vecchio amico ha subito un grande rinnovamento, ma molte persone non sono a conoscenza delle sue nuove funzionalità. XMLHttpRequest Level 2 introduce una serie di nuove funzionalità che mettono al bando gli hack complicati nelle nostre app web, come le richieste cross-origin, gli eventi di avanzamento del caricamento e il supporto per il caricamento/il download di dati binari. Queste consentono ad AJAX di lavorare in sinergia con molte delle API HTML5 più avanzate, 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

Recuperare un file come blob binario è stato complicato con XHR. Tecnicamente, non era nemmeno possibile. Un trucco ben documentato prevede la sostituzione del tipo MIME con un set di caratteri definito dall'utente, come mostrato di seguito.

Il vecchio modo 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();

Sebbene funzioni, ciò che ottieni effettivamente in responseText non è un blob binario. Si tratta di una stringa binaria che rappresenta il file immagine. Stiamo ingannando il server in modo che trasmetta i dati non elaborati. Anche se questa piccola gemma funziona, la chiamerò magia nera e sconsiglierò di utilizzarla. Ogni volta che ricorri a hack del codice carattere e manipolazione di stringhe per forzare i dati in un formato desiderato, si verifica un problema.

Specifica di un formato di risposta

Nell'esempio precedente, abbiamo scaricato l'immagine come "file" binario sovrascrivendo il tipo MIME del server ed elaborando il testo della risposta come stringa binaria. Sfrutta invece le nuove proprietà responseType e response di XMLHttpRequest per indicare al browser il formato in cui vogliamo che vengano 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. Tieni presente che l'impostazione xhr.responseType = '' (o l'omissione) imposta per impostazione predefinita la risposta su "text".
xhr.response
Dopo una richiesta andata a buon fine, la proprietà response di xhr conterrà i dati richiesti come DOMString, ArrayBuffer, Blob o Document (a seconda di ciò che è stato impostato per responseType.)

Con questa nuova funzionalità, possiamo rielaborare l'esempio precedente, ma questa volta recuperare 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 ArrayBuffer

Un ArrayBuffer è un contenitore generico di lunghezza fissa per i dati binari. Sono molto utili se hai bisogno di un buffer generalizzato di dati non elaborati, ma la vera potenza di questi elementi è che puoi creare "visualizzazioni" dei dati sottostanti utilizzando gli array con tipi di JavaScript. Infatti, è possibile creare più visualizzazioni da una singola origine ArrayBuffer. Ad esempio, puoi creare un array di interi a 8 bit che condivide lo stesso ArrayBuffer di un array di interi a 32 bit esistente dagli stessi dati. I dati sottostanti rimangono invariati, ma ne creiamo rappresentazioni diverse.

Ad esempio, il seguente comando recupera la stessa immagine come ArrayBuffer, ma questa volta crea un array di interi senza segno a 8 bit dal buffer di 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 Blob e/o non devi manipolare 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 di blob, come mostrato in questo esempio.

Invio di dati

È fantastico poter scaricare i dati in diversi formati, ma non ci porta da nessuna parte se non possiamo inviare questi formati avanzati alla base di partenza (il server). XMLHttpRequest ci ha limitato a inviare dati DOMString o Document (XML) per un po' di tempo. Non più. È stato sostituito un metodo send() ristrutturato per accettare uno dei seguenti tipi: DOMString, Document, FormData, Blob, File, ArrayBuffer. Gli esempi nel resto di questa sezione mostrano l'invio di dati utilizzando ciascun tipo.

Invio di dati di 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'è nulla di nuovo, anche se lo snippet corretto è leggermente diverso. Imposta responseType='text' per il confronto. Anche in questo caso, l'omissione di questa riga consente di ottenere gli stessi risultati.

Invio di moduli: xhr.send(FormData)

Probabilmente molte persone sono abituate a utilizzare plug-in jQuery o altre librerie per gestire l'invio di moduli AJAX. Possiamo invece utilizzare FormData, un altro nuovo tipo di dati concepito per XHR2. FormData è utile per creare un <form> HTML in tempo reale in JavaScript. Il modulo può quindi 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);
}

In sostanza, stiamo semplicemente creando dinamicamente un <form> e aggiungendogli valori <input> chiamando il metodo di accodamento.

Naturalmente, non è necessario creare un <form> da zero. Gli oggetti FormData possono essere inizializzati da un HTMLFormElement esistente nella pagina. 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ò gestirli anche. Basta aggiungere i file e il browser creerà una richiesta multipart/form-data quando viene 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 dati File o Blob utilizzando XHR. Tieni presente che tutti i File sono Blob, quindi entrambi vanno bene qui.

Questo esempio crea un nuovo file di testo da zero utilizzando il costruttore Blob() e carica Blob() sul server.Blob Il codice configura anche un gestore per informare l'utente sull'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)

Infine, 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 cross-domain a un altro dominio. È semplicissimo da attivare, in quanto richiede solo l'invio di una singola intestazione di risposta da parte del server.

Attivazione delle richieste CORS

Supponiamo che la tua applicazione si trovi su example.com e che tu voglia recuperare i dati da www.example2.com. Normalmente, se provi a effettuare questo tipo di chiamata AJAX, la richiesta non andrà a buon fine e il browser restituirà un errore di mancata corrispondenza dell'origine. Con CORS, www.example2.com puoi scegliere di consentire le richieste da example.com semplicemente aggiungendo un'intestazione:

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

Access-Control-Allow-Origin può essere aggiunto a una singola risorsa in 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 attivato 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

Attivare le richieste cross-origin è facile, quindi attiva CORS se i tuoi dati sono pubblici.

Effettuare una richiesta interdominio

Se l'endpoint del server ha attivato CORS, l'invio della richiesta cross-origin non è diverso da una normale richiesta XMLHttpRequest. Ad esempio, qui è riportata una richiesta che example.com può ora inviare 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 un mucchio di immagini, quindi salvarle localmente utilizzando il file system HTML5. Un modo per farlo è richiedere le immagini come Blobe 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();

Tagliare un file e caricare ogni parte

Utilizzando le API File, possiamo ridurre al minimo il lavoro necessario per caricare un file di grandi dimensioni. La tecnica consiste nel suddividere il caricamento in più chunk, generare un XHR per ogni parte e mettere insieme il file sul server. È simile al modo in cui Gmail carica così rapidamente gli allegati di grandi dimensioni. Questa tecnica potrebbe essere utilizzata anche per aggirare il limite di 32 MB per le richieste HTTP 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