Aller au contenu principal

Requêtes asynchrones

BoardGame

Préambule

Comme défini dans la documentation officielle de mozilla : "La programmation asynchrone est une technique qui permet à un programme de démarrer une tâche à l'exécution potentiellement longue et, au lieu d'avoir à attendre la fin de la tâche, de pouvoir continuer à réagir aux autres évènements pendant l'exécution de cette tâche. Une fois la tâche terminée, le programme en reçoit le résultat. De nombreuses fonctions fournies par les navigateurs, dont les plus intéressantes, peuvent prendre un certain temps et sont donc asynchrones.". Dans le cadre de ce tuto, nous allons étudier l'API de fetch(). Avec, fetch()nous allons pouvoir envoyer une requête qui pourra nous répondre sous différents formats.

Chargement asynchrone d'une image

Tout d'abord nous allons étudier un exemple de requête asynchrone permettant de charger une image. On imagine que cette requête peut prendre du temps. L'image peut être énorme ou le serveur peut simplement mettre du temps à répondre. Après avoir lancé un serveur local (via le live server de VSCode) étudiez le code ci-dessous :

const monImage = document.querySelector('img');
// construction d'un objet requete
// Ici on passe simplement le nom de l'image que l'on veut récupérer sur le serveur
//Evidemment vous devez avoir une image sur votre serveur local (ici dans le même répertoire que ce fichier)
//on verra plus tard qu'il est possible de passer des paramètres
let maRequete = new Request('IUT.png');
//let maRequete = new Request('https://www.iut.u-bordeaux.fr/info/assets/images/logo_ENT_Bx.png');

//la requête (exécutée côté client) part vers le serveur
fetch(maRequete)
//.then est exécuté quand la requète est revenue. Cette requète est aynschrone.
//donc pendant le temps d'attente le site reste utilisable (l'appel n'est pas bloquant)
.then(reponse => {
//on vérifie que le serveur à retourné un code 200
if (!reponse.ok) {
throw new Error(`erreur HTTP! statut: ${reponse.status}`);
}
//fetch retourne ce qu'on appelle une promesse
//c'est un objet qui indique que tout c'est bien passé (ou pas).
//ici la promesse contient la réponse (ici un blob c'est à dire un fichier binaire : l'image)
return reponse.blob();
})
//grace à cette promesse retournée on est certain que l'image est bien téléchargée,
//avec le then on peut ensuite chainer la suite des traitements.
//Sans cet appel asynchrone,
//on pourrait en venir à la situation ou l'image serait ajoutée via le code JS alors
// qu'elle n'est pas encore physiquement sur votre ordinateur
.then(reponse => {
//insertion de l'image
let URLobjet = URL.createObjectURL(reponse);
monImage.src = URLobjet;
});
astuce

Si vous décommentez la ligne

//let maRequete = new Request('IUT.png');
let maRequete = new Request('https://www.iut.u-bordeaux.fr/info/assets/images/logo_ENT_Bx.png');

L'image ne s'affichera pas. Vous obtenez une CORS erreur.

BoardGame

L'erreur CORS (Cross-Origin Resource Sharing) se produit lorsqu’une requête est faite à un domaine différent de celui de la page web qui l'a initiée (localhost dans votre cas), et que le serveur distant n'autorise pas explicitement cette requête. C'est une mesure de sécurité mise en place par les navigateurs web pour prévenir les attaques de type Cross-Site Request Forgery (CSRF) et d'autres vulnérabilités.

Dans le contexte du code ci-dessus, le serveur hébergeant l'image n'autorise pas explicitement votre site, le navigateur bloque la requête pour des raisons de sécurité.

Chargement asynchrone d'un fichier JSON

Dans l'exemple ci-après on charge de manière asynchrone un fichier JSON déjà présent sur votre disque dont on souhaite afficher le contenu. Le principe est le même que pour l'image sauf que l'on sait qu'il faut parser des données de type texte au format JSON.

foot.json
[
{
"annee":1930,
"vainqueur":"Uruguay"
},
{
"annee":1934,
"vainqueur":"Italie"
},
{
"annee":1938,
"vainqueur":"Italie"
},
{
"annee":1950,
"vainqueur":"Uruguay"
},
{
"annee":1954,
"vainqueur":"Allemagne"
},
{
"annee":1958,
"vainqueur":"Brésil"
}
]

