23 octobre 2022

Fetch

JavaScript peut envoyer des requêtes réseau au serveur et charger de nouvelles informations chaque fois que nécessaire.

Par exemple, nous pouvons utiliser une requête réseau pour :

  • Soumettre une commande,
  • Charger des informations utilisateur,
  • Recevoir les dernières mises à jour du serveur,
  • …etc.

… Et tout cela sans recharger la page !

Il y a un terme générique “AJAX” (abrégé de Asynchronous JavaScript And XML) pour les requêtes réseau à partir de JavaScript. Cependant nous n’avons pas besoin d’utiliser XML : le terme vient de l’ancien temps, c’est pourquoi ce mot est là. Vous avez peut-être déjà entendu ce terme.

Il existe plusieurs façons d’envoyer une requête réseau et d’obtenir des informations du serveur.

La méthode fetch() est moderne et polyvalente, nous allons donc commencer avec celle-ci. Elle n’est pas prise en charge par les anciens navigateurs (peut être polyfilled), mais très bien prise en charge par les navigateurs modernes.

La syntaxe de base est :

let promise = fetch(url, [options])
  • url – l’URL cible.
  • options – paramètres facultatifs : méthode, en-têtes, etc…

Sans options, c’est une simple requête GET, téléchargeant le contenu de l’url.

Le navigateur démarre la requête immédiatement et renvoie une promesse que le code appelant devrait utiliser pour obtenir le résultat.

Obtenir une réponse est généralement un processus en deux étapes.

Premièrement, la promise, renvoyée par fetch, se résout avec un objet de la classe intégrée Response dès que le serveur répond avec des en-têtes.

À ce stade, nous pouvons vérifier l’état HTTP, pour voir s’il est réussi ou non, vérifier les en-têtes, mais nous ne disposons pas encore du corps.

La promesse rejette si le fetch n’a pas pu faire de requête HTTP, par exemple problèmes de réseau, ou si l’adresse n’existe pas. Les statuts HTTP anormaux, tels que 404 ou 500, ne provoquent pas d’erreur.

Nous pouvons voir l’état HTTP dans les propriétés de réponse :

  • status – Code d’état HTTP, par exemple 200.
  • ok – booléen, true si le code d’état HTTP est 200-299.

Par exemple :

let response = await fetch(url);

if (response.ok) { // if HTTP-status is 200-299
  // obtenir le corps de réponse (la méthode expliquée ci-dessous)
  let json = await response.json();
} else {
  alert("HTTP-Error: " + response.status);
}

Deuxièmement, pour obtenir le corps de la réponse, nous devons utiliser un appel de méthode supplémentaire.

Response fournit plusieurs méthodes basées sur les promesses pour accéder au corps dans différents formats :

  • response.text() – lit la réponse et retourne sous forme de texte,
  • response.json() – analyse la réponse en JSON,
  • response.formData() – retourne la réponse en tant que objet FormData (expliqué dans le chapitre suivant),
  • response.blob() – retourne la réponse en tant que Blob (donnée binaire avec type),
  • response.arrayBuffer() – retourne la réponse en tant que ArrayBuffer (représentation de bas niveau de donnée binaire),
  • aditionellement, response.body est un objet ReadableStream, qui permet de lire le corps morceau par morceau, nous verrons un exemple plus tard.

Par exemple, obtenons un objet JSON avec les derniers commits de GitHub :

let url = 'https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits';
let response = await fetch(url);

let commits = await response.json(); // lire le corps de réponse et analyser en JSON

alert(commits[0].author.login);

Ou, la même chose sans await, en utilisant la syntaxe des promesses pures :

fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits')
  .then(response => response.json())
  .then(commits => alert(commits[0].author.login));

Pour obtenir la réponse en texte, await response.text() au lieu de .json() :

let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');

let text = await response.text(); // lire le corps de la réponse sous forme de texte

alert(text.slice(0, 80) + '...');

En tant que vitrine pour la lecture au format binaire, récupérons et affichons une image du logo de “fetch” specification (voir le chapitre Blob pour plus de détails sur les opérations de Blob):

let response = await fetch('/article/fetch/logo-fetch.svg');

