Nouvelles astuces pour XMLHttpRequest2

Introduction

XMLHttpRequest est l'un des héros méconnus de l'univers HTML5. À proprement parler, XHR2 n'est pas HTML5. Cependant, cela fait partie des améliorations incrémentielles apportées par les fournisseurs de navigateurs à la plate-forme principale. J'inclus XHR2 dans notre nouveau sac de cadeaux, car il fait partie intégrante des applications Web complexes d'aujourd'hui.

Notre vieil ami a fait peau neuve, mais beaucoup ne connaissent pas ses nouvelles fonctionnalités. Le niveau 2 de XMLHttpRequest introduit de nombreuses nouvelles fonctionnalités qui mettent fin aux piratages complexes de nos applications Web : requêtes multi-origines, importation d'événements de progression, importation et téléchargement de données binaires, etc. Elles 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 présente certaines des nouvelles fonctionnalités de XMLHttpRequest, en particulier celles qui peuvent être utilisées pour travailler avec des fichiers.

Récupération des données en cours

Avec XHR, récupérer un fichier sous forme de blob binaire était pénible. 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 illustré ci-dessous.

L'ancienne méthode pour récupérer 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 dans 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 ce petit bijou fonctionne, je vais l'appeler "magie noire" et conseiller de l'éviter. Chaque fois que vous avez recours à des piratages de code de caractères et à la manipulation de chaînes pour forcer les données dans un format souhaitable, c'est un problème.

Spécifier un format de réponse

Dans l'exemple précédent, nous avons téléchargé l'image sous forme de "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. Utilisons plutôt les nouvelles propriétés responseType et response de XMLHttpRequest pour indiquer au navigateur le format sous lequel nous souhaitons afficher les donné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 termes de données. Notez que si vous définissez xhr.responseType = '' (ou l'omission), la réponse sera "texte" par défaut.
xhr.response
Après que la requête aboutit, la propriété de réponse de xhr contient les données demandées sous la forme d'un élément DOMString, ArrayBuffer, Blob ou Document (selon ce qui a été défini pour responseType).

Grâce à cette nouveauté, nous pouvons retravailler l'exemple précédent, mais cette fois, récupérez l'image en tant que Blob plutôt qu'en tant que 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 joli !

Réponses ArrayBuffer

Un ArrayBuffer est un conteneur générique de longueur fixe pour les données binaires. Elles sont très pratiques si vous avez besoin d'un tampon généralisé de données brutes, mais leur véritable pouvoir est de vous permettre de créer des "vues" des données sous-jacentes à l'aide de tableaux typés JavaScript. En fait, plusieurs vues peuvent être créées à partir d'une seule source ArrayBuffer. Par exemple, vous pouvez créer un tableau d'entiers de 8 bits qui partage la même valeur ArrayBuffer qu'un tableau d'entiers de 32 bits existant à partir des mêmes données. Les données sous-jacentes restent les mêmes, nous en créons simplement des représentations différentes.

Par exemple, la requête suivante extrait la même image qu'une ArrayBuffer, mais cette fois, elle crée un tableau d'entiers de 8 bits non signé à 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 blob

Si vous souhaitez travailler directement avec un Blob et/ou n'avez pas besoin de manipuler des 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, y compris en l'enregistrant dans indexedDB, en l'écrivant dans le système de fichiers HTML5 ou en créant une URL Blob, comme illustré dans cet exemple.

Envoyer des données

Pouvoir télécharger des données dans différents formats est une bonne chose, mais cela ne nous mènera nulle part si nous ne pouvons pas renvoyer ces formats enrichis à notre base d'origine (le serveur). XMLHttpRequest nous a limités à 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 tous les types suivants : DOMString, Document, FormData, Blob, File, ArrayBuffer. Les exemples dans le reste de cette section illustrent l'envoi de données avec 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');

Il n'y a rien de nouveau ici, même si l'extrait de droite est légèrement différent. Elle définit responseType='text' à des fins de comparaison. Là encore, si vous omettez cette ligne, vous obtenez les mêmes résultats.

Envoi de formulaires: xhr.send(FormData)

De nombreuses personnes sont probablement habituées à utiliser des plug-ins jQuery ou d'autres bibliothèques pour gérer les envois 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 à la volée, dans 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);
}

Pour l'essentiel, nous créons simplement un <form> de manière dynamique et nous lui attribuons les valeurs <input> en appelant la méthode "app" (ajouter).

Bien entendu, vous n'avez pas besoin de créer une <form> à partir de zéro. Les objets FormData peuvent être initialisés à partir des HTMLFormElement existants 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. Il vous suffit d'ajouter le ou les fichiers, et le navigateur génère une requête multipart/form-data lorsque send() est 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);

Importation d'un fichier ou d'un blob: xhr.send(Blob)

Nous pouvons également envoyer des données File ou Blob à l'aide de XHR. N'oubliez pas que tous les File sont des Blob. Les deux fonctionnent donc ici.

Cet exemple crée un fichier texte à partir de zéro à l'aide du constructeur Blob() et importe ce fichier 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'}));

Importation d'un bloc d'octets: xhr.send(ArrayBuffer)

Enfin, nous pouvons envoyer des ArrayBuffer comme charge utile 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);
}

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, ne nécessitant qu'un seul en-tête de réponse à envoyer par le serveur.

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-concordance de l'origine. Avec CORS, www.example2.com peut choisir d'autoriser les requêtes provenant 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 sur 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é CORS sur toutes ses pages. Lancez les outils de développement. Le code Access-Control-Allow-Origin s'affiche dans notre réponse:

En-tête Access-Control-Allow-Origin sur html5rocks.com
En-tête "Access-Control-Allow-Origin" sur html5rocks.com

L'activation des requêtes multi-origines est simple. Par conséquent, veuillez activer CORS si vos données sont publiques.

Envoyer une requête inter-domaines

Si le point de terminaison du serveur a activé le CORS, 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

Imaginons que vous possédiez une galerie d'images et que vous souhaitiez récupérer un grand 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();

Segmenter un fichier et importer chaque partie

Les API File nous permettent de réduire le travail d'importation d'un fichier volumineux. La technique consiste à diviser l'importation en plusieurs fragments, à générer une requête XHR pour chaque partie, puis à rassembler le fichier sur le serveur. Ce processus est similaire à celui utilisé dans Gmail pour importer des pièces jointes volumineuses si rapidement. Cette technique peut également être utilisée pour contourner la limite de 32 Mo de requêtes HTTP imposée par 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);

})();

Ce qui n'est pas affiché ici, c'est le code permettant de reconstruire le fichier sur le serveur.

Références