Le code source permettant d'afficher toutes les données est :

const myList = document.querySelector('ul');
const myRequest = new Request('foot.json');

fetch(myRequest)
//Je vous encourage à mettre des points d'arrêt dans le debugger de votre navigateur
//pour observer le contenu des différents objets
.then((response) => response.json())
.then((data) => {
//JSON est un format qui permet de boucler sur le contenu (un Array)
for (const v of data) {
const listItem = document.createElement('li');
//il faut avoir lu l'API pour savoir comment se nomment les attributs (ici "vainqueur" et "année")
listItem.textContent = `${v.vainqueur} a gagné la coupe du monde en ${v.annee}`;
myList.appendChild(listItem);
}
})
.catch(console.error);
Attention

Le code précédent fonctionne, car le fichier JSon est déjà sur votre disque. Comme pour l'exemple avec l'image, si vous voulez charger une ressource directement depuis une URL, vous aurez une erreur CORS. Pour pouvoir récupérer une ressource précise sur un serveur, il faut qu'il vous y autorise. C'est ce qu'on va étudier tout de suite.

Paramétrer une requête

Concept

À partir de maintenant votre serveur live reload ne sera plus suffisant. Nous allons questionner un vrai serveur. Celui-ci contient les résultats de la coupe du monde de foot de 2006 à 2018.

astuce

Ici il n'y a pas de problème de CORS. Mon serveur est configuré pour ne pas en tenir compte.

Il y a deux endPoints disponibles sur mon serveur :

  • https://api-cours-s1.codenestedu.fr/finalesCoupeDuMonde retournera un tableau d'objets (du JSON)
https://api-cours-s1.codenestedu.fr/finalesCoupeDuMonde
[
{"annee":2018,"finaliste":"Croatie","gagnant":"France","score":"4-2"},
{"annee":2014,"finaliste":"Argentine","gagnant":"Allemagne","score":"1-0"},
{"annee":2010,"finaliste":"Pays-Bas","gagnant":"Espagne","score":"1-0"},
{"annee":2006,"finaliste":"France","gagnant":"Italie","score":"5-3 (t.a.b.)"}
]
  • https://api-cours-s1.codenestedu.fr/finalesCoupeDuMonde?annee=2018 retournera un seul objet (toujours du JSON)
https://api-cours-s1.codenestedu.fr/finalesCoupeDuMonde?annee=2018
{
"annee":2018,
"finaliste":"Croatie",
"gagnant":"France",
"score":"4-2"
}
  • si vous appellez mon API avec une année qui n'existe pas, le serveur vous retournera une erreur 404 avec un message d'erreur (toujours en JSON).
https://api-cours-s1.codenestedu.fr/finalesCoupeDuMonde?annee=2007
{"error":"Year not found"}
astuce

Testez directement dans votre navigateur, en faisant copier/coller de la requête

Une autre façon d'écrire cet enchainement de codes asynchrones consiste à utiliser await. De cette façon, le code patiente jusqu'à ce que la promesse soit réglée et la valeur de résolution de la promesse est fournie comme valeur de retour, ou alors la valeur d'échec déclenche une erreur. C'est exactement le même fonctionnement que then. La syntaxe est simplement plus légère. Si ce code est dans une fonction, il faut que le mot-clef async soit explicitement indiqué. Votre fonction est explicitement asynchrone.

const maFonctionAsynchrone = async () => {
try {
let response = await fetch(url, options); // se résout avec des en-têtes de réponse
// La ligne ci-dessous ne sera exécutée que si la réponse est arrivée.
let result = await response.json(); // lit le corps en tant que JSON
// La ligne ci-dessous ne sera exécutée que si la réponse est arrivée.
console.log("coucou");
} catch (error) {
console.error('Error:', error);
}
}

GET et POST et PATCH

