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
:
await
est 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 leawait
deconst 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
async
renvoie automatiquement une promesse ; cela signifie dans le code ci-dessus que la fonctionHomePage
est 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
/catch
pour 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
submit
sur 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 typesubmit
plutôt que des événements de typeclick
sur le boutonsubmit
afin notamment de prendre en compte si l'utilisateur appuie surEnter
pour 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é
action
du formulaire, ou sur la même URL que la page en cours siaction
n'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éralementoptions
qui doit contenir la propriétémethod
. - lorsque l'on doit envoyer des données dans le
body
d'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éstitle
etcontent
au format JSON. Nous devons donc utiliser la méthodeJSON.stringify
qui 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
header
et 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 fonctionnelNavigate
offert au sein du moduleNavigate.js
dans 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 unthrow
d'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
Delete
pour chaque ligne du tableau affichant les films. Lors du clic sur un boutonDelete
, vous feriez une requête de typeDELETE
vers 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 boutonDelete
pourrait contenir undata-attribute
étant l'id du film. Il est aussi possible d'utiliser la propriétéid
du 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
title
et lelink
ne 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'unetd
qui se trouve elle-même au sein d'unetr
:e.target.parentElement.parentElement
donne accès à latr
associée au boutonSave
sur 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èretd
au sein detr
.
- on obtient le parent d'un élément HTML via l'attribut
- Vous pourriez avoir un bouton
Save
pour 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
contenteditable
est 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 😉.