API Fetch

Cet atelier de programmation fait partie du cours de formation "Développer des progressive web apps", développé par l'équipe de formation Google Developers. Vous tirerez pleinement parti de ce cours en suivant les ateliers de programmation dans l'ordre.

Pour en savoir plus sur le cours, consultez la présentation du développement de progressive web apps.

Introduction

Cet atelier vous explique comment utiliser l'API Fetch, une interface simple pour récupérer des ressources, qui est une amélioration de l'API XMLHttpRequest.

Points abordés

  • Utiliser l'API Fetch pour demander des ressources
  • Envoyer des requêtes GET, HEAD et POST avec fetch
  • Lire et définir des en-têtes personnalisés
  • Utilisation et limites du CORS

À savoir

  • Connaissances de base en JavaScript et HTML
  • Vous maîtrisez le concept et la syntaxe de base des Promesses ES2015.

Ce dont vous avez besoin

  • Un ordinateur avec accès au terminal/shell
  • Connexion à Internet
  • Navigateur compatible avec Fetch
  • Un éditeur de texte
  • Node et npm

Remarque : Bien que l'API Fetch ne soit pas actuellement compatible avec tous les navigateurs, il existe un polyfill.

Téléchargez ou clonez le dépôt pwa-training-labs depuis GitHub et installez la version LTS de Node.js, si nécessaire.

Ouvrez la ligne de commande de votre ordinateur. Accédez au répertoire fetch-api-lab/app/ et démarrez un serveur de développement local :

cd fetch-api-lab/app
npm install
node server.js

Vous pouvez arrêter le serveur à tout moment avec Ctrl-c.

Ouvrez votre navigateur et accédez à localhost:8081/. Vous devriez voir une page avec des boutons permettant d'envoyer des requêtes (ils ne fonctionneront pas encore).

Remarque : Annulez l'enregistrement de tous les service workers et effacez tous les caches de service workers pour localhost afin qu'ils n'interfèrent pas avec l'atelier. Dans les outils pour les développeurs Chrome, vous pouvez y parvenir en cliquant sur Effacer les données du site dans la section Effacer le stockage de l'onglet Application.

Ouvrez le dossier fetch-api-lab/app/ dans l'éditeur de texte de votre choix. Le dossier app/ est celui dans lequel vous allez créer l'atelier.

Ce dossier contient :

  • echo-servers/ contient les fichiers utilisés pour exécuter les serveurs de test.
  • examples/ contient des exemples de ressources que nous utilisons pour tester la récupération.
  • js/main.js est le fichier JavaScript principal de l'application. C'est là que vous écrirez tout votre code.
  • index.html est la page HTML principale de notre exemple de site/application.
  • package-lock.json et package.json sont des fichiers de configuration pour nos dépendances de serveur de développement et de serveur d'écho.
  • server.js est un serveur de développement de nœuds

L'API Fetch dispose d'une interface relativement simple. Cette section explique comment écrire une requête HTTP de base à l'aide de fetch.

Récupérer un fichier JSON

Dans js/main.js, le bouton Fetch JSON (Récupérer le JSON) de l'application est associé à la fonction fetchJSON.

Mettez à jour la fonction fetchJSON pour demander le fichier examples/animals.json et consigner la réponse :

function fetchJSON() {
  fetch('examples/animals.json')
    .then(logResult)
    .catch(logError);
}

Enregistrez le script et actualisez la page. Cliquez sur Récupérer le fichier JSON. La console doit enregistrer la réponse de récupération.

Explication

La méthode fetch accepte le chemin d'accès à la ressource que nous souhaitons récupérer en tant que paramètre, en l'occurrence examples/animals.json. fetch renvoie une promesse qui se résout en un objet Response. Si la promesse est résolue, la réponse est transmise à la fonction logResult. Si la promesse est rejetée, catch prend le relais et l'erreur est transmise à la fonction logError.

Les objets de réponse représentent la réponse à une requête. Ils contiennent le corps de la réponse, ainsi que des propriétés et des méthodes utiles.

