11 juillet 2023

XMLHttpRequest

XMLHttpRequest est un objet intĂ©grĂ© du navigateur qui permet de faire des requĂȘtes HTTP en JavaScript.

Bien qu’il ait le mot “XML” dans son nom, il peut fonctionner sur toutes les donnĂ©es, pas seulement au format XML. Nous pouvons upload/download des fichiers, suivre les progrĂšs et bien plus encore.

À l’heure actuelle, il existe une autre mĂ©thode, plus moderne, fetch, qui dĂ©prĂ©cie quelque peu XMLHttpRequest.

Dans le développement Web moderne, XMLHttpRequest est utilisé pour trois raisons :

  1. Raisons historiques : nous devons prendre en charge les scripts existants avec XMLHttpRequest.
  2. Nous devons prendre en charge les anciens navigateurs et nous ne voulons pas de polyfills (par exemple pour garder les scripts minuscules).
  3. Nous avons besoin de quelque chose que fetch ne peut pas encore faire, par exemple pour suivre la progression de l’upload.

Cela vous semble-t-il familier ? Si oui, alors d’accord, continuez avec XMLHttpRequest. Sinon, rendez-vous sur Fetch.

Les bases

XMLHttpRequest a deux modes de fonctionnement : synchrone et asynchrone.

Voyons d’abord l’asynchrone, car il est utilisĂ© dans la majoritĂ© des cas.

Pour faire la requĂȘte, nous avons besoin de 3 Ă©tapes :

  1. Créer XMLHttpRequest:

    let xhr = new XMLHttpRequest();

    Le constructeur n’a aucun argument.

  2. L’initialiser, gĂ©nĂ©ralement juste aprĂšs new XMLHttpRequest :

    xhr.open(method, URL, [async, user, password])

    Cette mĂ©thode spĂ©cifie les principaux paramĂštres de la requĂȘte :

    • method – MĂ©thode HTTP. Habituellement "GET" ou "POST".
    • URL – l’URL Ă  demander, une chaĂźne de caractĂšres, peut ĂȘtre l’objet URL.
    • async – si explicitement dĂ©fini sur false, alors la demande est synchrone, nous couvrirons cela un peu plus tard.
    • user, password – identifiant et mot de passe pour l’authentification HTTP de base (si nĂ©cessaire).

    Veuillez noter que l’appel open, contrairement Ă  son nom, n’ouvre pas la connexion. Il configure uniquement la demande, mais l’activitĂ© rĂ©seau ne dĂ©marre qu’avec l’appel de send.

  3. L’envoyer.

    xhr.send([body])

    Cette mĂ©thode ouvre la connexion et envoie la demande au serveur. Le paramĂštre facultatif body contient le corps de la requĂȘte.

    Certaines mĂ©thodes de requĂȘte comme GET n’ont pas de corps. Et certains d’entre eux comme POST utilisent body pour envoyer les donnĂ©es au serveur. Nous verrons des exemples de cela plus tard.

  4. Écouter les Ă©vĂ©nements xhr pour obtenir une rĂ©ponse.

    Ces trois événements sont les plus utilisés :

    • load – lorsque la requĂȘte est terminĂ©e (mĂȘme si l’état HTTP est de type 400 ou 500) et que la rĂ©ponse est entiĂšrement tĂ©lĂ©chargĂ©e.
    • error – lorsque la requĂȘte n’a pas pu ĂȘtre faite, par exemple rĂ©seau en panne ou URL non valide.
    • progress – se dĂ©clenche pĂ©riodiquement pendant le tĂ©lĂ©chargement de la rĂ©ponse, indique combien a Ă©tĂ© tĂ©lĂ©chargĂ©.
    xhr.onload = function() {
      alert(`Loaded: ${xhr.status} ${xhr.response}`);
    };
    
    xhr.onerror = function() { // ne se dĂ©clenche que si la demande n'a pas pu ĂȘtre faite du tout
      alert(`Network Error`);
    };
    
    xhr.onprogress = function(event) { // se déclenche périodiquement
      // event.loaded - combien d'octets téléchargés
      // event.lengthComputable = true si le serveur a envoyĂ© l'en-tĂȘte Content-Length
      // event.total - nombre total d'octets (si lengthComputable)
      alert(`Received ${event.loaded} of ${event.total}`);
    };

Voici un exemple complet. Le code ci-dessous charge l’URL vers /article/xmlhttprequest/example/load depuis le serveur et affiche la progression :

// 1. Créer un nouvel objet XMLHttpRequest
let xhr = new XMLHttpRequest();