let blob = await response.blob(); // télécharger en tant qu'objet Blob

// create <img> for it
let img = document.createElement('img');
img.style = 'position:fixed;top:10px;left:10px;width:100px';
document.body.append(img);

// l'afficher
img.src = URL.createObjectURL(blob);

setTimeout(() => { // le cacher après 3 secondes
  img.remove();
  URL.revokeObjectURL(img.src);
}, 3000);
Important :

Nous ne pouvons choisir qu’une seule méthode de lecture du corps.

Si nous avons déjà la réponse avec response.text(), alors response.json() ne fonctionnera pas, car le contenu du corps a déjà été traité.

let text = await response.text(); // corps de la réponse consommé
let parsed = await response.json(); // echec (déjà consommé)

En-têtes de réponse

Les en-têtes de réponse sont disponibles dans un objet d’en-têtes de type Map-like response.headers.

Ce n’est pas exactement un Map, mais il a des méthodes similaires pour obtenir des en-têtes individuels par nom ou les parcourir :

let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');

// get one header
alert(response.headers.get('Content-Type')); // application/json; charset=utf-8

// iterate over all headers
for (let [key, value] of response.headers) {
  alert(`${key} = ${value}`);
}

En-têtes de requêtes

Pour définir un en-tête de requête dans fetch, nous pouvons utiliser l’option headers. Il a un objet avec des en-têtes sortants, comme ceci :

let response = fetch(protectedUrl, {
  headers: {
    Authentication: 'secret'
  }
});

… Mais il y a une liste d’en-têtes HTTP interdits que nous ne pouvons pas définir :

  • Accept-Charset, Accept-Encoding
  • Access-Control-Request-Headers
  • Access-Control-Request-Method
  • Connection
  • Content-Length
  • Cookie, Cookie2
  • Date
  • DNT
  • Expect
  • Host
  • Keep-Alive
  • Origin
  • Referer
  • TE
  • Trailer
  • Transfer-Encoding
  • Upgrade
  • Via
  • Proxy-*
  • Sec-*

Ces en-têtes assurent un HTTP correct et sûr, ils sont donc contrôlés exclusivement par le navigateur.

Requêtes POST

Pour faire une requête POST, ou une requête avec une autre méthode, nous devons utiliser les options fetch :

  • method – HTTP-method, par exemple POST,
  • body – le corps de la requête, un parmi ceux-ci :
    • une chaîne de caractères (par exemple encodé en JSON),
    • un objet FormData, pour soumettre les données en tant que multipart/form-data,
    • Blob/BufferSource pour envoyer des données binaires,
    • URLSearchParams, pour soumettre les données au format x-www-form-urlencoded, rarement utilisé.

Le format JSON est utilisé la plupart du temps.

Par exemple, ce code soumet l’objet user en JSON :

let user = {
  name: 'John',
  surname: 'Smith'
};

let response = await fetch('/article/fetch/post/user', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json;charset=utf-8'
  },
  body: JSON.stringify(user)
});

let result = await response.json();
alert(result.message);

Veuillez noter que si la requête body est une chaîne de caractères, alors l’en-tête Content-Type est défini sur text/plain;charset=UTF-8 par défaut.

Mais, si nous envoyons du JSON, nous utiliserons à la place l’option headers pour envoyer application/json, le bon Content-Type pour les données encodées en JSON.

Envoi d’une image

Nous pouvons également soumettre des données binaires avec fetch en utilisant des objets Blob ou BufferSource.

Dans cet exemple, il y a un <canvas> où nous pouvons dessiner en déplaçant une souris dessus. Un clic sur le bouton “submit” envoie l’image au serveur :

<body style="margin:0">
  <canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas>

  <input type="button" value="Submit" onclick="submit()">

  <script>
    canvasElem.onmousemove = function(e) {
      let ctx = canvasElem.getContext('2d');
      ctx.lineTo(e.clientX, e.clientY);
      ctx.stroke();
    };

    async function submit() {
      let blob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));
      let response = await fetch('/article/fetch/post/image', {
        method: 'POST',
        body: blob
      });

      // le serveur répond avec confirmation et la taille de l'image
      let result = await response.json();
      alert(result.message);
    }

  </script>