Tester les réponses non valides

Examinez la réponse enregistrée dans la console. Notez les valeurs des propriétés status, url et ok.

Remplacez la ressource examples/animals.json dans fetchJSON par examples/non-existent.json. La fonction fetchJSON mise à jour devrait désormais se présenter comme suit :

function fetchJSON() {
  fetch('examples/non-existent.json')
    .then(logResult)
    .catch(logError);
}

Enregistrez le script et actualisez la page. Cliquez à nouveau sur Récupérer le JSON pour essayer de récupérer cette ressource inexistante.

Notez que la récupération s'est terminée avec succès et n'a pas déclenché le bloc catch. Recherchez maintenant les propriétés status, URL et ok de la nouvelle réponse.

Les valeurs doivent être différentes pour les deux fichiers (comprenez-vous pourquoi ?). Si vous avez reçu des erreurs de console, les valeurs correspondent-elles au contexte de l'erreur ?

Explication

Pourquoi une réponse ayant échoué n'a-t-elle pas activé le bloc catch ? Remarque importante concernant les récupérations et les promesses : les mauvaises réponses (comme les erreurs 404) sont toujours résolues. Une promesse de récupération n'est rejetée que si la requête n'a pas pu être exécutée. Vous devez donc toujours vérifier la validité de la réponse. Nous validerons les réponses dans la section suivante.

Pour en savoir plus

Vérifier la validité de la réponse

Nous devons mettre à jour notre code pour vérifier la validité des réponses.

Dans main.js, ajoutez une fonction pour valider les réponses :

function validateResponse(response) {
  if (!response.ok) {
    throw Error(response.statusText);
  }
  return response;
}

Remplacez ensuite fetchJSON par le code suivant :

function fetchJSON() {
  fetch('examples/non-existent.json')
    .then(validateResponse)
    .then(logResult)
    .catch(logError);
}

Enregistrez le script et actualisez la page. Cliquez sur Récupérer le fichier JSON. Consultez la console. La réponse pour examples/non-existent.json devrait maintenant déclencher le bloc catch.

Remplacez examples/non-existent.json dans la fonction fetchJSON par le examples/animals.json d'origine. La fonction mise à jour devrait désormais se présenter comme suit :

function fetchJSON() {
  fetch('examples/animals.json')
    .then(validateResponse)
    .then(logResult)
    .catch(logError);
}

Enregistrez le script et actualisez la page. Cliquez sur Récupérer le fichier JSON. Vous devriez constater que la réponse est bien enregistrée, comme auparavant.

Explication

Maintenant que nous avons ajouté la vérification validateResponse, les mauvaises réponses (comme les erreurs 404) génèrent une erreur et le catch prend le relais. Cela nous permet de gérer les réponses ayant échoué et d'empêcher les réponses inattendues de se propager dans la chaîne de récupération.

Lire la réponse

Les réponses Fetch sont représentées sous la forme de ReadableStreams (spécification des flux) et doivent être lues pour accéder au corps de la réponse. Les objets de réponse disposent de méthodes pour ce faire.

Dans main.js, ajoutez une fonction readResponseAsJSON avec le code suivant :

function readResponseAsJSON(response) {
  return response.json();
}

Remplacez ensuite la fonction fetchJSON par le code suivant :

function fetchJSON() {
  fetch('examples/animals.json') // 1
  .then(validateResponse) // 2
  .then(readResponseAsJSON) // 3
  .then(logResult) // 4
  .catch(logError);
}