// 2. Le configure : GET-request pour l'URL /article/.../load
xhr.open('GET', '/article/xmlhttprequest/example/load');

// 3. Envoyer la requĂȘte sur le rĂ©seau
xhr.send();

// 4. Ceci sera appelé aprÚs la réception de la réponse
xhr.onload = function() {
  if (xhr.status != 200) { // analyse l'état HTTP de la réponse
    alert(`Error ${xhr.status}: ${xhr.statusText}`); // e.g. 404: Not Found
  } else { // show the result
    alert(`Done, got ${xhr.response.length} bytes`); // response est la réponse du serveur
  }
};

xhr.onprogress = function(event) {
  if (event.lengthComputable) {
    alert(`Received ${event.loaded} of ${event.total} bytes`);
  } else {
    alert(`Received ${event.loaded} bytes`); // pas de Content-Length
  }

};

xhr.onerror = function() {
  alert("Request failed");
};

Une fois que le serveur a répondu, nous pouvons recevoir le résultat dans les propriétés xhr suivantes :

status
Code d’état HTTP (un nombre): 200, 404, 403 et ainsi de suite, peut ĂȘtre 0 en cas d’échec non-HTTP.
statusText
Message d’état HTTP (une chaĂźne de caractĂšres): gĂ©nĂ©ralement OK pour 200, Not Found pour 404, Forbidden pour 403 et ainsi de suite.
response (les anciens scripts peuvent utiliser responseText)
Le corps de réponse du serveur.

Nous pouvons Ă©galement spĂ©cifier un dĂ©lai d’expiration en utilisant la propriĂ©tĂ© correspondante :

xhr.timeout = 10000; // délai d'attente en ms, 10 secondes

Si la demande Ă©choue dans le dĂ©lai imparti, elle est annulĂ©e et l’évĂ©nement timeout se dĂ©clenche.

Paramùtres de recherche d’URL

Pour ajouter des paramùtres à l’URL, comme ?name=value, et assurer le bon encodage, nous pouvons utiliser l’objet URL :

let url = new URL('https://google.com/search');
url.searchParams.set('q', 'test me!');

// le paramÚtre 'q' est encodé
xhr.open('GET', url); // https://google.com/search?q=test+me%21

Type de réponse

Nous pouvons utiliser la propriété xhr.responseType pour définir le format de réponse :

  • "" (default) – obtenir en tant que chaĂźne de caractĂšres,
  • "text" – obtenir en tant que chaĂźne de caractĂšres,
  • "arraybuffer" – obtenir en tant que ArrayBuffer (pour les donnĂ©es binaires, voir le chapitre ArrayBuffer, tableaux binaires),
  • "blob" – obtenir en tant que Blob (pour les donnĂ©es binaires, voir le chapitre Blob),
  • "document" – obtenir en tant que document XML (peut utiliser XPath et d’autres mĂ©thodes XML) ou document HTML (basĂ© sur le type MIME des donnĂ©es reçues),
  • "json" – obtenir en tant que JSON (analysĂ© automatiquement).

Par exemple, obtenons la réponse en JSON :

let xhr = new XMLHttpRequest();

xhr.open('GET', '/article/xmlhttprequest/example/json');

xhr.responseType = 'json';

xhr.send();

// la réponse est {"message": "Hello, world!"}
xhr.onload = function() {
  let responseObj = xhr.response;
  alert(responseObj.message); // Hello, world!
};
Veuillez noter :

Dans les anciens scripts, vous pouvez Ă©galement trouver des propriĂ©tĂ©s xhr.responseText et mĂȘme xhr.responseXML.

Ils existent pour des raisons historiques, pour obtenir une chaßne de caractÚres ou un document XML. De nos jours, nous devons définir le format dans xhr.responseType et obtenir xhr.response comme illustré ci-dessus.

États prĂȘts

XMLHttpRequest change entre les Ă©tats au fur et Ă  mesure de sa progression. L’état actuel est accessible en tant que xhr.readyState.

Tous les États, comme dans la spĂ©cification:

UNSENT = 0; // état initial
OPENED = 1; // open appelé
HEADERS_RECEIVED = 2; // en-tĂȘtes de rĂ©ponse reçus
LOADING = 3; // la réponse est en cours de chargement (une donnée empaquetée est reçue)
DONE = 4; // requĂȘte terminĂ©e

Un objet XMLHttpRequest voyagent dans l’ordre 0 → 1 → 2 → 3 → 
 → 3 → 4. L’état 3 se rĂ©pĂšte chaque fois qu’un paquet de donnĂ©es est reçu sur le rĂ©seau.

