Grégory Bourguin
SysReIC - LISIC - ULCO
Javascript (Requêtes)

AJAX

AJAX (Asynchronous JavaScript And XML) désigne un ensemble de technologies (HTML, CSS, Javascript, DOM, ... l'objet XMLHttpRequest) qui peuvent être utilisées conjointement pour créer des applications web qui présentent des interfaces utilisateurs mises à jour de manière incrémentales, c.a.d. ne nécessitant pas le rechargement global des pages.

Dans les faits, une page web utilisant AJAX va exécuter des scripts qui vont lancer des requêtes sur des serveurs web, attendre les réponses, et utiliser les résultats pour transformer dynamiquement le DOM de la page en cours (sans rechargement).

La classe XMLHttpRequest

Au coeur d'AJAX, il y a la classe XMLHttpRequest dont les instances permettent de lancer des requêtes en Javascript sur des serveurs distants.

Pour créer une instance, on écrira :

<script>
    ...
    
    let httpRequest = new XMLHttpRequest()
    // RQ : le nom de variable 'httpRequest' est souvent utilisé
    // mais vous pouvez tout à fait en choisir un autre... 
    
    ...
</script>

Préparation : gestionnaire d'état onreadystatechange

Avant de lancer une requête, il est nécessaire d'indiquer à l'instance d'XMLHttpRequest ce qu'on voudra faire de la réponse.

Pour ce faire, il faut bien comprendre que lancer des requêtes sur un serveur au travers du réseau est plus "aléatoire" (prend plus de temps, peut échouer, ...) qu'une exécution locale :

  • Après avoir lancé une requête, il faudra attendre la réponse.
  • Le traitement de la requête se fait en plusieurs étapes.
  • Il se peut qu'il y ait des problèmes d'exécution (ex. demande de fichier qui n'existe pas, panne de réseau, etc.).

Pour toutes ces raisons, XMLHttpRequest fournit un gestionnaire d'évènements nommé onreadystatechange dans lequel on peut enregistrer un handler (comme pour un click sur un bouton) qui va décrire comment doit réagir votre script pendant qu'il attend la réponse du serveur, lorsque la requête est en cours de traitement, lorsqu'elle est terminée, ou encore lorsqu'il se produit une erreur...


Syntaxe

La syntaxe pour écrire le handler est la suivante :

<script>
    ...

    let handler = function() {
        // détails des traitements en fonction de l'état de la réponse
    }
    httpRequest.onreadystatechange = handler 

    ...
</script>

Ou en plus synthétique :

<script>
    ...
   
    httpRequest.onreadystatechange =  function() {
        // détails des traitements en fonction de l'état de la réponse
    } 

    ...
</script>

Contenu du handler

Comme son nom l'indique, le handler sera déclenché à chaque changement d'état de la requête. Il faut donc (à chaque appel) tester l'état pour savoir comment réagir, ce qui résulte généralement en une suite de if indiquant l'action à exécuter en fonction de chaque état considéré.

L'état de la requête est stocké dans l'attribut readyState de l'instance de XMLHttpRequest.

L'état qui nous intéressera en général est le numéro correspondant à la constante XMLHttpRequest.DONE qui indique que la requête est terminée.

La liste des états possibles est décrite sur :
https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState.


On pourra ensuite vérifier le code de réponse de la requête HTPP (pour savoir si tout s'est bien passé, ou s'il y a eu des problèmes).

Le code de réponse de la requête HTPP est stocké dans l'attribut status de l'instance de XMLHttpRequest.

Le code 200 indique que tout est OK.

De plus amples informations sur les codes de réponse HTTP sont disponibles sur :
https://developer.mozilla.org/fr/docs/Web/HTTP/Status


Enfin, il faut indiquer ce qui va être fait de la réponse reçue.

Le résultat de la requête est stocké dans l'attribut response de l'instance de XMLHttpRequest, cet attribut pouvant être décliné en responseText ou encore responseXML selon le type de réponse attendu.


Exemple de structure de handler "classique" :

<script>
    ...

    let handler = function (){
        if (httpRequest.readyState === XMLHttpRequest.DONE) { // REQUETE TERMINÉE
            
            if (httpRequest.status === 200) { // LA RÉPONSE EST OK : 
                
                // traitement de la réponse
                let resultat = httpRequest.response
                ...
                
            } else { // IL Y A EU UN PROBLÈME
                
                // traitement de l'erreur
                
            }
        }
    }
    httpRequest.onreadystatechange = handler

    ...
</script>

Exécution de la requête

L'instance de XMLHttpRequest étant préparée à traiter les réponses du serveur, il ne reste plus qu'à effectivement lancer la requête avec open suivi de send.

La méthode open(method, url) de XMLHttpRequest permet d'instancier une requête HTTP.
Le paramètre method indique la méthode HTTP à utiliser (GET, POST, PUT, DELETE, ...).
Le paramètre url indique l'URL à laquelle la requête va être envoyée.

NB: il est possible d'ajouter d'autres paramètres, mais nous ne les utiliserons pas ici.
Pour plus de détails : https://developer.mozilla.org/fr/docs/Web/API/XMLHttpRequest/open


La méthode send() de XMLHttpRequest permet d'envoyer une requête vers le serveur avec la méthode et l'URL qui on été spécifiées grâce à open.

Pour plus de détails : https://developer.mozilla.org/fr/docs/Web/API/XMLHttpRequest/send


Le shéma global d'utilisation d'AJAX est donc le suivant :

<script>
    ...

    let url = "..." // une URL valide
    let method = "..." // un choix parmi "GET", "POST", "PUT", ...
    
    // préparation
    let httpRequest = new XMLHttpRequest()
    httpRequest.onreadystatechange = function (){
        // code du handler pour la gestion des états
        ...
    }
    
    // lancement de la requête qui a été préparée
    httpRequest.open(method, url)
    httpRequest.send()

    ...
</script>

Un exemple avec la méthode 'GET'

Le code ci-dessous donne un exemple dans lequel un div nommé #ajax-01-display est rempli dynamiquement avec du contenu HTML récupéré par une instance de XMLHttpRequest qui lance une requête HTTP 'GET'.

ATTENTION:
Si vous voulez tester ce code, vous ne pouvez pas utiliser une URL relative comme je l'ai fait (car votre page n'est pas sur mon serveur..).
Remplacez donc la valeur de url par :
http://web.gregory-bourguin.fr/teaching/php/requests/ajax/01_test.php

<p>Voici du HTML récupéré avec AJAX :</p>

<div id="ajax-01-display" style="width: 50%"></div>

<p>Cool !</p>

<script>
    document.addEventListener('DOMContentLoaded', function (){

        let display = document.querySelector("#ajax-01-display")

        let method = "GET"
        let url = "/teaching/php/requests/ajax/01_test.php"
        // NB : j'ai utilisé ici une URL relative (le script php visé est sur le même serveur).  
        // Si on voulait accéder à un site tiers, il faudrait mettre une URL complète.
        // (... et dans ce cas il faudrait en plus que le serveur tiers visé autorise les CORS...)
        
        let httpRequest = new XMLHttpRequest()
        httpRequest.onreadystatechange = function (){
            if (httpRequest.readyState === XMLHttpRequest.DONE) {
                if (httpRequest.status === 200) {
                    display.innerHTML = httpRequest.response
                } else {
                    alert('ERREUR avec la requête.');
                }
            }
        }
        httpRequest.open(method, url)
        httpRequest.send()
    })
</script>
Resultat

Voici du HTML récupéré avec AJAX :

Cool !

Remarque importante à propos des C.O.R.S.

Il faut savoir que pour des raisons de sécurité, un script utilisant AJAX ne peut par défaut recevoir des données que du serveur dont il est issu.

Ainsi, il n'y a pas de problème pour qu'une page de votre site web utilise AJAX pour récupérer des informations provenant de ce même site (votre site web donc...).

Par contre, pour qu'AJAX puisse consommer des données provenant d'un site tiers, il faut que ce site tiers ait autorisé les CORS (Cross-Origin Resource Sharing) .

Si vous tentez de consommer des informations d'un site tiers n'autorisant pas les CORS, votre script déclenchera une erreur du style : XMLHttpRequest ... blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

JSON

Les serveurs web sont aujourd'hui de plus en plus utilisés non plus "seulement" pour fournir des pages web, mais aussi pour fournir des données. En d'autres termes, dans cette configuration, le serveur ne fournit plus directement la page à afficher, mais "juste" des données/informations qui peuvent être récupérées par un client qui pourra lui-même les mettre en forme et les intégrer dans son interface.

JSON (JavaScript Object Notation) est un format d'échange de données qui par essence peut être facilement consommé par Javascript pour reconstruire des objets.

De fait, JSON est particulièrement adapté à la réception de données par AJAX (Javascript), une réponse de requête en JSON pouvant être transformée en objets Javascript qui seront utilisés pour manipuler le DOM et mettre à jour/compléter la page web en cours d'affichage.

L'objet JSON

JSON est un format d'échange de données, mais en Javascript, c'est aussi un objet.

L'objet JSON permet de :

  • JSON.stringify(object) : sérialiser des objets Javascript (obtenir la représentation d'un objet sous forme de chaine au format JSON).
  • JSON.parse(string) parser des String au format JSON
    (reconstruire un objet à partir d'une chaine au format JSON).

stringify : objet -> string

Dans l'exemple suivant, un objet user est créé en Javascript, puis l'objet est sérialisé dans une chaine qui est alors affichée dans l'élément HTML #json-display. Dans une application plus conséquente, cette chaine pourrait être envoyée sur le réseau, mise dans un fichier, etc.

<style>
    #json-display{
        color: darkgray;
    }
</style>

Voici un objet sérialisé en JSON : <span id="json-display"></span>

<script>
    document.addEventListener('DOMContentLoaded', function (){

        // création d'un objet Javascript
        let user = new Object()
        user.firstName = "Grégory"
        user.lastName = "Bourguin"

        // sérialisation en JSON
        let result = JSON.stringify(user)

        // affichage
        let display = document.getElementById("json-display")
        display.innerText = result
    })
</script>       
Resultat
Voici un objet sérialisé en JSON :

parse : string -> objet

Dans l'exemple suivant, une chaine JSON est utilisée pour reconstruire un objet Javascript user. Les attribut de l'objet sont alors affichés dans des éléments HTML nommés #user-firstname et #user-lastname. Dans une application plus conséquente, cette chaine pourrait provenir du réseau, etc.

<style>
    #user-firstname, #user-lastname{
        color: blue;
        font-weight: bold;
    }
</style>


Voici les attributs d'un objet construit à partir d'une chaine JSON :<br>
Prénom > <span id="user-firstname"></span> <br>
Nom > <span id="user-lastname"></span>

<script>
    document.addEventListener('DOMContentLoaded', function (){

        // chaine en JSON
        let jsonString = '{"firstName":"Grégory","lastName":"Bourguin"}'

        // récupération d'un objet Javascript à partie de JSON
        let user = JSON.parse(jsonString)

        // affichage
        let fname = document.getElementById("user-firstname")
        fname.innerText = user.firstName
        let lname = document.getElementById("user-lastname")
        lname.innerText = user.lastName
    })
</script>    
Resultat
Voici les attributs d'un objet construit à partir d'une chaine JSON :
Prénom >
Nom >

AJAX & JSON

Il existe de nombreux serveurs web qui ont pour but de fournir des données JSON, et puisque ce sont des serveurs web, il est possible de les interroger grâce à AJAX.

Nous allons donc pouvoir écrire des applications web dont la partie cliente en Javascript utilise des données reçue en JSON en réponse à des requêtes AJAX lancées sur des serveurs tiers (ou votre propre serveur si vous le configurez dans ce but...).

API REST

Ces serveurs (appelés REST) sont généralement capables de répondre à diverses questions.

Les différentes questions, la manière de les poser, et le "type" des résultats sont décrits dans l'API REST du serveur. Pour simplifier, on peut imaginer que l'API REST décrit des fonctions (noms, paramètres, types de retour) qui peuvent être exécutées à distance.

Pour poser une question précise, il "suffit" de configurer spécifiquement la requête HTTP qu'on va envoyer en fonction de la question qu'on veut leur poser (et éventuellement du type de réponse qu'on veut recevoir).

Il faut aussi savoir que certains serveurs limitent les accès (par exemple en demandant de configurer les requêtes avec une clé d'identification).

Du point de vue d'AJAX, interroger un serveur REST revient à lancer une requête HTTP avec une instance XMLHttpRequest configurée conformément à l'API du serveur ciblé.

Un exemple : "Les blagues à papa"

Dans l'exemple ci-dessous, nous allons utiliser le serveur https://icanhazdadjoke.com dont le but est de fournir des "blagues à papa"...

Chaque serveur REST étant différent, la première étape consiste à aller consulter la documentation de son API pour savoir :

  • quelles questions peuvent être posées
  • ... et comment s'y prendre !

En consultant la documentation sur https://icanhazdadjoke.com/api, vous constaterez que ce serveur ne pose pas de restrictions ( #authentication ), qu'il propose des blagues en aléatoire ( #fetch-a-random-dad-joke ), des blagues contenant certains mots ( #search-for-dad-jokes ), etc.

Il est aussi possible d'indiquer sous quelle forme on veut recevoir la/les blague(s) : message texte, slack, JSON, image.


Pour notre exemple, nous allons demander une blague aléatoire en JSON.

En AJAX, il faut donc :

  • créer une instance de XMLHttpRequest
  • configurer un header (cf. ci-après) avec "Accept: application/json"
    NB: je ne l'invente pas -> c'est la documentation qui le dit...
  • lancer la requête en HTTP GET sur l'URL : https://icanhazdadjoke.com/
    NB: je ne l'invente pas non plus -> c'est aussi la documentation qui le dit...
  • parser la réponse (faite en JSON) pour obtenir un objet Javascript
    La blague se trouvera dans l'attribut joke.
    NB: je ne l'invente toujours pas non plus -> documentation...
  • afficher la blague reçue dans un des éléments HTML de la page
    (pour que l'utilisateur puisse en profiter :) )

Voici le code correspondant :

<style>
    @import url('https://fonts.googleapis.com/css2?family=Architects+Daughter&display=swap');

    .joke{
        margin: 10px 0;
        background: antiquewhite;
        padding: 15px;

        font-family: 'Architects Daughter', cursive;
        font-size: 1.3em;
    }
</style>

<div id="joke-01" class="joke"></div>

<script>
(function() {

    let display = undefined

    let method = "GET"
    let url = "https://icanhazdadjoke.com/"

    let httpRequest = new XMLHttpRequest()
    httpRequest.onreadystatechange = function () {
        if (httpRequest.readyState === XMLHttpRequest.DONE) {
            if (httpRequest.status === 200) {
                let result = JSON.parse(httpRequest.response)
                display.innerHTML = result.joke
            } else {
                alert('ERREUR avec la requête.');
            }
        }
    }

    // l'envoi est ici pour attendre que le DOM soit chargé
    document.addEventListener('DOMContentLoaded', function () {
        display = document.querySelector("#joke-01")

        httpRequest.open(method, url)
        // configuration de l'entête pour indiquer qu'on veut du JSON (cf. doc de l'API du serveur)
        httpRequest.setRequestHeader('Accept', 'application/json');
        httpRequest.send()
    })

})()
</script>
Resultat

On peut modifier un peu le code :

  • Pour éviter de faire un JSON.parse(...) sur la réponse, il est possible d'indiquer à l'instance XMLHttpRequest que ce qui est attendu est du JSON. Il suffit dans ce cas de fixer l'attribut responseType avec la valeur "json".
  • On peut mettre le chargement de blague dans une fonction getNewJoke() qu'on lie au click d'un bouton (pour charger dynamiquement de nouvelles blagues).
<div id="joke-02" class="joke"></div>

<button id="get-new-joke" class="btn">Nouvelle blague..</button>

<script>
    (function() {

        let display = undefined

        let method = "GET"
        let url = "https://icanhazdadjoke.com/"

        let httpRequest = new XMLHttpRequest()

        // on indique que la réponse sera en JSON : "response" sera alors AUTOMATIQUEMENT PARSÉ
        httpRequest.responseType = "json"

        httpRequest.onreadystatechange = function () {
            if (httpRequest.readyState === XMLHttpRequest.DONE) {
                if (httpRequest.status === 200) {
                    let result = httpRequest.response
                    display.innerHTML = result.joke // PLUS BESOIN DE PARSER
                } else {
                    alert('ERREUR avec la requête.');
                }
            }
        }

        document.addEventListener('DOMContentLoaded', function () {
            display = document.querySelector("#joke-02")

            // handler sur le click du bouton
            let button = document.getElementById("get-new-joke")
            button.addEventListener('click', function (){
                getNewJoke()
            })

            getNewJoke() // pour la 1ère blague lors du chargement
        })

        function getNewJoke() {
            httpRequest.open(method, url)
            httpRequest.setRequestHeader('Accept', 'application/json');
            httpRequest.send()
        }

    })()
</script>
Resultat

Paramètres

Comme évoqué dans la partie précédente, un serveur est souvent capable de fournir plusieurs services qui sont différenciés par l'extension du chemin indiqué dans l'URL utilisée pour lancer la requête.

Par exemple, la documentation de https://icanhazdadjoke.com/api fournit les URLs :

  • GET https://icanhazdadjoke.com/ : charger une blague aléatoire
  • GET https://icanhazdadjoke.com/search : rechercher une blague

Cependant, pour des requêtes complexes, le serveur peut avoir besoin d'informations complémentaires, comme par exemple la (ou les) chaine(s) à utiliser pour effectuer une recherche.

Ces informations devront alors être envoyées au serveur sous forme de paramètres de la requête.

Paramètres GET

Les paramètres d'une requête GET correspondent à des paires nom=valeur que l'on va ajouter à l'URL visée en utilisant le signe ?

Lorsqu'il y a plusieurs paramètres à envoyer, ceux-ci doivent être séparés par des &.

La syntaxe des paramètres pour une requête HTTP GET est :
?param=valeur[&param=valeur]

Chaque serveur (et chaque URL) utilise ses propres noms de paramètres. De fait, pour savoir comment créer une requête paramétrée pour une URL particulière, il est nécessaire de se référer à la documentation de l'API visée.

Par exemple, pour la recherche de blagues sur https://icanhazdadjoke.com il faut :

  • Utiliser l'URL https://icanhazdadjoke.com/search.
  • Renseigner un paramètre "term" contenant le(s) terme(s) à rechercher.
  • renseigner un paramètre "limit" contenant le nombre maximal de blagues renvoyées lors d'une requête (ce qui constitue une "page" de réponses).
  • Renseigner un paramètre "page" contenant le numéro de la page de blagues.
    (Lorsque le nombre de blagues sur le serveur correspondant à la requête dépasse la limit, le serveur découpe sa réponses en pages, et on peut alors indiquer celle qu'on veut récupérer).

On peut imaginer une recherche particulière contenant par exemple le terme "dog" :
https://icanhazdadjoke.com/search?term=dog

La suite d'instructions pour lancer cette requête est alors :

// url pour la recherche
let url = "https://icanhazdadjoke.com/search"

// construction des paramètres
let params = "?term=dog&limit=5&page=1"
let request = url + params

// exécution de la requête
httpRequest.open(method, request) // ATTENTION : on a remplacé 'url' par 'request' 
httpRequest.setRequestHeader('Accept', 'application/json');
httpRequest.send()

encodeURIComponent(...) : gérer les caractères interdits

Les paramètres font partie de l'URL : il y a donc certains caractères à éviter.

Par exemple, si l'on veut faire une recherche sur les blagues contenant plusieurs termes comme "dog" ET "bike, il faut lister ces termes dans le paramètre term en les séparant par des espaces... or les espaces sont interdits dans une URL !

La méthode encodeURIComponent(...) permet de résoudre ce problème en encodant les caractères interdits. (Par exemple les espaces seront transformés en : "%20").

Ainsi le code de construction de la chaine de paramètres peut être complété comme suit :

// construction des paramètres
let search = "dog bike"
search = encodeURIComponent(search)
let params = "?term=" + search + "&limit=5&page=1"
let request = url + params  

Exemple d'une requête GET paramétrée

Il nous est maintenant possible de lancer une requête complexe avec des paramètres.
(On recherchera ici les blagues contenant les termes "dog" et "bike").

La documentation nous indique que la réponse du serveur à une telle requête est quelque peu différente de celle que nous obtenions précédemment : le serveur ne renvoie plus simplement une "joke", mais une "page" qui en contient plusieurs (ce nombre étant fixé par le paramètre limit).

Il nous faut donc adapter le code de traitement du résultat : dans l'exemple ci-dessous,
nous n'affichons que la 1ère blague de la 1ère page de réponse.

<div id="joke-05" class="joke"></div>

<script>
    (function() {

        let display = undefined

        /**
         * Recherche de blagues.
         */
        function searchJoke() {
            let method = "GET"
            let url = "https://icanhazdadjoke.com/search" // url pour la recherche

            // construction des paramètres
            let search = "dog bike"
            search = encodeURIComponent(search)
            let params = "?term=" + search + "&limit=5&page=1"
            let request = url + params

            // exécution de la requête
            httpRequest.open(method, request) // ATTENTION : on a remplacé 'url' par 'request'
            httpRequest.setRequestHeader('Accept', 'application/json');
            httpRequest.send()
        }

        let httpRequest = new XMLHttpRequest()
        httpRequest.responseType = "json"
        httpRequest.onreadystatechange = function () {
            if (httpRequest.readyState === XMLHttpRequest.DONE) {
                if (httpRequest.status === 200) {
                    let results = httpRequest.response.results // la listes des blagues reçues
                    if(results.length > 0){
                        display.innerHTML = results[0].joke // affichage de la 1ère blague
                    }else{
                        display.innerHTML = ""
                    }

                } else {
                    alert('ERREUR avec la requête.');
                }
            }
        }

        document.addEventListener('DOMContentLoaded', function () {
            display = document.querySelector("#joke-05")
            searchJoke()
        })

    })()
</script>
Resultat

Exercice 01

Modifiez le code de l'exemple ci-avant pour ajouter un formulaire qui permet de lancer une recherche de blague avec les mots clés indiqués dans un champ de recherche.

Résultat attendu.

NB: la documentation indique que la recherche sur une chaine vide renvoie toutes les blagues du serveur, ce qui explique que la recherche sans mot clé donne une blague quand même.

TP 03

A partir des exemples précédents et de la documentation de l'API icanhazdadjoke, réalisez la page ci-dessous en prêtant bien attention aux détails aussi bien visuels que fonctionnels.

Éléments à prendre en compte :

  • Au départ, la zone de résultats est vide.
  • L'application fournit (au maximum) 5 blagues par page, et permet de lister toutes les blagues correspondant à une recherche grâce aux boutons "prev" et "next".
  • Le numéro de la page courante et le nombre de pages total sont affichés.
  • Les boutons "prev" et "next" sont désactivés quand ils ne peuvent (doivent) pas être utilisés.
  • La liste des mots clés correspondant à la page affichée est rappelée entre guillemets sous la liste des blagues.
  • Une recherche avec mot(s) clé(s) qui n'existe(nt) pas affiche le message "Pas de résultat !".
Résultat attendu.

Paramètres POST

POST vs GET

À la différence des paramètres GET qui, comme nous l'avons vu précédemment, sont envoyés au serveur en étant directement écrits dans l'URL d'une requête, la méthode POST encapsule les paramètres dans la requête HTTP.

De plus, l'envoi de données par la méthode GET est limité par les contraintes de la syntaxe des URL : elles ne peuvent contenir que des caractères ASCII, pas d'informations binaires (comme des fichiers images, etc.), et la taille d'une URL ne peut excéder 2048 caractères.

La méthode POST est donc utilisée lorsque le serveur requiert des données sensibles (ex. informations sur l'utilisateur, mot de passe, ...), ou encore lors de l'envoi de fichiers.

Pour envoyer des données par POST on peut construire une chaine ressemblant à celle du GET et l'envoyer en paramètre du send, en n'oubliant pas de prévenir le serveur de la forme des paramètres envoyés grâce à un setRequestHeader.

let method = 'POST'
let url = "..." // une url acceptant des requêtes POST
httpRequest.open(method, url)

httpRequest.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
let params = 'param1=valeur1&param2=valeur2' // etc.
httpRequest.send(params)

Cependant, le plus simple est d'utiliser une objet FormData !

La classe FormData

Les instances de la classe FormData facilitent la gestion de paires clé:valeur qui peuvent être envoyées simplement grâce à XMLHttpRequest.

La syntaxe pour un envoi de ce type est la suivante :

let method = 'POST'
let url = "..." // une url acceptant des requêtes POST
httpRequest.open(method, url)

let data = new FormData()
data.append('param1', 'valeur1')
data.append('param2', 'valeur2')
// etc.
httpRequest.send(data)

Exemple d'une requête POST paramétrée

Dans cet exemple, nous allons utiliser une (très très simple pour la démo) API REST qui accepte des requêtes POST. Cette API attend 2 paramètres nommés firstname et lastname.
Le script serveur renvoie alors une chaine JSON contenant un id qui a été généré à partir des données reçues. (NB: cet id ne sert à rien, c'est juste pour l'exemple).

Notre exemple utilise XMLHttpRequest et FormData pour envoyer une requête POST avec des données dès le chargement de la page, puis insère l'id reçu dans le DOM de la page pour affichage.

<style>
    #ajax-post-01{
        color: blue;
        font-weight: bold;
    }
</style>

ID reçu du serveur : <span id="ajax-post-01"></span>

<script>
    document.addEventListener('DOMContentLoaded', function (){

        let display = document.querySelector("#ajax-post-01")

        let httpRequest = new XMLHttpRequest()
        httpRequest.onreadystatechange = function (){
            if (httpRequest.readyState === XMLHttpRequest.DONE) {
                if (httpRequest.status === 200) {
                    let response = JSON.parse(httpRequest.response)
                    display.innerHTML = response.id
                } else {
                    alert('ERREUR avec la requête.');
                }
            }
        }

        let url = "http://web.gregory-bourguin.fr/teaching/php/requests/ajax/02_generateUserID.php"
        httpRequest.open('POST', url)

        let data = new FormData()
        data.append('firstname', 'Grégory')
        data.append('lastname', 'Bourguin')
        httpRequest.send(data)
        
    })
</script>
Resultat
ID reçu du serveur :

Formulaires et FormData

L'utilisation "classique" des formulaires HTML a pour effet le chargement d'une nouvelle page correspondant à la réponse du serveur.

Cependant, une application web "moderne" aura plutôt tendance à récupérer le résultat du serveur pour modifier le DOM de la page.

AJAX est bien entendu tout indiqué pour réaliser ces fonctionnalités puisqu'il s'agit d'utiliser Javascript pour envoyer les données au serveur, récupérer le résultat, et modifier le DOM sans changer de page.

Nous avons vu précédemment que la classe FormData facilite l'agrégation de données pour l'envoi dans des requêtes POST.

FormData permet aussi d'agréger aisément les données correspondant aux inputs d'un formulaire.

<style>
    #ajax-post-02{
        color: blue;
        font-weight: bold;
    }
</style>

<form name="user-infos" id="user-infos" style="width: 300px"
      action="http://web.gregory-bourguin.fr/teaching/php/requests/ajax/02_generateUserID.php"
      method="POST">

    <div class="form-group">
        <label for="firstname">Prénom</label>
        <input type="text" class="form-control" name="firstname" id="firstname" value="Gregory">
    </div>

    <div class="form-group">
        <label for="lastname">Nom</label>
        <input type="text" class="form-control" name="lastname" id="lastname" value="Bourguin">
    </div>

    <button type="submit" class="btn btn-primary">Envoyer</button>

</form>

<hr>
<p>
    ID reçu du serveur : <span id="ajax-post-02" style="width: 50%"></span>
</p>

<script>

    let formulaire = undefined
    let display = undefined

    let httpRequest = new XMLHttpRequest()
    httpRequest.onreadystatechange = function (){
        if (httpRequest.readyState === XMLHttpRequest.DONE) {
            if (httpRequest.status === 200) {
                let response = JSON.parse(httpRequest.response)
                display.innerHTML = response.id
            } else {
                alert('ERREUR avec la requête.');
            }
        }
    }

    document.addEventListener('DOMContentLoaded', function (){

        formulaire = document.querySelector("#user-infos")
        display = document.querySelector("#ajax-post-02")

        formulaire.addEventListener('submit', function (event){

            event.preventDefault() // bloquer le comportement par défaut du submit

            // s'ils existent, on peut récupérer la méthode et l'action (url) sur les attributs du form
            let method = formulaire.getAttribute("method")
            let url = formulaire.getAttribute("action")
            httpRequest.open(method, url)
            
            // constructeur avec le formulaire en paramètre
            let data = new FormData(formulaire)
            // il faut que les noms des champs du formulaire correspondent à ce qu'attend le serveur !

            httpRequest.send(data)

        })
    })
</script>
Resultat

ID reçu du serveur :

FETCH

L'API fetch a pour vocation de remplacer l'objet XMLHttpRequest. fetch permet en effet de réaliser les mêmes fonctionnalités que XMLHttpRequest mais en mettant en oeuvre du Javascript plus "moderne" et fondé sur le mécanisme de promesses (Promise).

Template fecth

Il est possible d'écrire les requêtes fecth avec plusieurs syntaxes différentes mais on peut considérer que les étapes principales sont les suivantes :

let url = "..." // l'url du service
let options = {
    method: '...', // GET, POST, PUT, DELETE, ...
    headers: { ... } // à remplir au besoin
}
fetch(url, options).then(response => { // ceci déclenche l'appel...
        if (response.ok) {
            response.text().then(data => { // remplacer text() par json() pour parser directement
                // ici le traitement quand tout s'est bien passé
            })
        } else {
            // erreur sur le serveur
        }
    }).catch(error => {
        // pb avec l'appel (ex. l'url est fausse)
    })

Fetch & GET

La mise en place d'une requête GET paramétrée avec fetch est similaire à la technique mise en oeuvre avec XMLHttpRequest : les paramètres sont encodés puis concaténés à l'url du service GET visé.

Pour illustrer, nous allons transformer l'exemple que nous avions pris précédemment.

<div id="fetch-joke-05" class="joke"></div>

<script>
    (function() {

        let display = undefined

        /**
         * Recherche de blagues.
         */
        function searchJoke() {
            let url = "https://icanhazdadjoke.com/search" // url pour la recherche
            let options = {
                method: 'GET',
                headers: { Accept: 'application/json' }
            }

            // construction des paramètres
            let search = "dog bike"
            search = encodeURIComponent(search)
            let params = "?term=" + search + "&limit=5&page=1"
            let request = url + params

            // exécution de la requête
            fetch(request, options).then(response => {
                if (response.ok) {
                    response.json().then(data => { // json() parse les données
                        display.innerHTML = data.results[0].joke
                    })
                } else {
                    alert("ERREUR avec la requête.", response.statusText);
                }
            }).catch(error => {
                console.log("ERREUR avec le fetch.", error)
            })
        }

        document.addEventListener('DOMContentLoaded', function () {
            display = document.querySelector("#fetch-joke-05")
            searchJoke()
        })

    })()
</script>
Resultat

Fetch & POST

fetch permet aussi d'exécuter des requêtes POST : les données sont ajoutées dans le corps (body) des options de l'appel.

Pour illustrer, nous transformons l'exemple POST précédent.

<style>
    #fetch-post-01{
        color: blue;
        font-weight: bold;
    }
</style>

ID reçu du serveur : <span id="fetch-post-01" style="width: 50%"></span>

<script>
    document.addEventListener('DOMContentLoaded', function (){

        let display = document.querySelector("#fetch-post-01")

        let url = "http://web.gregory-bourguin.fr/teaching/php/requests/ajax/02_generateUserID.php"

        // préparation des données à envoyer
        let data = new FormData()
        data.append('firstname', 'Grégory')
        data.append('lastname', 'Bourguin')

        let options = {
            method: 'POST',
            body: data // ajout des données pour le POST
        }

        fetch(url, options).then(response => {
            if (response.ok) {
                response.json().then(data => { // json() parse les données
                    display.innerHTML = data.id
                })
            } else {
                alert("ERREUR avec la requête.", response.statusText);
            }
        }).catch(error => {
            console.log("ERREUR avec le fetch.", error)
        })

    })
</script>
Resultat
ID reçu du serveur :

TP 04

Pour vous exercer à mettre en oeuvre la "nouvelle" API fetch, recréez les exercices précédents, ainsi que le TP03, en supprimant toute utilisation de XMLHttpRequest.

Exercices/Fichiers à (re)créer :

  • tp04_ex01.html : correspond à l'Exercice 01.
  • tp04_jokes.html : correspond au TP 03.
  • tp04_form.html : correspond à l'Exemple POST avec formulaire.