Enregistrez le script et actualisez la page. Cliquez sur Récupérer le fichier JSON. Vérifiez dans la console que le JSON de examples/animals.json est enregistré (au lieu de l'objet Response).

Explication

Examinons la situation.

Étape 1 : Fetch est appelé sur une ressource, examples/animals.json. Fetch renvoie une promesse qui se résout en un objet Response. Lorsque la promesse est résolue, l'objet de réponse est transmis à validateResponse.

Étape 2 : validateResponse vérifie si la réponse est valide (code 200). Si ce n'est pas le cas, une erreur est générée, ce qui permet d'ignorer le reste des blocs then et de déclencher le bloc catch. C'est particulièrement important. Sans cette vérification, les mauvaises réponses sont transmises à la chaîne et pourraient casser le code ultérieur qui peut dépendre de la réception d'une réponse valide. Si la réponse est valide, elle est transmise à readResponseAsJSON.

Étape 3 : readResponseAsJSON lit le corps de la réponse à l'aide de la méthode Response.json(). Cette méthode renvoie une promesse qui se résout en JSON. Une fois cette promesse résolue, les données JSON sont transmises à logResult. (Si la promesse de response.json() est rejetée, le bloc catch est déclenché.)

Étape 4 : Enfin, les données JSON de la requête d'origine adressée à examples/animals.json sont enregistrées par logResult.

Pour en savoir plus

Fetch ne se limite pas à JSON. Dans cet exemple, nous allons récupérer une image et l'ajouter à la page.

Dans main.js, écrivez une fonction showImage avec le code suivant :

function showImage(responseAsBlob) {
  const container = document.getElementById('img-container');
  const imgElem = document.createElement('img');
  container.appendChild(imgElem);
  const imgUrl = URL.createObjectURL(responseAsBlob);
  imgElem.src = imgUrl;
}

Ajoutez ensuite une fonction readResponseAsBlob qui lit les réponses en tant que Blob :

function readResponseAsBlob(response) {
  return response.blob();
}

Modifiez la fonction fetchImage avec le code suivant :

function fetchImage() {
  fetch('examples/fetching.jpg')
    .then(validateResponse)
    .then(readResponseAsBlob)
    .then(showImage)
    .catch(logError);
}

Enregistrez le script et actualisez la page. Cliquez sur Récupérer l'image. Vous devriez voir un adorable chien chercher un bâton sur la page (c'est une blague sur la récupération de données !).

Explication

Dans cet exemple, une image est récupérée, examples/fetching.jpg. Comme dans l'exercice précédent, la réponse est validée avec validateResponse. La réponse est ensuite lue en tant que Blob (au lieu de JSON comme dans la section précédente). Un élément d'image est créé et ajouté à la page, et l'attribut src de l'image est défini sur une URL de données représentant le Blob.

Remarque : La méthode createObjectURL() de l'objet URL est utilisée pour générer une URL de données représentant le Blob. Il est important de le noter. Vous ne pouvez pas définir directement la source d'une image sur un blob. Le Blob doit être converti en URL de données.

Pour en savoir plus

Cette section est un défi facultatif.

Mettez à jour la fonction fetchText pour

  1. fetch /examples/words.txt
  2. valider la réponse avec validateResponse
  3. lire la réponse sous forme de texte (indice : voir Response.text())
  4. et afficher le texte sur la page.

Vous pouvez utiliser cette fonction showText comme aide pour afficher le texte final :

function showText(responseAsText) {
  const message = document.getElementById('message');
  message.textContent = responseAsText;
}

Enregistrez le script et actualisez la page. Cliquez sur Récupérer le texte. Si vous avez correctement implémenté fetchText, du texte devrait s'afficher sur la page.

Remarque  : Bien qu'il puisse être tentant de récupérer du code HTML et de l'ajouter à l'aide de l'attribut innerHTML, soyez prudent. Cela peut exposer votre site à des attaques de script intersites.

Pour en savoir plus

Par défaut, la récupération utilise la méthode GET, qui récupère une ressource spécifique. Toutefois, la récupération peut également utiliser d'autres méthodes HTTP.

Effectuer une requête HEAD

Remplacez la fonction headRequest par le code suivant :

function headRequest() {
  fetch('examples/words.txt', {
    method: 'HEAD'
  })
  .then(validateResponse)
  .then(readResponseAsText)
  .then(logResult)
  .catch(logError);
}