Nous pouvons les suivre en utilisant l’évĂ©nement readystatechange :

xhr.onreadystatechange = function() {
  if (xhr.readyState == 3) {
    // chargement
  }
  if (xhr.readyState == 4) {
    // requĂȘte terminĂ©e
  }
};

Vous pouvez trouver des Ă©couteurs readystatechange dans un code trĂšs ancien, il est lĂ  pour des raisons historiques, car il fut un temps oĂč il n’y avait pas de load et d’autres Ă©vĂ©nements. De nos jours, les gestionnaires load/error/progress le dĂ©prĂ©cient.

Abandon de la requĂȘte

Nous pouvons mettre fin Ă  la requĂȘte Ă  tout moment. L’appel Ă  xhr.abort() fait cela :

xhr.abort(); // met fin Ă  la requĂȘte

Cela dĂ©clenche l’évĂ©nement abort et xhr.status devient 0.

RequĂȘtes synchrones

Si dans la méthode open le troisiÚme paramÚtre async est réglé sur false, la demande est faite de maniÚre synchrone.

En d’autres termes, l’exĂ©cution de JavaScript s’interrompt Ă  send() et reprend lorsque la rĂ©ponse est reçue. Un peu comme les commandes alert ou prompt.

Voici l’exemple réécrit, le 3Ăšme paramĂštre de open est false :

let xhr = new XMLHttpRequest();

xhr.open('GET', '/article/xmlhttprequest/hello.txt', false);

try {
  xhr.send();
  if (xhr.status != 200) {
    alert(`Error ${xhr.status}: ${xhr.statusText}`);
  } else {
    alert(xhr.response);
  }
} catch(err) { // en cas d'erreur
  alert("Request failed");
}

Cela peut sembler correct, mais les appels synchrones sont rarement utilisĂ©s, car ils bloquent le JavaScript dans la page jusqu’à la fin du chargement. Dans certains navigateurs, il devient impossible de faire dĂ©filer. Si un appel synchrone prend trop de temps, le navigateur peut suggĂ©rer de fermer la page Web “suspendue”.

De nombreuses capacitĂ©s avancĂ©es de XMLHttpRequest, comme la requĂȘte d’un autre domaine ou la spĂ©cification d’un dĂ©lai d’expiration, ne sont pas disponibles pour les demandes synchrones. De plus, comme vous pouvez le voir, aucune indication de progression.

À cause de tout cela, les requĂȘtes synchrones sont utilisĂ©es avec parcimonie, pour ainsi dire presque jamais. Nous n’en parlerons plus.

En-tĂȘtes HTTP

XMLHttpRequest permet Ă  la fois d’envoyer des en-tĂȘtes personnalisĂ©s et de lire les en-tĂȘtes Ă  partir de la rĂ©ponse.

Il existe 3 mĂ©thodes pour les en-tĂȘtes HTTP :

setRequestHeader(name, value)

DĂ©finit l’en-tĂȘte de demande avec le name donnĂ© et la value.

Par exemple :

xhr.setRequestHeader('Content-Type', 'application/json');
Limites des en-tĂȘtes

Plusieurs en-tĂȘtes sont gĂ©rĂ©s exclusivement par le navigateur, par exemple Referer et Host. La liste complĂšte est dans la spĂ©cification.

XMLHttpRequest n’est pas autorisĂ© Ă  les modifier, pour la sĂ©curitĂ© des utilisateurs et l’exactitude de la requĂȘte.

Impossible de supprimer un en-tĂȘte

Une autre particularitĂ© de XMLHttpRequest est qu’on ne peut pas annuler setRequestHeader.

Une fois l’en-tĂȘte dĂ©fini, il est dĂ©fini. Des appels supplĂ©mentaires ajoutent des informations Ă  l’en-tĂȘte, ils ne les Ă©crasent pas.

Par exemple :

xhr.setRequestHeader('X-Auth', '123');
xhr.setRequestHeader('X-Auth', '456');

// l'en-tĂȘte sera :
// X-Auth: 123, 456
getResponseHeader(name)

Obtient l’en-tĂȘte de rĂ©ponse avec le name donnĂ© (sauf Set-Cookie et Set-Cookie2).

Par exemple :

xhr.getResponseHeader('Content-Type')
getAllResponseHeaders()

Renvoie tous les en-tĂȘtes de rĂ©ponse, Ă  l’exception de Set-Cookie et Set-Cookie2.

