Introduction
XMLHttpRequest
est l'un des héros méconnus de l'univers HTML5.
À strictement parler, XHR2 n'est pas HTML5. Toutefois, il fait partie des améliorations incrémentielles apportées par les fournisseurs de navigateurs à la plate-forme de base. J'inclus XHR2 dans notre nouvelle sélection de fonctionnalités, car il joue un rôle essentiel dans les applications Web complexes d'aujourd'hui.
Il s'a avéré que notre vieil ami avait subi un lifting complet, mais de nombreuses personnes ne connaissent pas ses nouvelles fonctionnalités. XMLHttpRequest Level 2 introduit de nombreuses nouvelles fonctionnalités qui mettent fin aux piratages complexes dans nos applications Web, comme les requêtes inter-origines, les événements de progression de l'importation et la prise en charge de l'importation/du téléchargement de données binaires. Ils permettent à AJAX de fonctionner de concert avec de nombreuses API HTML5 de pointe telles que l'API File System, l'API Web Audio et WebGL.
Ce tutoriel met en avant certaines des nouvelles fonctionnalités de XMLHttpRequest
, en particulier celles qui peuvent être utilisées pour gérer des fichiers.
Récupération des données en cours
La récupération d'un fichier en tant que blob binaire était difficile avec XHR. Techniquement, ce n'était même pas possible. Une astuce bien documentée consiste à remplacer le type MIME par un jeu de caractères défini par l'utilisateur, comme indiqué ci-dessous.
Ancienne méthode d'extraction d'une image:
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();
Bien que cela fonctionne, ce que vous obtenez réellement dans le responseText
n'est pas un blob binaire. Il s'agit d'une chaîne binaire représentant le fichier image.
Nous trompons le serveur pour qu'il renvoie les données non traitées.
Même si cette petite merveille fonctionne, je vais l'appeler "magie noire" et vous déconseiller de l'utiliser. Chaque fois que vous recourez à des piratages de code de caractères et à la manipulation de chaînes pour forcer les données dans un format souhaité, cela pose problème.
Spécifier un format de réponse
Dans l'exemple précédent, nous avons téléchargé l'image en tant que "fichier" binaire en remplaçant le type mime du serveur et en traitant le texte de la réponse en tant que chaîne binaire.
À la place, utilisons les nouvelles propriétés responseType
et response
de XMLHttpRequest
pour indiquer au navigateur le format dans lequel nous souhaitons que les données soient renvoyées.
- xhr.responseType
- Avant d'envoyer une requête, définissez
xhr.responseType
sur "text", "arraybuffer", "blob" ou "document", en fonction de vos besoins en données. Notez que le paramètrexhr.responseType = ''
(ou l'omission) définira la réponse par défaut sur "text". - xhr.response
- Après une requête réussie, la propriété de réponse de l'objet xhr contient les données demandées sous forme de
DOMString
,ArrayBuffer
,Blob
ouDocument
(selon ce qui a été défini pourresponseType
).
Grâce à cette nouvelle fonctionnalité, nous pouvons retravailler l'exemple précédent, mais cette fois, récupérez l'image en tant que Blob
au lieu d'une chaîne:
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();
C'est beaucoup plus agréable !
Réponses ArrayBuffer
Un ArrayBuffer
est un conteneur générique à longueur fixe pour les données binaires. Ils sont très pratiques si vous avez besoin d'un tampon généralisé de données brutes, mais leur véritable force réside dans le fait que vous pouvez créer des "vues" des données sous-jacentes à l'aide d'tableaux typés JavaScript.
En fait, vous pouvez créer plusieurs vues à partir d'une seule source ArrayBuffer
.
Par exemple, vous pouvez créer un tableau d'entiers 8 bits qui partage le même ArrayBuffer
qu'un tableau d'entiers 32 bits existant à partir des mêmes données. Les données sous-jacentes restent les mêmes, nous créons simplement différentes représentations.
Par exemple, l'instruction suivante extrait la même image en tant que ArrayBuffer
, mais cette fois, elle crée un tableau d'entiers 8 bits non signés à partir de ce tampon de données:
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();
Réponses de blob
Si vous souhaitez travailler directement avec un Blob
et/ou que vous n'avez pas besoin de manipuler les octets du fichier, utilisez 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
peut être utilisé à plusieurs endroits, par exemple pour l'enregistrer dans indexedDB, l'écrire dans le fichier système HTML5 ou créer une URL de blob, comme illustré dans cet exemple.
Envoyer des données
Il est très utile de pouvoir télécharger des données dans différents formats, mais cela ne nous mènera nulle part si nous ne pouvons pas renvoyer ces formats riches à la base (le serveur).
XMLHttpRequest
nous a limité à l'envoi de données DOMString
ou Document
(XML) depuis un certain temps. Plus vraiment. Une méthode send()
remaniée a été remplacée pour accepter l'un des types suivants : DOMString
, Document
, FormData
, Blob
, File
et ArrayBuffer
. Les exemples du reste de cette section illustrent l'envoi de données à l'aide de chaque type.
Envoyer des données de chaîne: 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');
Rien de nouveau ici, mais l'extrait de code de droite est légèrement différent.
Il définit responseType='text'
pour la comparaison. Encore une fois, l'omission de cette ligne donne les mêmes résultats.
Envoyer des formulaires: xhr.send(FormData)
De nombreuses personnes sont probablement habituées à utiliser des plug-ins jQuery ou d'autres bibliothèques pour gérer l'envoi de formulaires AJAX. À la place, nous pouvons utiliser FormData
, un autre nouveau type de données conçu pour XHR2. FormData
est pratique pour créer un <form>
HTML instantanément, en JavaScript.
Ce formulaire peut ensuite être envoyé à l'aide d'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);
}
En substance, nous ne créons qu'un <form>
dynamiquement et y ajoutons des valeurs <input>
en appelant la méthode d'ajout.
Bien sûr, vous n'avez pas besoin de créer un <form>
à partir de zéro.
Les objets FormData
peuvent être initialisés à partir d'un HTMLFormElement
existant sur la page. Exemple :
<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 formulaire HTML peut inclure des importations de fichiers (par exemple, <input type="file">
), et FormData
peut également gérer cela. Ajoutez simplement le ou les fichiers, et le navigateur créera une requête multipart/form-data
lorsque send()
sera appelé:
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);
Importer un fichier ou un blob: xhr.send(Blob)
Vous pouvez également envoyer des données File
ou Blob
à l'aide de XHR.
N'oubliez pas que tous les File
sont des Blob
. Vous pouvez donc utiliser l'un ou l'autre.
Cet exemple crée un fichier texte à partir de zéro à l'aide du constructeur Blob()
et importe ce Blob
sur le serveur. Le code configure également un gestionnaire pour informer l'utilisateur de la progression de l'importation:
<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'}));
Importer un bloc d'octets: xhr.send(ArrayBuffer)
Enfin, nous pouvons envoyer des ArrayBuffer
en tant que charge utile de l'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);
}
Partage des ressources entre origines multiples (CORS)
CORS permet aux applications Web d'un domaine d'envoyer des requêtes AJAX interdomaines à un autre domaine. Il est très simple à activer, car le serveur n'a besoin d'envoyer qu'un seul en-tête de réponse.
Activer les requêtes CORS
Supposons que votre application se trouve sur example.com
et que vous souhaitiez extraire des données de www.example2.com
. Normalement, si vous essayez d'effectuer ce type d'appel AJAX, la requête échoue et le navigateur génère une erreur de non-correspondance d'origine. Avec CORS, www.example2.com
peut choisir d'autoriser les requêtes de example.com
en ajoutant simplement un en-tête:
Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Origin
peut être ajouté à une seule ressource sous un site ou à l'ensemble du domaine. Pour autoriser n'importe quel domaine à vous envoyer une requête, définissez les éléments suivants:
Access-Control-Allow-Origin: *
En fait, ce site (html5rocks.com) a activé le CORS sur toutes ses pages. Lancez les outils pour les développeurs. Vous verrez Access-Control-Allow-Origin
dans notre réponse:
Il est facile d'activer les requêtes inter-origines. Alors, activez CORS si vos données sont publiques.
Envoyer une requête interdomaine
Si le point de terminaison du serveur a activé CORS, l'envoi de la requête multi-origine ne diffère pas d'une requête XMLHttpRequest
normale. Par exemple, voici une requête que example.com
peut désormais envoyer à 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();
Exemples pratiques
Télécharger et enregistrer des fichiers dans le système de fichiers HTML5
Supposons que vous disposiez d'une galerie d'images et que vous souhaitiez extraire un certain nombre d'images, puis les enregistrer localement à l'aide du système de fichiers HTML5.
Pour ce faire, vous pouvez demander des images en tant que Blob
et les écrire à l'aide de 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();
Découper un fichier et importer chaque portion
Grâce aux API File, nous pouvons réduire le travail d'importation d'un fichier volumineux. La technique consiste à diviser l'importation en plusieurs segments, à générer une requête XHR pour chaque partie et à assembler le fichier sur le serveur. Cela ressemble à la façon dont Gmail importe des pièces jointes volumineuses si rapidement. Une telle technique peut également être utilisée pour contourner la limite de 32 Mo de requêtes HTTP de 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);
})();
Le code permettant de reconstruire le fichier sur le serveur n'est pas affiché ici.
Références
- Spécification XMLHttpRequest de niveau 2
- Spécification du partage de ressources entre origines (CORS)
- Spécification de l'API File
- Spécification de l'API FileSystem