Ce TP servira introduction à l'utilisation d'AJAX (Asynchronous JavaScript and XML) dans une page web.
Dans un premier temps, il s'agira de découvrir la technologie AJAX ainsi que son utilisation à l'aide de l'API fetch, reposant sur l'utilisation des promesses JavaScript.
Les notes des TP seront obtenues à partir de vos dépôts Git, vous devrez donc prendre grand soin de la qualité de ces dépôts et de leurs « commits ».
De manière similaire, les descriptions de vos « commits » devront être claires et informatives, afin de permettre l'évaluation de la progression de votre travail.
Lors de la création du projet, afin de ne pas installer tous les paquets npm à nouveau manuellement, vous allez copier les fichiers nécessaires depuis l'un de vos précédents TP.
Dans un nouveau répertoire de travail nommé « r301-js-fetch », vous copierez les fichiers :
Cette copie peut être réalisée facilement en ligne de commande si l'on considère que vous êtes dans votre nouveau répertoire de travail et que votre précédent projet se trouve dans « ../r301-js-platformer/ » :
cp -r ../r301-js-platformer/{eslint.config.js,.gitignore,package.json,vite.config.js} . mkdir src cp -r ../r301-js-platformer/src/{index.js,index.html} src/ mkdir -p public/css cp -r ../r301-js-platformer/public/css/index.css public/css/
Vous éditerez le fichier « package.json » pour modifier le projet (nom, description, repository...).
Toujours dans le fichier « package.json », vous supprimerez la dépendance inutile vers « phaser », nous ne feront pas de Phaser dans ce TP.
Vous supprimerez tout le contenu des fichiers « src/index.js » et « public/css/index.css », ainsi que les éléments inutiles du fichier « src/index.html » pour ne plus afficher qu'une page vide.
Vous installerez ensuite les paquets de votre projet à l'aide de la commande suivante :
npm install
Enfin, vous pourrez créer un dépôt Git local dans lequel vous réaliserez votre commit initial contenant les fichiers précédents.
Pour finir, vous lui associerez un dépôt distant nommé « r301-js-fetch » sur le GitLab du département sur lequel vous pousserez votre premier commit.
Vous contrôlerez que la copie s'est bien passée et vous ajouterez votre intervenant de TP avec un rôle de « Reporter ».
Le serveur web de développement est un serveur local lancé sur votre machine, celui-ci peut être accédé au travers deux urls : la boucle locale (localhost
) et l'adresse ip de votre machine.
Comme vous le découvrirez dans la suite de ce TP, pour accéder à des ressources d'un autre serveur à l'aide d'AJAX, le site fournissant vos pages doit-être autorisé par le serveur de ressources. Par souci de simplicité, je n'ai autorisé que les requêtes provenant de localhost et non pas toutes les ip du département informatique.
C'est pourquoi vous devez impérativement utiliser l'adresse utilisant localhost pour accéder à vos pages web :
Historiquement, les requêtes AJAX sont réalisées à l'aide de l'objet JavaScript XMLHttpRequest
. Cependant, son utilisation est un peu fastidieuse et peu lisible, et il était courant de l'encapsuler dans une bibliothèque permettant d'en simplifier l'utilisation.
ECMAScript 2015 (juin 2015) introduit l'API fetch simplifiant l'utilisation d'AJAX en introduisant l'utilisation des promesses pour gérer l'asynchronisme.
Vous commencerez par recopier le code suivant comme contenu du script JavaScript principal « src/index.js » :
La fonction print(message, group)
permet d'ajouter le message passé en paramètre dans la page HTML. Le paramètre group
permet de regrouper les messages visuellement en affectant la même couleur pour tous les messages d'un même groupe. La fonction réalise aussi un affichage du message dans la console de développement
Vous remplacerez le fichier CSS « public/css/index.css » par le fichier suivant : « index.css » (télécharger).
Vous devriez obtenir le résultat suivant en ouvrant la page HTML dans votre navigateur :
Vous pouvez constater dans la page que l'affichage est bien réalisé dans l'ordre du script (début, avant le fetch et fin, après le fetch). Mais si vous ouvrez la console de développement, après avoir activé l'affichage des requêtes AJAX, vous constaterez qu'une requête est bien réalisée, mais après l'affichage de la fin du script. En effet, la requête vers la ressource est asynchrone ; elle a lieu lors de l'invocation de la fonction fetch()
, mais le résultat de la réponse n'est disponible qu'une fois que le serveur a répondu, dans notre cas, après que notre script JavaScript « src/index.js » soit terminé, même si celui-ci est terminé.
L'activation de l'affichage des requêtes AJAX dans la console de développement dépend de votre navigateur et de sa version, mais il s'agit généralement d'une option de la console. Vous trouverez ci-dessous des captures d'écran identifiant l'option pour Firefox et Chrome.
ESLint devrait vous indiquer une erreur sur l'utilisation de fetch
. Le linter vous indique que cette fonction est encore experimental dans le cadre de Node, mais elle est bien supportée dans les navigateurs. Vous désactiverez cette erreur pour ce projet dans la configuration d'ESLint :
fetch()
.
Pour l'instant, vous avez réalisé une requête vers une ressource distante, mais vous n'avez pas accès à la réponse associée à cette requête. L'API fetch repose sur les promesses pour gérer l'aspect asynchrone des requêtes, en retournant une promesse lors de l'invocation de la fonction fetch()
.
Comme indiqué dans la documentation des promesses du Mozilla Developers Network, « une promesse représente une valeur qui peut être disponible maintenant, dans le futur, voire jamais ».
Concrètement, une promesse peut être dans l'un de ces trois états :
Pour obtenir la valeur et/ou réagir aux changements d'état d'une promesse, vous ne disposez que de trois méthodes : then()
, catch()
et finally()
. Ces méthodes permettent d'enregistrer une fonction de rappel, qui sera invoquée lors des changements d'état de la promesse, dans l'ordre de l'enregistrement des fonctions de rappel.
Voyons plus en détail les situations dans lesquelles les fonctions de rappel sont invoquées. Lorsque la fonction de rappel est enregistrée à l'aide de :
then()
, la fonction de rappel est invoquée lorsque la promesse est tenue et donc que le serveur a répondu (peu importe le code de réponse HTTP).
catch()
, la fonction de rappel est invoquée lorsque la promesse est rompue et donc qu'une erreur est survenue.
finally()
, la fonction de rappel est invoquée dans tous les cas, que la promesse soit tenue ou rompue.
Vous ajouterez une fonction de rappel à la requête précédente qui affichera un message dans la page lors de la réponse du serveur. Vous constaterez que cet affichage a bien lieu après la fin du script.
Vous réaliserez ensuite une nouvelle requête sur l'URL « https://iut-info.univ-reims.fr/users/jonquet/resources/fetch/ », en lui associant aussi une fonction de rappel affichant un message lors de la réponse du serveur. Vous associerez le groupe 2 à ces affichages. Vous constaterez dans la console de développement que le serveur répond avec un code HTTP 403
(« Forbidden »). Mais puisque le serveur a bien répondu, la promesse est tenue, et c'est toujours la fonction de rappel qui avait été enregistrée par la méthode then()
qui est déclenchée.
Vous devriez obtenir un rendu similaire à l'illustration suivante, l'ordre des réponses aprés la fin du script étant aléatoire :
L'utilisation d'AJAX permet de réaliser des requêtes en arrière-plan dans une page web, et donc potentiellement à l'insu de l'utilisateur. D'un point de vue de la sécurité des utilisateurs, il serait envisageable de réaliser des requêtes vers d'autres sites en profitant que l'utilisateur y soit connecté.
Pour protéger les utilisateurs, une requête entre des domaines différents (CORS : « cross-origin resource sharing ») est interdite, sauf explicitement autorisée par le serveur hébergeant la ressource.
Pour illustrer ce cas, vous ajouterez une nouvelle requête vers l'URL de ce sujet, à l'exterieur du département : « https://iut-info.univ-reims.fr/users/jonquet/intranet/but/r301/tp/fetch/ ». Vous constaterez dans la console de développement un message d'erreur impliquant une requête multiorigines. Vous pourrez constater en inspectant la requête dans l'onglet « réseau », que vous avez bien obtenu le contenu de la page de sujet, mais c'est le navigateur qui vous empêche d'y accéder, car le serveur n'a pas d'en-tête HTTP « Access-Control-Allow-Origin ».
Vous constaterez que les requêtes précédentes, vers « https://iut-info.univ-reims.fr/users/jonquet/resources/fetch/ » contiennent cet en-tête avec la valeur « http://localhost:5173 ». Cette valeur permet de faire une requête AJAX depuis le serveur local de développement.
Vous en profiterez pour afficher un message dans la page lors de la réponse du serveur pour la requête du sujet. Vous pourrez utiliser le groupe 3 pour les affichages de cette requête.
Vous ne devriez pas être confronté à des problèmes de CORS avant le semestre prochain où les infrastructures se complexifieront.
Dans le cadre de votre SAÉ, le serveur de la page HTML initiant la requête AJAX et le serveur de ressources asynchrones seront les mêmes.
La fonction de rappel enregistrée à l'aide de la méthode then()
est invoquée lors de la réponse du serveur, et comme indiqué dans la documentation, la fonction de rappel reçoit comme paramètre une instance de Response
.
Vous créerez une nouvelle fonction responseToHtmlUl(response)
qui recevra en paramètre une instance de Response
et retournera une chaîne de caractères contenant une liste non-ordonnée (ul
), avec un unique élément (li
) affichant le code HTTP pour la réponse passée en paramètre.
La version 2 du protocole HTTP ne fournit plus le texte du status de la réponse. Et donc si le serveur avec le protocole HTTP/2 ou supérieur, la propriété statusText
sera systématiquement une chaîne de caractères vide.
Vous utiliserez cette fonction pour compléter l'affichage du résultat des deux requêtes vers « https://iut-info.univ-reims.fr/users/jonquet/resources/fetch/hello.php » et « https://iut-info.univ-reims.fr/users/jonquet/resources/fetch/ ».
Vous constaterez que, conformément à l'explication de la documentation du MDN, les promesses ne sont pas rejetées en cas d'erreurs HTTP, mais uniquement pour des erreurs réseaux, comme les CORS par exemple. Vous pouvez utiliser les propriétés ok
ou status
pour identifier si une requête a été fructueuse ou non.
Vous ajouterez un nouvel élément de la liste générée par la fonction responseToHtmlUl
contenant la valeur de l'en-tête « Content-Type » de la réponse.
Vous devriez maintenant obtenir un rendu similaire à l'illustration suivante :
responseToHtmlUl(response)
.
La plupart des navigateurs attendent que la réponse soit complète avant d'invoquer la fonction de rappel enregistrée à l'aide de la méthode then()
, mais l'API fetch prévoit qu'en cas de corps de requête trop volumineux, la réponse soit accessible avant la fin de la réception. C'est pourquoi le corps de la requête n'est accessible qu'au travers de promesses ou de flux.
Il est donc courant de devoir gérer une première promesse pour la requête d'une ressource effectuée avec fetch
suivie d'une seconde pour obtenir le contenu du corps de la réponse. Afin de simplifier l'écriture et la lecture de cet enchaînement de promesses, il est conseillé de profiter de la syntaxe de chaînée.
Pour le moment, l'unique réponse valide que vous avez obtenue contient une charge utile de type texte. Suite à l'affichage actuel, vous ajouterez un affichage dans la page contenant le contenu du corps de la réponse.
Le rendu de votre page devrait maintenant ressembler à l'illustration suivante :
La ressource « https://iut-info.univ-reims.fr/users/jonquet/resources/fetch/hello.php » accepte un paramètre d'URL user
. Vous réaliserez deux nouvelles requêtes AJAX vers cette ressource en spécifiant le nom d'utilisateur à saluer.
Dans le cas où les paramètres de la « query string » (clé/valeur) sont fixés dynamiquement à l'exécution, vous êtes encouragé à utiliser la fonction encodeURIComponent
afin de vous éviter toutes surprises.
De manière similaire à la requête sans paramètre, vous afficherez dans la page le status et le type mime de la réponse, ainsi que son contenu.
Il est aussi possible de modifier la méthode de la requête, pour faire une requête « POST », en modifiant la propriété method
des options de la fonction fetch()
. Vous modifierez votre deuxième requête pour faire une requête de type « POST ». Vous pourrez ensuite ajouter un paramètre times
dans le corps de la requête, pour spécifier le nombre de fois que doit être affiché le mot « Hello » (le serveur borne times
à 5). Vous penserez à préciser le type mime « application/x-www-form-urlencoded » dans les en-têtes de la requête.
Il est possible de laisser la fonction fetch()
déterminer automatiquement le contenu de la charge utile si les données sont clairement identifiables.
Vous devriez obtenir un rendu similaire à l'illustration suivante :
Vous avez vu en TP de PHP et en Symfony comment utiliser la session pour gérer l'authentification. Par défaut, dans le cadre des requêtes multiorigines, le cookie de session n'est pas transmis dans la requête HTTP, ce qui empêche le serveur de charger la session et donc de maintenir l'identification de l'utilisateur.
Pour transmettre le cookie de session lors de la requête AJAX, vous devez utiliser la propriété credentials
des options de la fonction fetch()
avec une valeur « include
».
Vous ajouterez ensuite deux nouvelles requêtes sur le script PHP « https://iut-info.univ-reims.fr/users/jonquet/resources/fetch/last-user.php », la première sans spécifier la propriété credentials
et la seconde avec. Et pour chacune, vous afficherez le statut et le type mime de la réponse ainsi que le contenu du corps de la réponse.
Vous devriez constater que les deux requêtes se comportent de manière identique, car le script enregistrant les données dans la session est le script « hello.php » et que vous devez donc également lui transmettre le cookie de session pour qu'il partage la même session que « last-user.php ».
Vous ajouterez le cookie de session à la requête en POST et vous devriez maintenant obtenir le nom de l'utilisateur défini dans la requête POST dans la réponse à la requête sur « last-user.php » contenant le cookie de session.
Encore une fois, vous ne devriez pas être confronté aux requêtes multiorigines dans le cadre de votre SAÉ.
Vous avez probablement constaté que lorsque plusieurs requêtes sont émises rapidement, il n'est pas possible de prévoir l'ordre dans lequel les réponses sont reçues.
Vous allez mettre en place une petite illustration de cette problématique.
Après vous être assuré d'avoir ajouté votre travail précédent à votre dépôt Git, vous remplacerez le body
de votre page HTML par le code suivant :
Puis, vous supprimerez le contenu du script « index.js » pour le remplacer par du code dont le fonctionnement devra permettre :
button
et div
stockant respectivement des références sur le bouton et l'élément « div.info » de la page, que vous obtiendrez à l'aide de querySelector()
.
Vous devriez obtenir un rendu similaire à l'illustration suivante et vous devriez pouvoir constater que l'ordre d'arrivée des réponses n'est pas nécessairement celui des requêtes correspondantes. Ceci peut conduire à des aberrations, comme dans l'illustration où la dernière réponse correspond à la deuxième requête.
Pour résoudre le problème précédent, vous allez utiliser l'objet JavaScript AbortController
afin d'interrompre la requête précédente avant d'en émettre une nouvelle.
Concrètement, il s'agit de transmettre la propriété signal
de l'instance d'AbortController
à la propriété signal des options de la fonction fetch()
pour que la requête s'enregistre auprès du contrôleur.
Il est ensuite possible d'abandonner toutes les requêtes enregistrées en invoquant la méthode abort()
du contrôleur.
Vous noterez que le contrôleur est à usage unique, il ne peut plus enregistrer de nouvelles requêtes après l'invocation de sa méthode abort()
. Un nouveau contrôleur doit alors être créé.
Certains navigateurs considèrent l'abandon de la requête AJAX comme un échec d'obtention de la ressource, vous ajouterez donc une fonction de rappel au gestionnaire d'erreurs de votre requête AJAX, qui affichera un message dans la console de développement.
AbortController
pour abandonner la requête précédente lors de l'émission d'une requête.