Il est possible d'échanger avec le serveur de plusieurs manières différentes. Nous allons-en étudier 3, avec à chaque fois des paramètres passés au serveur :

  • GET : Utilisez GET pour récupérer des informations sur une ressource existante à partir du serveur. Lorsqu'on veut récupérer une ressource, les paramètres sont passés dans l'URL. Par exemple, récupérer la liste de tous les utilisateurs.
  • POST : Utilisez POST lorsque vous créez une nouvelle ressource sur le serveur. Les paramètres sont écrits dans le corps de la requête HTTP. Par exemple, demander à créer un nouvel utilisateur sur le serveur à partir d'informations saisies dans un formulaire.
  • PATCH: Utilisez PATCH lorsque vous effectuez une mise à jour partielle d'une ressource existante sur le serveur. Par exemple, modifier le mot de passe d'un utilisateur existant. Les paramètres sont écrits dans le corps de la requête HTTP.
info

Le tableau ci-dessous résume les différences entre GET et POST/PATCH

GETPOST et PATCH
VisibilitéVisible pour l’utilisateur dans le champ d’adresseInvisible pour l’utilisateur, mais il peut regarder les paquets
Marque-page et historique de navigationLes paramètres de l’URL sont stockés en même temps que l’URL.L’URL est enregistrée sans paramètres URL.
Cache et fichier log du serveurLes paramètres de l’URL sont stockés sans chiffrementLes paramètres de l’URL ne sont pas enregistrés automatiquement.
Comportement lors de l’actualisation du navigateur / Bouton « précédent »Les paramètres de l’URL ne sont pas envoyés à nouveau.Le navigateur avertit que les données du formulaire doivent être renvoyées.
Type de donnéesCaractères ASCII uniquement.Caractères ASCII, mais également des données binaires.
Longueur des donnéesLimitée - longueur maximale de l’URL à 2 048 caractères.Illimitée.

Exercice dirigé (tout le code est donné)

Vous allez coder une application qui permet d'afficher/ajouter/modifier les résultats des finales de coupe du monde.

Toutes les données sont sur un serveur. en utilisant fetch, vous allez faire des requêtes au serveur comme on peut le voir sur la vidéo. À chaque fois qu'on appuie sur un bouton vert, une requête part vers le serveur pour récupérer des informations ou pour demander une cration d'information ou une modification d'une information existante.

Exemple d'un GET pour récupérer les données sur le serveur

<body>
<button id="allFinalsBtn">Afficher toutes les finales</button>
<div>
<label for="yearInput">Année:</label>
<input type="number" id="yearInput" min="1930" max="2022">
<button id="specificFinalBtn">Afficher la finale</button>
</div>
<ul id="finalsList"></ul>

<script>
document.getElementById('allFinalsBtn').addEventListener('click', () => {
fetch('https://api-cours-s1.codenestedu.fr/finalesCoupeDuMonde')
.then(response => response.json())
.then(data => {
const finalsList = document.getElementById('finalsList');
finalsList.innerHTML = ''; // Efface la liste précédente
data.forEach(finale => {
const listItem = document.createElement('li');
listItem.textContent = `Année: ${finale.annee}, Finaliste: ${finale.finaliste}, Gagnant: ${finale.gagnant}, Score: ${finale.score}`;
finalsList.appendChild(listItem);
});
})
.catch(error => console.error('Erreur lors de la récupération des données :', error));
});

document.getElementById('specificFinalBtn').addEventListener('click', () => {
const year = document.getElementById('yearInput').value;
if (year.trim() === '') {
alert('Veuillez saisir une année.');
return;
}

fetch(`https://api-cours-s1.codenestedu.fr/finalesCoupeDuMonde?annee=${year}`)
//si on arrive ici (la première étape de la gestion de la réponse du serveur)
//on teste si on n'a pas eu une erreur retournée
.then(response => {
//si erreur 404
if (!response.ok) {
//il faut analyser le json retourner par le serveur {"error":"Year not found"}
return response.json().then(data => {
//ce throw est récupéré par la catch un peu plus bas
throw new Error(data.error);
});
}
//si pas d'erreur alors le json est "retournée" au then juste en dessous
return response.json();
})
//si on arrive ici (la seconde étape de la gestion de la réponse du serveur)
//on peut traiter sans crainte cette réponse
.then(data => {
const finalsList = document.getElementById('finalsList');
finalsList.innerHTML = ''; // Efface la liste précédente
const listItem = document.createElement('li');
listItem.textContent = `Année: ${data.annee}, Finaliste: ${data.finaliste}, Gagnant: ${data.gagnant}, Score: ${data.score}`;
finalsList.appendChild(listItem);
})
.catch(error => {
alert(error.message);
console.error('Erreur lors de la récupération des données :', error);
});
});
</script>
</body>