Enregistrez le script et actualisez la page. Cliquez sur Requête HEAD. Notez que le contenu textuel enregistré est vide.

Explication

La méthode fetch peut recevoir un deuxième paramètre facultatif, init. Ce paramètre permet de configurer la requête d'extraction, comme la méthode de requête, le mode cache, les identifiants, et plus encore.

Dans cet exemple, nous définissons la méthode de requête fetch sur HEAD à l'aide du paramètre init. Les requêtes HEAD sont identiques aux requêtes GET, sauf que le corps de la réponse est vide. Ce type de requête peut être utilisé lorsque vous souhaitez uniquement obtenir des métadonnées sur un fichier, mais que vous n'avez pas besoin de transporter toutes les données du fichier.

Facultatif : Trouver la taille d'une ressource

Examinons les en-têtes de la réponse de récupération pour examples/words.txt afin de déterminer la taille du fichier.

Mettez à jour la fonction headRequest pour enregistrer la propriété content-length de la réponse headers (conseil : consultez la documentation sur les en-têtes et la méthode get).

Une fois le code modifié, enregistrez le fichier et actualisez la page. Cliquez sur Requête HEAD. La console doit enregistrer la taille (en octets) de examples/words.txt.

Explication

Dans cet exemple, la méthode HEAD est utilisée pour demander la taille (en octets) d'une ressource (représentée dans l'en-tête content-length) sans charger la ressource elle-même. En pratique, cela peut être utilisé pour déterminer si la ressource complète doit être demandée (ou même comment la demander).

Facultatif : Déterminez la taille de examples/words.txt à l'aide d'une autre méthode et vérifiez qu'elle correspond à la valeur de l'en-tête de réponse (vous pouvez rechercher comment procéder pour votre système d'exploitation spécifique. Bonus si vous utilisez la ligne de commande !).

Pour en savoir plus

Fetch peut également envoyer des données avec des requêtes POST.

Configurer un serveur d'écho

Pour cet exemple, vous devez exécuter un serveur d'écho. À partir du répertoire fetch-api-lab/app/, exécutez la commande suivante (si votre ligne de commande est bloquée par le serveur localhost:8081, ouvrez une nouvelle fenêtre ou un nouvel onglet de ligne de commande) :

node echo-servers/cors-server.js

Cette commande démarre un serveur simple sur localhost:5000/ qui renvoie les requêtes qui lui sont envoyées.

Vous pouvez arrêter ce serveur à tout moment avec ctrl+c.

Envoyer une requête POST

Remplacez la fonction postRequest par le code suivant (assurez-vous d'avoir défini la fonction showText de la section 4 si vous ne l'avez pas fait) :

function postRequest() {
  fetch('http://localhost:5000/', {
    method: 'POST',
    body: 'name=david&message=hello'
  })
    .then(validateResponse)
    .then(readResponseAsText)
    .then(showText)
    .catch(logError);
}

Enregistrez le script et actualisez la page. Cliquez sur Requête POST. Observez la demande envoyée qui s'affiche sur la page. Il doit contenir le nom et le message (notez que nous n'obtenons pas encore de données du formulaire).

Explication

