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'impostazionexhr.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
oDocument
(a seconda di ciò che è stato impostato perresponseType
.)
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:
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 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();
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
- Specifica XMLHttpRequest Level 2
- Specifica Cross-Origin Resource Sharing (CORS)
- Specifiche dell'API File
- Specifica dell'API FileSystem