Les en-tĂȘtes sont renvoyĂ©s sur une seule ligne, par exemple :

Cache-Control: max-age=31536000
Content-Length: 4260
Content-Type: image/png
Date: Sat, 08 Sep 2012 16:53:16 GMT

Le saut de ligne entre les en-tĂȘtes est toujours "\r\n" (ne dĂ©pend pas du systĂšme d’exploitation), nous pouvons donc facilement le diviser en en-tĂȘtes individuels. Le sĂ©parateur entre le nom et la valeur est toujours un deux-points suivi d’un espace ": ". C’est fixĂ© dans la spĂ©cification.

Donc, si nous voulons obtenir un objet avec des paires nom/valeur, nous devons ajouter un peu de JS.

Comme ceci (en supposant que si deux en-tĂȘtes ont le mĂȘme nom, alors le dernier Ă©crase l’ancien) :

let headers = xhr
  .getAllResponseHeaders()
  .split('\r\n')
  .reduce((result, current) => {
    let [name, value] = current.split(': ');
    result[name] = value;
    return result;
  }, {});

// headers['Content-Type'] = 'image/png'

POST, FormData

Pour faire une requĂȘte POST, nous pouvons utiliser l’objet intĂ©grĂ©e FormData.

La syntaxe :

let formData = new FormData([form]); // crée un objet, éventuellement remplir à partir de <form>
formData.append(name, value); // ajoute un champ

Nous le crĂ©ons, remplissons Ă©ventuellement Ă  partir d’un formulaire, ajoutons d’autres champs si nĂ©cessaire, puis :

  1. xhr.open('POST', ...) – utilise la mĂ©thode POST.
  2. xhr.send(formData) pour soumettre le formulaire au serveur.

Par exemple :

<form name="person">
  <input name="name" value="John">
  <input name="surname" value="Smith">
</form>

<script>
  // pré-remplir FormData du formulaire
  let formData = new FormData(document.forms.person);

  // ajouter un champ de plus
  formData.append("middle", "Lee");

  // l'envoie
  let xhr = new XMLHttpRequest();
  xhr.open("POST", "/article/xmlhttprequest/post/user");
  xhr.send(formData);

  xhr.onload = () => alert(xhr.response);
</script>

Le formulaire est envoyé avec un encodage multipart/form-data.

Ou, si nous aimons davantage JSON, alors JSON.stringify et l’envoyer sous forme de chaüne de caractùres.

N’oubliez juste pas de dĂ©finir l’en-tĂȘte Content-Type: application/json, de nombreux frameworks cĂŽtĂ© serveur dĂ©codent automatiquement JSON avec :

let xhr = new XMLHttpRequest();

let json = JSON.stringify({
  name: "John",
  surname: "Smith"
});

xhr.open("POST", '/submit')
xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');

xhr.send(json);

La mĂ©thode .send(body) est assez omnivore. Il peut envoyer presque n’importe quel body, y compris les objets Blob et BufferSource.

Progression de l’upload

L’évĂ©nement progress se dĂ©clenche uniquement Ă  l’étape du tĂ©lĂ©chargement.

C’est-Ă -dire: si nous envoyons via POST quelque chose, XMLHttpRequest upload d’abord nos donnĂ©es (le corps de la requĂȘte), puis tĂ©lĂ©charge la rĂ©ponse.

Si nous uploadons quelque chose de gros, alors nous sommes sĂ»rement plus intĂ©ressĂ©s Ă  suivre la progression de l’envoi. Mais xhr.onprogress n’aide pas ici.

Il existe un autre objet, sans mĂ©thodes, exclusivement pour suivre les Ă©vĂ©nements de l’envoi : xhr.upload.

Il gĂ©nĂšre des Ă©vĂ©nements, similaires Ă  xhr, mais xhr.upload les dĂ©clenche uniquement lors de l’upload :

  • loadstart – upload dĂ©marrĂ©.
  • progress – se dĂ©clenche pĂ©riodiquement pendant l’upload.
  • abort – upload annulĂ©.
  • error – erreur non-HTTP.
  • load – upload terminĂ© avec succĂšs.
  • timeout – upload expirĂ© (si la propriĂ©tĂ© timeout est dĂ©finie).
  • loadend – upload terminĂ© avec succĂšs ou erreur.

Exemple de gestionnaires :

xhr.upload.onprogress = function(event) {
  alert(`Uploaded ${event.loaded} of ${event.total} bytes`);
};

xhr.upload.onload = function() {
  alert(`Upload finished successfully.`);
};

