Il s'agit d'une version statique de l'intranet d'A. Jonquet, certaines fonctionnalités sont donc potentiellement non fonctionnelles.
Rejoindre la version dynamique 🔒
R301
Navigation

Introduction à AJAX

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.

Remarque importante

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.

Objectifs de la séance

  • Comprendre le fonctionnement d'AJAX
  • Utilisation de l'API fetch
  • Utilisation des promesses JavaScript
  • Exploitation d'une API web à l'aide d'AJAX
  • Approfondissement du langage JavaScript

Mise en place du projet

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 votre précédent TP.

Dans un nouveau répertoire de travail nommé « r301-js-fetch », vous copierez les fichiers :

  • « .eslintrc.js », le fichier de configuration d'ESLint,
  • « .gitignore », le fichier d'exclusion de Git,
  • « babel.config.js », le fichier de configuration de Babel,
  • « package.json », c'est le fichier décrivant le projet, il contient la liste de paquets à installer.
  • « webpack.config.js », le fichier de configuration de Webpack et de son serveur de développement,
  • « src/index.js », le fichier principale de la bibliothèque JavaScript,
  • « src/template.html », le patron de la page HTML, servi par le serveur de développement.
  • « public/css/index.css », la feuille de style.

Cette copie peut être réalisée facilement en ligne de commande si l'on considère que votre précédent projet se trouve dans « ../r301-js-introduction/ » :

cp -r ../r301-js-introduction/{.eslintrc.js,.gitignore,babel.config.js,package.json,webpack.config.js} .
mkdir src
cp -r ../r301-js-introduction/src/{index.js,template.html} src/
mkdir -p public/css
cp -r ../r301-js-introduction/public/css/index.css public/css/

Vous éditerez le fichier « package.json » pour modifier le projet (nom, description, repository...).

Vous supprimerez tout le contenu du fichier « src/index.js », ainsi que les éléments inutiles du fichier « src/template.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 ».

Travail à réaliser
  • Créer un répertoire local.
  • Y copier les fichiers nécessaires du TP précédent.
  • Y créer un dépôt Git local.
  • Répliquer le dépôt local sur le serveur distant GitLab.
  • Vérifier les membres et leurs rôles.

Une première requête

Remarque importante

Lores du lancement du serveur web de développement de Webpack, celui-ci vous affiche dans la console deux urls pour acceder à votre site : 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 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 http://localhost:8080 pour accéder à vos pages web :

Illustration du message de démarrage du serveur de développement local&

Vous pouvez automatiser l'ouverture du navigateur à l'adresse http://localhost:8080 en ajoutant l'option open à la configuration du serveur de développement ou en ajoutant l'option --open à la commande de démarrage du serveur de développement.

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 :

Illustration de l'affichage de la première requête

Vous pouvez constater dans la page que l'affichage est bien réalisé dans l'ordre du script (début, avant le fetch, après le fetch et fin). 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, après que le script JavaScript soit terminé, et même si celui-ci est terminé.

Remarque importante

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.

Firefox
Illustration de l'option d'affichage des requêtes AJAX dans Firefox
Chrome
Illustration de l'option d'affichage des requêtes AJAX dans Chrome
Travail à réaliser
  • Mise en place d'une requête AJAX à l'aide de fetch().
  • Affichage des requête AJAX dans la console de développement.

Exploitation de la promesse

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 :

  • « pending » (en attente) : l'état initial, la promesse n'est pas encore résolue, la valeur n'est pas encore disponible,
  • « fulfilled » (tenue) : l'opération a réussi, la valeur est disponible,
  • « rejected » (rompue) : l'opération a échoué, la valeur ne sera jamais disponible.

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 :

Illustration de l'affichage de la seconde requête
Travail à réaliser
  • Ajout d'une fonction de rappel à la réponse du serveur.
  • Ajout d'une requête vers une ressource inexistante et affichage d'un message à la réponse du serveur.

Interlude sur les « CORS »

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:8080 ». 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.

Remarque importante

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.

Travail à réaliser
  • Ajout d'une nouvelle requête AJAX vers ce sujet de TP et affichage d'un message à la réponse du serveur.