Pour envoyer une requête POST avec fetch, nous utilisons le paramètre init pour spécifier la méthode (comme nous l'avons fait pour la méthode HEAD dans la section précédente). C'est également ici que nous définissons le corps de la requête, qui est une simple chaîne dans ce cas. Le corps correspond aux données que nous souhaitons envoyer.

Remarque : En production, n'oubliez pas de toujours chiffrer les données utilisateur sensibles.

Lorsque des données sont envoyées sous forme de requête POST à localhost:5000/, la requête est renvoyée en tant que réponse. La réponse est ensuite validée avec validateResponse, lue sous forme de texte et affichée sur la page.

En pratique, ce serveur représenterait une API tierce.

Facultatif : Utiliser l'interface FormData

Vous pouvez utiliser l'interface FormData pour récupérer facilement les données des formulaires.

Dans la fonction postRequest, instanciez un nouvel objet FormData à partir de l'élément de formulaire msg-form :

const formData = new FormData(document.getElementById('msg-form'));

Remplacez ensuite la valeur du paramètre body par la variable formData.

Enregistrez le script et actualisez la page. Remplissez le formulaire (champs Name et Message) sur la page, puis cliquez sur la requête POST. Observez le contenu du formulaire affiché sur la page.

Explication

Le constructeur FormData peut accepter un form HTML et créer un objet FormData. Cet objet est renseigné avec les clés et les valeurs du formulaire.

Pour en savoir plus

Démarrer un serveur d'écho non CORS

Arrêtez le serveur d'écho précédent (en appuyant sur ctrl+c depuis la ligne de commande) et démarrez un nouveau serveur d'écho depuis le répertoire fetch-lab-api/app/ en exécutant la commande suivante :

node echo-servers/no-cors-server.js

Cette commande configure un autre serveur d'écho simple, cette fois à l'adresse localhost:5001/. Toutefois, ce serveur n'est pas configuré pour accepter les requêtes d'origine croisée.

Récupérer les données depuis le nouveau serveur

Maintenant que le nouveau serveur s'exécute sur localhost:5001/, nous pouvons lui envoyer une requête fetch.

Mettez à jour la fonction postRequest pour qu'elle récupère les données à partir de localhost:5001/ au lieu de localhost:5000/. Une fois le code mis à jour, enregistrez le fichier, actualisez la page, puis cliquez sur Requête POST.

Une erreur devrait s'afficher dans la console, indiquant que la requête cross-origin est bloquée, car l'en-tête CORS Access-Control-Allow-Origin est manquant.

Mettez à jour le fetch dans la fonction postRequest avec le code suivant, qui utilise le mode no-cors (comme le suggère le journal des erreurs) et supprime les appels à validateResponse et readResponseAsText (voir l'explication ci-dessous) :

function postRequest() {
  const formData = new FormData(document.getElementById('msg-form'));
  fetch('http://localhost:5001/', {
    method: 'POST',
    body: formData,
    mode: 'no-cors'
  })
    .then(logResult)
    .catch(logError);
}

Enregistrez le script et actualisez la page. Remplissez ensuite le formulaire de message et cliquez sur POST Request (Requête POST).

Observez l'objet de réponse consigné dans la console.

Explication

Fetch (et XMLHttpRequest) suivent la règle d'origine identique. Cela signifie que les navigateurs limitent les requêtes HTTP d'origine croisée à partir de scripts. Une requête inter-origines se produit lorsqu'un domaine (par exemple, http://foo.com/) demande une ressource à un autre domaine (par exemple, http://bar.com/).

Remarque : Les restrictions concernant les requêtes d'origine croisée sont souvent source de confusion. De nombreuses ressources, telles que des images, des feuilles de style et des scripts, sont récupérées sur plusieurs domaines (c'est-à-dire d'origine croisée). Toutefois, il s'agit d'exceptions à la règle d'origine commune. Les requêtes d'origine croisée sont toujours limitées dans les scripts.

Étant donné que le serveur de notre application possède un numéro de port différent de celui des deux serveurs d'écho, les requêtes adressées à l'un ou l'autre des serveurs d'écho sont considérées comme des requêtes d'origine croisée. Toutefois, le premier serveur d'écho, qui s'exécute sur localhost:5000/, est configuré pour prendre en charge CORS (vous pouvez ouvrir echo-servers/cors-server.js et examiner la configuration). Le nouveau serveur d'écho, qui s'exécute sur localhost:5001/, ne l'est pas (c'est pourquoi nous obtenons une erreur).

L'utilisation de mode: no-cors permet de récupérer une réponse opaque. Cela nous permet d'obtenir une réponse, mais empêche l'accès à la réponse avec JavaScript (c'est pourquoi nous ne pouvons pas utiliser validateResponse, readResponseAsText ou showResponse). La réponse peut toujours être consommée par d'autres API ou mise en cache par un service worker.

Modifier les en-têtes de requête

Fetch permet également de modifier les en-têtes de requête. Arrêtez le serveur d'écho localhost:5001 (sans CORS) et redémarrez le serveur d'écho localhost:5000 (CORS) de la section 6 :

node echo-servers/cors-server.js

Restaurez la version précédente de la fonction postRequest qui récupère les données à partir de localhost:5000/ :

function postRequest() {
  const formData = new FormData(document.getElementById('msg-form'));
  fetch('http://localhost:5000/', {
    method: 'POST',
    body: formData
  })
    .then(validateResponse)
    .then(readResponseAsText)
    .then(showText)
    .catch(logError);
}

Utilisez maintenant l'interface Header pour créer un objet Headers à l'intérieur de la fonction postRequest appelée messageHeaders avec l'en-tête Content-Type égal à application/json.

Définissez ensuite la propriété headers de l'objet init sur la variable messageHeaders.

Mettez à jour la propriété body pour qu'elle soit un objet JSON sous forme de chaîne, par exemple :

JSON.stringify({ lab: 'fetch', status: 'fun' })

Une fois le code modifié, enregistrez le fichier et actualisez la page. Cliquez ensuite sur Requête POST.

Notez que la requête renvoyée a maintenant un Content-Type de application/json (au lieu de multipart/form-data comme précédemment).

Ajoutez maintenant un en-tête Content-Length personnalisé à l'objet messageHeaders et attribuez une taille arbitraire à la requête.

Une fois le code mis à jour, enregistrez le fichier, actualisez la page, puis cliquez sur POST Request (Requête POST). Notez que cet en-tête n'est pas modifié dans la requête renvoyée.

Explication

L'interface d'en-tête permet de créer et de modifier des objets Headers. Certains en-têtes, comme Content-Type, peuvent être modifiés par la récupération. D'autres, comme Content-Length, sont protégés et ne peuvent pas être modifiés (pour des raisons de sécurité).

Définir des en-têtes de requête personnalisés

Fetch permet de définir des en-têtes personnalisés.

Supprimez l'en-tête Content-Length de l'objet messageHeaders dans la fonction postRequest. Ajoutez l 'en-tête personnalisé X-Custom avec une valeur arbitraire (par exemple, X-CUSTOM': 'hello world').

Enregistrez le script, actualisez la page, puis cliquez sur POST Request (Demande POST).

Vous devriez voir que la requête renvoyée contient la propriété X-Custom que vous avez ajoutée.

Ajoutez maintenant un en-tête Y-Custom à l'objet Headers. Enregistrez le script, actualisez la page, puis cliquez sur POST Request (Requête POST).

Vous devriez obtenir une erreur semblable à celle-ci dans la console :

Fetch API cannot load http://localhost:5000/. Request header field y-custom is not allowed by Access-Control-Allow-Headers in preflight response.

Explication

Comme pour les requêtes inter-origines, les en-têtes personnalisés doivent être acceptés par le serveur à partir duquel la ressource est demandée. Dans cet exemple, notre serveur d'écho est configuré pour accepter l'en-tête X-Custom, mais pas l'en-tête Y-Custom (vous pouvez ouvrir echo-servers/cors-server.js et rechercher Access-Control-Allow-Headers pour le vérifier). Chaque fois qu'un en-tête personnalisé est défini, le navigateur effectue une vérification prévol. Cela signifie que le navigateur envoie d'abord une requête OPTIONS au serveur pour déterminer les méthodes et les en-têtes HTTP autorisés par le serveur. Si le serveur est configuré pour accepter la méthode et les en-têtes de la requête d'origine, celle-ci est envoyée. Sinon, une erreur est générée.

Pour en savoir plus

Code de solution

Pour obtenir une copie du code fonctionnel, accédez au dossier solution.

Vous savez maintenant comment utiliser l'API Fetch.

Ressources

Pour voir tous les ateliers de programmation du cours de formation sur les PWA, consultez l'atelier de programmation de bienvenue.