xhr.upload.onerror = function() {
  alert(`Error during the upload: ${xhr.status}`);
};

Voici un exemple réel : upload de fichier avec indication de progression :

<input type="file" onchange="upload(this.files[0])">

<script>
function upload(file) {
  let xhr = new XMLHttpRequest();

  // suivre la progression de l'upload
  xhr.upload.onprogress = function(event) {
    console.log(`Uploaded ${event.loaded} of ${event.total}`);
  };

  // suivi de l'envoi : réussi ou non
  xhr.onloadend = function() {
    if (xhr.status == 200) {
      console.log("success");
    } else {
      console.log("error " + this.status);
    }
  };

  xhr.open("POST", "/article/xmlhttprequest/post/upload");
  xhr.send(file);
}
</script>

RequĂȘtes Cross-origin

XMLHttpRequest peut faire des requĂȘtes cross-origin, en utilisant la mĂȘme politique CORS que fetch.

Tout comme fetch, elle n’envoie pas de cookies et d’autorisation HTTP Ă  une autre origine par dĂ©faut. Pour les activer, dĂ©finissez xhr.withCredentials sur true :

let xhr = new XMLHttpRequest();
xhr.withCredentials = true;

xhr.open('POST', 'http://anywhere.com/request');
...

Voir le chapitre Fetch: RequĂȘtes Cross-Origin pour plus de dĂ©tails sur les en-tĂȘtes cross-origin.

Résumé

Code typique de la requĂȘte GET avec XMLHttpRequest :

let xhr = new XMLHttpRequest();

xhr.open('GET', '/my/url');

xhr.send();

xhr.onload = function() {
  if (xhr.status != 200) { // HTTP error?
    // handle error
    alert( 'Error: ' + xhr.status);
    return;
  }

  // obtenir la réponse de xhr.response
};

xhr.onprogress = function(event) {
  // report progress
  alert(`Loaded ${event.loaded} of ${event.total}`);
};

xhr.onerror = function() {
  // gérer les erreurs non HTTP (par exemple, panne de réseau)
};

Il y a en fait plus d’évĂ©nements, la spĂ©cification moderne les rĂ©pertorie (dans l’ordre du cycle de vie) :

  • loadstart – la requĂȘte a commencĂ©.
  • progress – un paquet de donnĂ©es de la rĂ©ponse est arrivĂ©, tout le corps de la rĂ©ponse est actuellement dans response.
  • abort – la requĂȘte a Ă©tĂ© annulĂ©e par l’appel xhr.abort().
  • error – une erreur de connexion s’est produite, par exemple nom de domaine incorrect. Ne se produit pas pour les erreurs HTTP comme 404.
  • load – la requĂȘte s’est terminĂ©e avec succĂšs.
  • timeout – la requĂȘte a Ă©tĂ© annulĂ©e en raison du dĂ©lai d’attente (ne se produit que si elle a Ă©tĂ© dĂ©finie).
  • loadend – se dĂ©clenche aprĂšs load, error, timeout ou abort.

Les Ă©vĂ©nements error, abort, timeout, et load s’excluent mutuellement. Un seul d’entre eux peut se produire.

Les Ă©vĂ©nements les plus utilisĂ©s sont la progression du chargement (load), l’échec du chargement (error), ou nous pouvons utiliser un seul gestionnaire loadend et vĂ©rifier les propriĂ©tĂ©s de l’objet de requĂȘte xhr pour voir ce qui s’est passĂ©.

Nous avons dĂ©jĂ  vu un autre Ă©vĂ©nement : readystatechange. Historiquement, il est apparu il y a longtemps, avant que la spĂ©cification ne soit rĂ©glĂ©e. De nos jours, il n’est pas nĂ©cessaire de l’utiliser, nous pouvons le remplacer par des Ă©vĂ©nements plus rĂ©cents, mais il peut souvent ĂȘtre trouvĂ© dans des scripts plus anciens.

Si nous devons suivre spĂ©cifiquement l’uplaod, alors nous devons Ă©couter les mĂȘmes Ă©vĂ©nements sur l’objet xhr.upload.

Carte du tutoriel

Commentaires

lire ceci avant de commenter

  • Si vous avez des amĂ©liorations Ă  suggĂ©rer, merci de soumettre une issue GitHub ou une pull request au lieu de commenter.
  • Si vous ne comprenez pas quelque chose dans l'article, merci de prĂ©ciser.
  • Pour insĂ©rer quelques bouts de code, utilisez la balise <code>, pour plusieurs lignes – enveloppez-les avec la balise <pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepen
)