Exemple d'un POST pour créer une nouvelle entrée dans la base de données

La route /finaleImaginaire attend que vous lui passiez un JSON de la forme

{
"annee":"1934",
"finaliste":"Italie",
"gagnant":"Groland",
"score":"10-0"
}

le serveur vous répondra, selon le cas :

  • { message: 'Finale imaginaire ajoutée avec succès' } avec le statut 201 si tout s'est bien passé. Votre finale fictive a bien été enregistrée dans la base de données du serveur
  • { error: 'L\'année de cette finale a déjà été jouée, il est interdit de la modifier' } avec le statut 400 si l'année existe déjà (l'API ne vous autorise pas ajouter une finale déjà existante)
  • { error: 'Object too large' } avec le statut 400 si vous avez envoyé des paramètres trop gros (trop de texte). C'est une sécurité de base côté serveur pour éviter d'être noyé de données.
  • { error: 'Le nombre maximal de finales a été atteint' } avec le statut 400 si vous essayez de spammer le serveur en lui faisant créer un grand nombre de finales
<form id="imaginaryFinalForm">
<label for="anneeInput">Année:</label>
<input type="number" id="anneeInput" min="1930" max="2022" required><br>
<label for="finalisteInput">Finaliste:</label>
<input type="text" id="finalisteInput" required><br>
<label for="gagnantInput">Gagnant:</label>
<input type="text" id="gagnantInput" required><br>
<label for="scoreInput">Score:</label>
<input type="text" id="scoreInput" required><br>
<button type="submit">Ajouter Finale Imaginaire</button>
</form>

<script>
let serveur = "https://api-cours-s1.codenestedu.fr/";
document.getElementById('imaginaryFinalForm').addEventListener('submit', (event) => {
//annule l'action par défaut associée à un événement spécifique sur un élément HTML.
//sinon le comportement par défaut serait de recharger la page
event.preventDefault();

//construction d'un objet à partir des informations du formulaire
const formData = {
annee: document.getElementById('anneeInput').value,
finaliste: document.getElementById('finalisteInput').value,
gagnant: document.getElementById('gagnantInput').value,
score: document.getElementById('scoreInput').value
};

//requete POST
fetch(`${serveur}/finaleImaginaire`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
//le données sont associées à la requete
body: JSON.stringify(formData)
})
.then(response => {
//si le serveur a répondu une erreur
if (!response.ok) {
return response.json().then(data => {
throw new Error(data.error);
});
}
return response.json();
})
.then(data => {
//si tout s'est bien passé
alert('Finale imaginaire ajoutée avec succès!');
console.log('Réponse du serveur:', data);
})
.catch(error => {
alert(error.message);
console.error('Erreur lors de l\'ajout de la finale imaginaire:', error);
});
});
</script>

Exemple d'un PATCH pour créer une nouvelle entrée dans la base de données

<form id="updateScoreForm">
<label for="anneeToUpdate">Année:</label>
<input type="number" id="anneeToUpdate" min="1930" max="2222" required><br>
<label for="newScore">Nouveau Score:</label>
<input type="text" id="newScore" required><br>
<button type="submit">Mettre à jour le score de la finale</button>
</form>

<script>
document.getElementById('updateScoreForm').addEventListener('submit', (event) => {
event.preventDefault();
const annee = document.getElementById('anneeToUpdate').value;
const newScore = document.getElementById('newScore').value;

// Construire le corps de la requête
const requestBody = {
annee: parseInt(annee),
score: newScore
};

fetch(`${serveur}/modifierScoreFinal`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
})
.then(response => {
if (!response.ok) {
return response.json().then(data => {
throw new Error(data.error);
});
}
return response.json();
})
.then(data => {
alert('Score de la finale mis à jour avec succès!');
console.log('Réponse du serveur:', data);
})
.catch(error => {
alert(error.message);
console.error('Erreur lors de la mise à jour du score de la finale:', error);
});
});
</script>

