l) Programmation asynchrone & les promesses
Utilisation de promesses & async / await
Introduction
Plutôt que d'utiliser des .then() pour chaîner des traitements asynchrones, il est possible de simplifier la syntaxe des promesses à l'aide de async et await.
On va donc écrire du code d'une manière équivalente à ce qui serait fait en programmation synchrone, tout en bénéficiant des effets de la programmation asynchrone.
async / await : les bases
Pour ce nouveau tutoriel, nous allons refactorer l'IHM pour améliorer le code associé aux appels asynchrones aux API.
Au sein de votre repo web2, veuillez créer le projet nommé /web2/tutorials/pizzeria/hmi/async-await sur base d'un copié/collé de /web2/tutorials/pizzeria/hmi/basic-fetch (ou basic-fetch-hmi).
Pour la suite du tutoriel, nous considérons que tous les chemins absolus démarrent du répertoire
/web2/tutorials/pizzeria/hmi/async-await.
Nous allons donc refactorer le code où est fait le fetch, c'est à dire /src/Components/Pages/HomePage.js (veuillez mettre à jour tout le code de la fonction arrow associée à la variable HomePage) :
js1const HomePage = async () => {2try {3clearPage();45const response = await fetch('/api/pizzas');67if (!response.ok) throw new Error(`fetch error : ${response.status} : ${response.statusText}`);89const pizzas = await response.json();1011renderMenuFromString(pizzas);12attachOnMouseEventsToGoGreen();13renderDrinksFromNodes(DRINKS);14} catch (err) {15console.error('HomePage::error: ', err);16}17};
Pour tester ce code, il ne faut pas oublier de démarrer la RESTful API auparavant : /web2/tutorials/pizzeria/api/fat-model ou via le code de ce web repo si vous avez un souci : api-fat-model.
Voici quelques caractéristiques importantes de async / await :
awaitest utilisé pour chaîner une tâche asynchrone (sur une fonction renvoyant une promesse) et ne peut se faire qu'au sein d'une fonction taguée parasync; c'est donc le remplaçant du.then(callback).
⚡ Attention, il est donc important qu'au niveau de la fonctionarrow, à la ligne 1 du code donné ci-dessus, on indique leasync!
⚡ Dans le code donné ci-dessus, il est aussi très important de ne pas oublier lesawait. N'hésitez pas à faire le test en enlevant leawaitdeconst pizzas = await response.json();.
Que se passe-t-il dans ce cas ?response.json()étant une fonction asynchrone, on passera directement à la fonctionrenderMenuFromString(pizzas);avant même d'avoir récupéré les pizzas de notre RESTful API !- Toute fonction "taguée" par
asyncrenvoie automatiquement une promesse ; cela signifie dans le code ci-dessus que la fonctionHomePageest elle même asynchrone.
💭 Il est à parier, et n'hésitez pas à trouver un moyen de vous en rendre compte visuellement, que le footer s'affiche avant le menu ! - On utilise des blocs
try/catchpour gérer les erreur ; c'est donc le remplaçant du.catch(callback).
Opération asynchrone d'écriture d'une ressource
A présent, nous souhaiterions que notre IHM puisse créer une ressource au sein de la RESTful API. Dans un premier temps, nous allons mettre à jour le frontend en acceptant que n'importe quel utilisateur puisse créer une pizza et l'ajouter au menu de la pizzeria.
Bien entendu, cela est temporaire. Nous verrons plus tard comment sécuriser cette opération, en autorisant un admin seulement à réaliser l'ajout d'une pizza au menu.
Nous allons créer une nouvelle page nommée AddPizzaPage.js.
Pour commencer, nous allons réaliser une page statique car il est toujours plus aisé de d'abord avoir une version visuelle d'une application web, avec la navigation entre les pages, avant de peaufiner chaque page et leurs interactions avec les utilisateurs et les APIS.
Veuillez ajouter ce code dans la nouvelle page /src/Components/Pages/AddPizzaPage.js:
jsimport { clearPage, renderPageTitle } from '../../utils/render';const AddPizzaPage = () => {clearPage();renderPageTitle('Add a pizza to the menu');renderAddPizzaForm();};function renderAddPizzaForm() {const main = document.querySelector('main');const form = document.createElement('form');form.className = 'p-5';const title = document.createElement('input');title.type = 'text';title.id = 'title';title.placeholder = 'title of your pizza';title.required = true;title.className = 'form-control mb-3';const content = document.createElement('input');content.type = 'text';content.id = 'content';content.required = true;content.placeholder = 'Content of your pizza';content.className = 'form-control mb-3';const submit = document.createElement('input');submit.value = 'Add pizza to the menu';submit.type = 'submit';submit.className = 'btn btn-danger';form.appendChild(title);form.appendChild(content);form.appendChild(submit);main.appendChild(form);}export default AddPizzaPage;
Nous devons aussi ajouter un élément dans la Navbar afin de pouvoir accéder à cette nouvelle page. Pour ce faire, veuillez mettre à jour /src/Components/Navbar/Navbar.js :
js1// eslint-disable-next-line no-unused-vars2import { Navbar as BootstrapNavbar } from 'bootstrap';34const Navbar = () => {5renderNavbar();6};78function renderNavbar() {9const navbar = document.querySelector('#navbarWrapper');10navbar.innerHTML = `11<nav class="navbar navbar-expand-lg navbar-light bg-danger">12<div class="container-fluid">13<a class="navbar-brand" href="#">e-Pizzeria</a>14<button15class="navbar-toggler"16type="button"17data-bs-toggle="collapse"18data-bs-target="#navbarSupportedContent"19aria-controls="navbarSupportedContent"20aria-expanded="false"21aria-label="Toggle navigation"22>23<span class="navbar-toggler-icon"></span>24</button>25<div class="collapse navbar-collapse" id="navbarSupportedContent">26<ul class="navbar-nav me-auto mb-2 mb-lg-0">27<li class="nav-item">28<a class="nav-link active" aria-current="page" href="#" data-uri="/">Home</a>29</li>30<li id="loginItem" class="nav-item">31<a class="nav-link" href="#" data-uri="/login">Login</a>32</li>33<li id="registerItem" class="nav-item">34<a class="nav-link" href="#" data-uri="/register">Register</a>35</li>36<li class="nav-item">37<a class="nav-link" href="#" data-uri="/add-pizza">Add a pizza</a>38</li>39</ul>40</div>41</div>42</nav>43`;44}4546export default Navbar;
Et finalement, comme le frontend de ce tutoriel utilise le boilerplate du cours, il faut encore configurer le router de l'IHM afin d'indiquer la page à afficher lorsqu'on clique sur le lien dont data-uri vaut "/add-pizza". Pour ce faire, veuillez mettre à jour le fichier /src/Components/Router/Router.js pour ajouter ces deux lignes :
jsimport AddPizzaPage from '../Pages/AddPizzaPage';import HomePage from '../Pages/HomePage';import LoginPage from '../Pages/LoginPage';import RegisterPage from '../Pages/RegisterPage';const routes = {'/': HomePage,'/login': LoginPage,'/register': RegisterPage,'/add-pizza': AddPizzaPage,};
A ce stade-ci, votre application /web2/tutorials/pizzeria/hmi/async-await devrait être fonctionnelle, vous devriez pouvoir naviguer vers la nouvelle page contenant un formulaire pour ajouter une pizza.
Nous allons maintenant ajouter l'interaction avec l'API au sein de AddPizzaPage.
Lorsque nous soumettons le formulaire, nous voulons faire une requête de création de pizza à la RESTful API, c'est donc une requête de type POST /pizzas qui doit être l'équivalent de ce que nous faisions avec REST Client. Pour rappel, nous faisions une requête de ce genre :
http### Create a pizzaPOST {{baseUrl}}/pizzasContent-Type: application/json{"title":"Magic Green","content":"Epinards, Brocolis, Olives vertes, Basilic"}
Ici, c'est le JS à rajouter dans AddPizzaPage qui doit, lors du clic, aller chercher les valeurs des deux champs du formulaire pour créer la représentation des données (title et content) et faire un fetch de l'opération de création offerte par l'API.
Si l'ajout se fait avec succès, on souhaite faire en sorte que l'utilisateur soit redirigé vers la HomePage.
Pour arriver à nos fins, veuillez ajouter ce code dans la page /src/Components/Pages/AddPizzaPage.js :
js1import { clearPage, renderPageTitle } from '../../utils/render';2import Navigate from '../Router/Navigate';34const AddPizzaPage = () => {5clearPage();6renderPageTitle('Add a pizza to the menu');7renderAddPizzaForm();8};910function renderAddPizzaForm() {11const main = document.querySelector('main');12const form = document.createElement('form');13form.className = 'p-5';14const title = document.createElement('input');15title.type = 'text';16title.id = 'title';17title.placeholder = 'title of your pizza';18title.required = true;19title.className = 'form-control mb-3';20const content = document.createElement('input');21content.type = 'text';22content.id = 'content';23content.required = true;24content.placeholder = 'Content of your pizza';25content.className = 'form-control mb-3';26const submit = document.createElement('input');27submit.value = 'Add pizza to the menu';28submit.type = 'submit';29submit.className = 'btn btn-danger';30form.appendChild(title);31form.appendChild(content);32form.appendChild(submit);33main.appendChild(form);34form.addEventListener('submit', onAddPizza);35}3637async function onAddPizza(e) {38e.preventDefault();3940const title = document.querySelector('#title').value;41const content = document.querySelector('#content').value;4243const options = {44method: 'POST',45body: JSON.stringify({46title,47content,48}),49headers: {50'Content-Type': 'application/json',51},52};5354const response = await fetch('/api/pizzas', options); // fetch return a promise => we wait for the response5556if (!response.ok) throw new Error(`fetch error : ${response.status} : ${response.statusText}`);5758const newPizza = await response.json(); // json() returns a promise => we wait for the data5960console.log('New pizza added : ', newPizza);6162Navigate('/');63}6465export default AddPizzaPage;
Quelques explications sur ce code, pour les parties déjà connues :
- La gestion d'événements n'est pas nouvelle. Ici, on met un écouteur d'événements de type
submitsur le formulaire. Cela permet d'écouter tant les clics sur le champs de typesubmit(le bouton) que si l'utilisateur appuie surEnter.
👍 On recommande, pour les formulaires, d'utiliser des événements de typesubmitplutôt que des événements de typeclicksur le boutonsubmitafin notamment de prendre en compte si l'utilisateur appuie surEnterpour tenter de soumettre le formulaire. - L'action par défaut d'un formulaire, lors d'un submit, et de faire une requête synchrone vers l'URL du backend indiqué dans la propriété
actiondu formulaire, ou sur la même URL que la page en cours siactionn'est pas donné. Pour éviter un chargement de page non désiré dans le cadre d'une SPA, on stoppe cette action par défaut viae.preventDefault().
Pour la nouveauté et le fetch :
- pour faire une requête de type
POST, tout comme pour les requêtes de typeDELETE,PATCH,UPDATE..., il faut l'indiquer à la méthodefetch.
Cela est indiqué dans un objet que nous appelons généralementoptionsqui doit contenir la propriétémethod. - lorsque l'on doit envoyer des données dans le
bodyd'une requête, alors il faut le faire au sein de la propriétébody. Ici, nous souhaitons envoyer un objet contenant les propriétéstitleetcontentau format JSON. Nous devons donc utiliser la méthodeJSON.stringifyqui permet de créer une représentation JSON d'un objet JS. Notez ici que nous avons écrit l'objet JS selon une notation simplifiée ("object property shorthand") :
jsconst title = document.querySelector('#title').value;const content = document.querySelector('#content').value;{title,content,}// Cet object literal est l'équivalent de :{title: title,content: content,}
- Il est très important de spécifier le type de la représentation de l'objet qui devrait être utilisé par l'API et qui se trouve dans le body de la requête. Cela est fait via un
headeret la propriétéContent-Type('Content-Type': 'application/json',).
⚡ Si vous oubliez cela, l'API ne pourra pas parser les données au format JSON vers des objets JS et donc les opérations d'écriture de ressources échoueront ! - Finalement, si l'opération de création de la pizza réussi, nous redirigeons l'utilisateur vers la
HomePageà l'aide du composant fonctionnelNavigateoffert au sein du moduleNavigate.jsdans le dossier Router du boilerplate du frontend.
Si tout fonctionne bien, faites un commit de votre repo (web2) avec comme message
: async-await-hmi tutorial.
En cas de souci, vous pouvez accéder au code du tutoriel ici : async-await-hmi.
Création de fonctions asynchrones renvoyant une promesse
A l'aide d'async / await, il est très simple de créer des fonctions asynchrones qui renvoient une promesse.
Imaginons que nous souhaitons créer une fonction asynchrone qui renvoie toutes les pizzas qui sont offertes par l'opération de lecture des pizzas de la RESTful API.
Voici comment nous écririons ce code :
jsasync function getAllPizzas() {try {const response = await fetch('/api/pizzas');if (!response.ok) throw new Error(`fetch error : ${response.status} : ${response.statusText}`);const pizzas = await response.json();return pizzas;} catch (err) {console.error('getAllPizzas::error: ', err);throw err;}}
Cette fonction getAllPizzas ne renvoie pas un array de pizzas, mais une Promise !
Si la promesse :
- résout avec succès, alors c'est bien un array de pizzas qui sera renvoyé par cette fonction.
- échoue, c'est une exception qui sera renvoyée.
Pour que cela fonctionne, vous devez donc faire en sorte, dans vos fonctions asynchrones, de faire unthrowd'une erreur en cas d'échec du traitement asynchrone.
Comment utiliser ce code au sein de la HomePage ?
Voici comment le code pourrait être mis à jour pour utiliser la fonction asynchrone getAllPizzas au sein de HomePage.js :
jsconst HomePage = async () => {try {clearPage();const pizzas = await getAllPizzas();renderMenuFromString(pizzas);attachOnMouseEventsToGoGreen();renderDrinksFromNodes(DRINKS);} catch (err) {console.error('HomePage::error: ', err);}};
Exercice 2.19 : Intégration de la RESTful API au sein de l'IHM de myMovies via un proxy
Objectif
Vous allez intégrer le frontend de myMovies avec sa RESTful API afin que toutes les opérations CRUD soient traitées par votre application.
L'application myMovies doit permettre tous ces cas d'utilisation (ou use cases):
- UC1 : l'affichage, sous forme de tableau, de toutes les ressources de type films.
- UC2 : l'ajout d'une ressource de type films via un formulaire d'ajout d'un film.
- UC3 : la suppression d'un film.
- UC4 : la mise à jour des données d'un film (à l'exception de l'id associé à un film).
Nous acceptons, à ce stade-ci, que des utilisateurs anonymes puissent réaliser des opérations qui normalement devraient être sécurisées. Nous verrons plus tard comment authentifier et autoriser des utilisateurs afin de protéger l'accès aux opérations d'API.
Veuillez utiliser le proxy de votre frontend afin de contourner les problèmes associé à la gestion des CORS. Tous les appels aux opérations des API doivent se faire à l'aide de async / await.
Mise en place des projets
Vous allez développer le frontend de manière incrémentale.
UC1 : l'affichage, sous forme de tableau, de toutes les ressources de type films
Veuillez consommer l'opération de lecture de films de l'API au sein de ViewMoviePage à l'aide de async / await et du proxy.
Quand c'est fonctionnel, veuillez faire un commit de votre code avec comme message : 2.19.1 : spa read operation & async / await.
UC2 : l'ajout d'une ressource de type films via un formulaire d'ajout d'un film
Veuillez consommer l'opération de création de films de l'API au sein de AddMoviePage à l'aide de async / await et du proxy.
⚡ Lors de l'ajout d'un film, n'oubliez pas que le budget et la durée doivent être des nombres, pas des strings !
Quand c'est fonctionnel, veuillez faire un commit de votre code avec comme message : 2.19.2 : spa create operation.
UC3 : la suppression d'un film
Veuillez consommer l'opération de suppression de films de l'API au sein de ViewMoviePage à l'aide de async / await et du proxy.
Quand c'est fonctionnel, veuillez faire un commit de votre code avec comme message : 2.19.3 : spa delete operation.
UC4 : la mise à jour des données d'un film (à l'exception de l'id associé à un film)
Veuillez consommer l'opération de mise à jour de films de l'API au sein de ViewMoviePage à l'aide de async / await et du proxy.
Quand c'est fonctionnel, veuillez faire un commit de votre code avec comme message : 2.19.4 : spa update operation.
🤝 Tips
Comment gérer l'UC de suppression d'un film ?
- Vous pourriez avoir un bouton
Deletepour chaque ligne du tableau affichant les films. Lors du clic sur un boutonDelete, vous feriez une requête de typeDELETEvers la RESTful API. Attention, pour une requête de typeDELETE, l'identifiant de l'objet à supprimer doit être donnée dans l'URL associée aufetch. - Comment retrouver l'identifiant du film affiché dans le tableau ?
Pensez audata-attribute, vous pouvez cacher de l'information dans l'HTML. Par exemple, chaque boutonDeletepourrait contenir undata-attributeétant l'id du film. Il est aussi possible d'utiliser la propriétéiddu bouton pour "cacher" de l'info.
Comment gérer la mise à jour des données d'un film ? Nous vous proposons deux options à choix :
- L'option 1 : elle va vous permettre de ne pas devoir créer des formulaires et de directement mettre à jour de l'info en modifiant le contenu de containers HTML.
- L'option 2 : vous pourriez créer des formulaires en les pré-remplissant des données existantes.
Option 1 pour la mise à jour d'info : la plus cool à découvrir 😉
Vous pourriez faire en sorte que l'HTML du tableau affichant les films, lors d'un clic, devienne éditable. Pour cette option :
- Voici, à quoi pourrait ressembler votre application à la fin de l'exercice. Vous avez bien sûr la liberté de faire quelque chose de totalement différent visuellement !

- Veuillez noter que, contrairement à ce qui a été fait pour l'Exercice 2.11, le
titleet lelinkne sont plus intégrés dans une même colonne, via des hyperlinks ; avec l'option 1, nous devrions considérer deux colonnes, une pour le titre et l'autre pour le lien. - Vous pouvez utiliser la propriété HTML
contenteditable="true"pour rendre les cellules du tableau éditables. Voici un exemple pour rendre une cellule associée au titre éditable :js<td class="fw-bold text-info" contenteditable="true">${element.title}</td> - Pour accéder aux cellules qui se trouvent dans une même ligne que vous mettez à jour, vous pouvez utiliser la DOM API de votre browser :
- on obtient le parent d'un élément HTML via l'attribut
parentElement; par exemple, si vous avez un écouteur d'événements de clics sur un boutonSave, ce bouton se trouvant au sein d'unetdqui se trouve elle-même au sein d'unetr:e.target.parentElement.parentElementdonne accès à latrassociée au boutonSavesur lequel on a cliqué. - on accède aux enfants d'un élément HTML via l'attribut
children; par exemple,tr.children[0]donne accès à la premièretdau sein detr.
- on obtient le parent d'un élément HTML via l'attribut
- Vous pourriez avoir un bouton
Savepour chaque ligne du tableau affichant un film. Lors d'un clic sur un boutonSave, vous faites appel à l'API en faisant la requête de mettre à jour toutes les propriétés du film, même celle n'ayant pas de nouvelles valeurs. - Attention, pour une requête de type
PUT(mise à jour de toutes les propriétés d'une ressource) ou de typePATCH(mise à jour partielle d'une ressource), vous devez indiquer l'id dans l'URL dufetch, et la représentation de données à mettre à jour doit se trouver dans le body de la requête (ce sont les conventions REST que nous avons fixées dans le cadre de ce cours). - Si vous aviez besoin de réaliser une action en cas de changement du contenu d'une cellule dont
contenteditableest activé, vous pouvez gérer le type d'événementinput.
Option 2 pour la mise à jour d'info
Il est aussi possible de créer un nouveau composant Javascript (une page, une modal ou autre) qui reprendrait un formulaire dont les inputs contiendraient déjà les valeurs existantes des propriétés d'un film. Si vous choisissez cette option, c'est à vous de trouver l'inspiration 😉.