Exploration de la réponse du serveur

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 et le texte associé pour la réponse passée en paramètre.

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 :

Illustration de l'affichage détaillé des réponses
Travail à réaliser
  • Création de la fonction responseToHtmlUl(response).
  • Affichage du statut de la requête.
  • Affichage du type mime de la charge utile de la réponse.

Exploitation du corps de la réponse

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 :

Illustration de l'affichage avec le corps de la réponse
Travail à réaliser
  • Affichage du contenu de la réponse.

Paramètre et type de requête

La ressource « https://iut-info.univ-reims.fr/users/jonquet/resources/fetch/hello.php » accepte un paramètre d'URL user. Vous réaliserez une nouvelle requête AJAX vers cette ressource en spécifiant un 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 pourrez ensuite ajouter un paramètre times dans le corps de la requête (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.

Remarque importante

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 :

Illustration de l'affichage de requêtes avec paramètres
Travail à réaliser
  • Ajout d'une requête GET contenant un paramètre d'URL.
  • Ajout d'une requête POST contenant un paramètre dans le corps de la requête.

Interlude sur la session PHP et les requêtes multiorigines

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 ».

Cependant, les navigateurs sont de plus contraignants sur les requêtes AJAX multiorigines, pour permettre la transmission des cookies de session, l'échange doit être sécurisé. Vous allez donc configurer votre serveur de développement pour qu'il utilise le protocole HTTPS. Dans le fichier de configuration « webpack.config.js », vous ajouterez une nouvelle propriété server à la configuration du serveur avec la valeur « https ». Vous penserez à relancer votre serveur.

Remarque importante

Le serveur de développement n'étant pas accéssible de l'extérieur, il ne dispose pas d'un vrai certificat pour faire du HTTPS, il s'agit d'un certificat autosigné, c'est-à-dire que le serveur certifie lui-même son identité. Évidement, les navigateurs ne font pas confiance à ce type de certificat, et vous devrez donc accepter l'exception de sécurité de votre navigateur pour pouvoir continuer à utiliser votre serveur de développement.

De plus, selon votre navigateur, vous devez autoriser le navigateur à enregistrer les cookies pour la ressource « https://iut-info.univ-reims.fr » depuis les pages de votre serveur de développement. C'est généralement fait depuis le bouton indiquant que l'URL n'est pas sécurisé, à gauche de la barre d'adresse.

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.

Remarque importante

Encore une fois, vous ne devriez pas être confronté aux requêtes multiorigines dans le cadre de votre SAÉ.

Travail à réaliser
  • Configuration du serveur de développement en HTTPS.
  • Ajout du cookie de session à la requête POST.
  • Ajout d'une requête vers « https://iut-info.univ-reims.fr/users/jonquet/resources/fetch/last-user.php » sans cookie de session.
  • Ajout d'une requête vers « https://iut-info.univ-reims.fr/users/jonquet/resources/fetch/last-user.php » avec cookie de session.

Asynchronisme des requêtes

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 :

  • De créer deux variables 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().
  • D'initialiser un compteur de clics.
  • D'ajouter une fonction de rappel sur le clic du bouton, qui incrémentera le compteur, affichera la valeur du compteur sur le bouton et réalisera une requête AJAX vers la resource « https://iut-info.univ-reims.fr/users/jonquet/resources/fetch/echo.php » en transmettant la valeur du compteur dans le paramètre d'URL « nb ».
  • Lors de la réponse du serveur, la charge utile de la réponse HTTP sera injectée dans le contenu du « div » de la page.

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.

Illustration de l'asynchronisme des requêtes
Travail à réaliser
  • Remplacer le contenu de la page HTML.
  • Remplacer le contenu du script CSS.
  • Remplacer le contenu du script JavaScript.
  • Observer les modifications d'ordre entre des requêtes successives et les réponses correspondantes.

Abandon de 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.

Travail à réaliser
  • Utilisation d'une instance d'AbortController pour abandonner la requête précédente lors de l'émission d'une requête.
A. Jonquet DUT-INFO/REIMS