</body>

Veuillez noter qu’ici, nous ne définissons pas l’en-tête Content-Type manuellement, car un objet Blob a un type intégré (ici image/png, tel que généré par toBlob). Pour les objets Blob, ce type devient la valeur de Content-Type.

La fonction submit() peut être réécrite sans async/await comme ceci :

function submit() {
  canvasElem.toBlob(function(blob) {
    fetch('/article/fetch/post/image', {
      method: 'POST',
      body: blob
    })
      .then(response => response.json())
      .then(result => alert(JSON.stringify(result, null, 2)))
  }, 'image/png');
}

Résumé

Une requête fetch typique se compose de deux appels await :

let response = await fetch(url, options); // se résout avec des en-têtes de réponse
let result = await response.json(); // lit le corps en tant que JSON

Ou, sans await :

fetch(url, options)
  .then(response => response.json())
  .then(result => /* process result */)

Propriétés de réponse :

  • response.status – Code HTTP de la réponse,
  • response.oktrue est le statut 200-299.
  • response.headers – objet Map-like avec en-têtes HTTP.

Méthodes pour obtenir le corps de réponse :

  • response.text() – retourne la réponse sous forme de texte,
  • response.json() – analyse la réponse en tant qu’objet JSON,
  • response.formData() – retourne la réponse en tant qu’objet FormData (encodage multipart/form-data, voir le chapitre suivant),
  • response.blob() – retourne la réponse en tant que Blob (données binaires avec type),
  • response.arrayBuffer() – retourne la réponse en tant que ArrayBuffer (données binaires de bas niveau),

Options de fetch jusque là :

  • method – Méthode HTTP,
  • headers – un objet avec en-têtes de requête (aucun en-tête n’est autorisé),
  • body – les données à envoyer (corps de la demande) en tant que string, FormData, BufferSource, Blob ou objet UrlSearchParams.

Dans les chapitres suivants, nous verrons plus d’options et de cas d’utilisation de fetch.

Exercices

Créez une fonction asynchrone getUsers(names), qui obtient un tableau de connexions GitHub, récupère les utilisateurs de GitHub et renvoie un tableau d’utilisateurs GitHub.

L’URL GitHub avec les informations utilisateur pour la donnée USERNAME est : https://api.github.com/users/USERNAME.

Il y a un exemple de test dans la sandbox.

Détails importants :

  1. Il devrait y avoir une requête fetch par utilisateur.
  2. Les demandes ne doivent pas s’attendre les unes les autres. Pour que les données arrivent le plus tôt possible.
  3. Si une requête échoue, ou si l’utilisateur n’existe pas, la fonction doit retourner null dans le tableau de résultats.

Open a sandbox with tests.

Pour récupérer un utilisateur, nous avons besoin de : fetch('https://api.github.com/users/USERNAME').

Si la réponse a le statut 200, appelons .json() pour lire l’objet JS.

Sinon, si un fetch échoue, ou si la réponse a un statut différent de 200, nous renvoyons simplement null dans le tableau de résutats.

Voici donc le code :

async function getUsers(names) {
  let jobs = [];

  for(let name of names) {
    let job = fetch(`https://api.github.com/users/${name}`).then(
      successResponse => {
        if (successResponse.status != 200) {
          return null;
        } else {
          return successResponse.json();
        }
      },
      failResponse => {
        return null;
      }
    );
    jobs.push(job);
  }

  let results = await Promise.all(jobs);

  return results;
}

Veuillez noter : l’appel .then est directement attaché à fetch, de sorte que lorsque nous avons la réponse, il n’attend pas d’autres fetches, mais commence à lire .json() immédiatement.

Si nous avions utilisé await Promise.all(names.map(name => fetch(...))), et appelé .json() sur les résultats, il aurait attendu que tous les fetches répondent. En ajoutant .json() directement à chaque fetch, nous nous assurons que les fetches individuels commencent à lire les données en JSON sans s’attendre les uns les autres.

C’est un exemple de la façon dont l’API Promise de bas niveau peut toujours être utile même si nous utilisons principalement async/wait.

Ouvrez la solution avec des tests dans une sandbox.

Carte du tutoriel