Exercice

Travail à faire

Comme sur la vidéo, créez une page web permettant de

  • cliquer sur le rond orange pour envoyer une requête demandant une citation de JCVD
  • afficher cette citation
  • informer l'utilisateur quand le serveur vous a envoyé toutes ses citations
  • le webService de citations de JCVD tourne sur https://api-cours-s1.codenestedu.fr/
  • il y a un seul endpoint fourni par le serveur : /citation et en GET uniquement.
  • ce endpoint nécessite un paramètre numero pour spécifier l'indice de la citation que vous souhaitez obtenir. Par exemple, si vous souhaitez récupérer la deuxième citation, vous pouvez envoyer une requête à https://api-cours-s1.codenestedu.fr/citation?numero=1.
  • le serveur renverra la citation demandée sous forme de réponse JSON. Si le numéro de citation est valide, la réponse contiendra la citation demandée. Sinon, le serveur renverra un code d'erreur HTTP approprié (400 pour une mauvaise requête ou 404 si la citation demandée n'existe pas) avec un message d'erreur expliquant le problème.
  • si tout va bien, le serveur répondra avec une réponse JSON qui ressemblera à ceci :
{
"citation": "Texte de la citation demandée"
}

Cette réponse contient la citation demandée, avec le texte correspondant.

<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title>JCVD > Chuck </title>
<style type="text/css">
body {
background : repeat rgba(235, 150, 108, 0.1);
}

.main {
position : relative;
}


.mb-wrap {
margin : 20px auto;
padding : 20px;
position : relative;
width : 300px;
}

.mb-wrap p {
margin : 0;
padding : 0;
}

.mb-wrap blockquote {
margin : 0;
padding : 0;
position : relative;
}

.mb-wrap cite {
font-style : normal;
}

.mb-style-2 blockquote {
padding-top : 150px;
}

.mb-style-2 blockquote:after {
background: none repeat scroll 0 0 rgba(235, 150, 108, 0.8);
border-radius: 50% 50% 50% 50%;
color: rgba(255, 255, 255, 0.5);
content: "❞";
font-family: 'icons';
font-size: 70px;
height: 130px;
left: 50%;
line-height: 130px;
margin-left: -65px;
position: absolute;
text-align: center;
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.1);
top: 0;
width: 130px;
}
.mb-style-2 blockquote:before {
border-left: 5px solid rgba(235, 150, 108, 0.1);
border-radius: 50% 50% 50% 50%;
content: "";
height: 500px;
left: -50px;
position: absolute;
top: 0;
width: 500px;
z-index: -1;
}

.mb-style-2 blockquote p {
background : none repeat scroll 0 0 rgba(255, 255, 255, 0.5);
box-shadow : 0 -6px 0 rgba(235, 150, 108, 0.2);
color : rgba(235, 150, 108, 0.8);
display : inline;
font-family : Baskerville, Georgia, serif;
font-style : italic;
font-size : 28px;
line-height : 46px;
text-shadow : 0 1px 1px rgba(255, 255, 255, 0.5);
}

.mb-style-2 blockquote p:before{
color:black;
content: "Citation de JCVD";
display: block;

}

.mb-attribution {
text-align : right;
}

.mb-author {
color : #D48158;
font-size : 18px;
font-weight : bold;
padding-top : 10px;
text-shadow : 0 1px 1px rgba(255, 255, 255, 0.1);
text-transform : uppercase;
}

cite a {
color : #D7AA94;
font-style : italic;
}

cite a:hover {
color : #D48158;
}

</style>
</head>
<body>

<section id="pensees" class="main">
<div class="mb-wrap mb-style-2">
<blockquote>
<p></p>
</blockquote>

</div>

<div class="mb-attribution">
<p class="mb-author">
JCVD
</p>
<cite>
<a href="http://citation-celebre.leparisien.fr/auteur/jean-claude-van-damme">Recueil de citations</a>
</cite>
</div>

</section>

<script >

</script>
</body>
</html>