Nouvelles astuces pour XMLHttpRequest2

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ètre xhr.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 ou Document (selon ce qui a été défini pour responseType).

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:

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

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