prototype.js : Gestionnaires d'événements pour Ajax.Request

23 février 2007Ajax, Javascript, prototype.js
 Cet article a été rédigé il y a plusieurs années et peut ne plus être tout à fait à jour…

Lorsque nous avons appris à réaliser un premier appel Ajax en utilisant le framework prototype.js, nous avons toujours utilisé l'événement onComplete pour brancher notre fonction exploitant les données renvoyées par le serveur en réponse à nos requêtes Ajax.

Pour conclure cet article, j'écrivais que prototype.js en proposait de nombreux autres, chacun déclenchés à différents moments du cycle de vie d'une requête Ajax.

Le moment est venu de découvrir quels sont ces événements, et comment les exploiter à notre avantage.

Rappel : Cycle de vie d'un objet XMLHttpRequest

Avec le framework prototype.js, les requêtes Ajax sont effectuées en utilisant une instance de la classe Ajax.Request.

Celle-ci s'appuie sur l'objet XMLHttpRequest - XHR pour faire plus court - fourni par le navigateur Web.

Au cours d'une requête Ajax, cet objet passe successivement par cinq état :

  • 0 : Uninitialized : Valeur initiale, lorsque l'objet n'a pas été initialisé. Entre sa création, et l'appel de la méthode open, donc.
  • 1 : Open : Une fois que l'objet a été initialisé via la méthode open, mais que la requête n'a pas encore été déclenchée par la méthode send.
  • 2 : Sent : Une fois la requête envoyée par la méthode send.
  • 3 : Receiving : Avant la réception du corps de la réponse. Les en-têtes HTTP de la réponse ont déjà été reçus, eux, par contre.
  • 4 : Loaded : Quand la réponse a été entièrement reçue : l'appel Ajax est terminé ; il est possible d'utiliser son résultat.

La valeur numérique indiquée pour chaque état correspond à celle de la propriété readyState de l'objet XHR. Le libellé correspond au nom généralement utilisé, et n'est présent qu'à titre d'indication et par soucis de lisibilité.

Prototype.js permet de brancher un gestionnaire d'événement sur chacun de ces états.


Page HTML, et programme serveur

Les requêtes Ajax que j'utiliserai pour mes exemples se basent sur la page HTML et le script PHP suivants :

Page HTML

Comme page HTML déclenchant la requête Ajax, nous utiliserons toujours le même modèle :

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
    <title>AJAX : Exemple de client</title>
    <script type="text/javascript" src="prototype-1.5.0.js"></script>
    <script type="text/javascript" src="test2.js"></script>
</head>
<body>
    <p>
        <a href="" onclick="gestionClic(); return false;">
            Cliquez ici !
        </a>
    </p>
    
    <div id="resultat">&nbsp;</div>
</body>

Un clic sur le lien déclenchera l'appel d'une fonction JavaScript, nommée gestionClic. C'est son implémentation qui nous intéressera pour la suite de cet article.

Le <div> d'id resultat nous servira pour l'affichage des résultats.

Script côté serveur

Et côté serveur, nous utilisons encore une fois le langage PHP pour développer le programme qui sera appelé par la requête Ajax :

<?php
    sleep(1);

    header('Content-Type: text/html; charset: UTF-8');
    
    $str = '[{"nom": "Lerdorf", "prenom": "Rasmus"}, {"nom": "Gutmans", "prenom": "Andi"}, {"nom": "Suraski", "prenom": "Zeev"}]';
    header('X-JSON: ' . $str);
    
    sleep(1);
    
    echo str_repeat('blah blah !', 5000);
    
    sleep(1);
?>

Quelques points sont à noter :

  • Ce programme renvoi un en-tête nommé X-JSON. Les données au format JSON qu'il contiendra seront automatiquement décodées par prototype.js. (Pour plus d'informations, je vous invite à lire l'article Utiliser JSON comme format d'échange de données, avec prototype.js)
  • Un contenu de grande taille est renvoyé en corps de la réponse : 5000 fois le texte "blah blah !". Selon la puissance/rapidité de votre machine/serveur/connexion, vous serez peut-être amenés à augmenter ou diminuer le nombre de répétitions.
  • Entre le début du script et l'envoi des premiers en-têtes, entre l'envoi des en-têtes et l'envoi du corps de la réponse, et avant la fin du programme, celui-ci attend une seconde - ce dans le but de nous laisser le temps de voir ce qu'il se passe du côté de la réception de la réponse à notre requête.


Principe du script côté client

Côté client, nous utilisons, en JavaScript, la classe Ajax.Request pour effectuer nos requêtes Ajax, et obtenir les valeurs retournées.

Le principe d'appel sera, pour tous nos exemples, de la forme du suivant :

function gestionClic()
{
  var url = './test2.php';
  var params = '';
  $('resultat').innerHTML = '';
  var myAjax = new Ajax.Request(
      url, 
      {
        method: 'post', 
        postBody: params, 
        
        // TODO : Liste de gestionnaires d'événements
        
      });
} // gestionClic()

La méthode que nous implémentons ici est celle qui sera appelée lors d'un clic sur le lien incorporé à la page HTML présentée plus haut.

Quelques points à noter :

  • Nos requêtes Ajax sont envoyées à la page test2.php.
  • Nous ne lui passons aucun paramètre.
  • Nos requêtes sont envoyées en POST.

Pour plus d'informations, n'hésitez pas à consulter mon article expliquant comment réaliser un premier appel Ajax avec prototype.js.

Pour la suite de cet article, nous travaillerons généralement sur les différents gestionnaires d'événements que prototype.js nous permet de définir.


onComplete : Fin de la requête Ajax

Le gestionnaire d'événements qu'on a le plus tendance à utiliser lorsque l'on commence à développer avec prototype.js est onComplete.

La fonction qui lui sera associée sera appelée en fin de requête Ajax, que celle-ci ait aboutie à un code HTTP indiquant le succès ou l'échec.
(Autrement dit, par exemple, onComplete sera déclenché que votre requête Ajax ait abouti à une erreur HTTP 404 "Not Found", ou à un code retour 200 "OK")

Le gestionnaire d'événement branché sur onComplete étant appelé quelque soit le code retour de la requête, c'est à vous de déterminer si celle-ci a réussi ou a échoué.

Par exemple, si nous utilisons le code suivant pour déclencher la requête Ajax :

function gestionClic()
{
    var url = './test2.php';
    var myAjax = new Ajax.Request(
        url, 
        {
          method: 'post',
          onComplete: gestionReponse
        });
} // gestionClic()

La fonction gestionReponse sera appelée une fois la requête terminée :

function gestionReponse(xhr)
{
    if (xhr.status == 200)
    {
        $('resultat').innerHTML = 'OK';
    }
    else
    {
        $('resultat').innerHTML = xhr.status 
                    + ' : ' + xhr.statusText;
    }
}

Le première paramètre passé par prototype.js aux gestionnaires d'événements étant l'objet XHR utilisé pour effectuer la requête, nous pouvons utiliser ses propriétés pour déterminer comment notre appel s'est déroulé.

Dans le cas où tout se passe bien, nous afficherons

OK

Et, dans les autres cas, nous afficherons le code statut HTTP renvoyé par le serveur, ainsi que le message correspondant.

Données JSON

Nous avons vu dans un autre article comment utiliser JSON comme format d'échange de données avec prototype.js.

Pour rappel, si le programme serveur appelé par la requête Ajax renvoi des données au format JSON dans un en-tête HTTP nommé X-JSON, alors celles-ci seront automatiquement evaluées par la bibliothèque, est passées comme second paramètre à notre fonction gestionnaire d'événement branchée sur onComplete.

Par exemple, avec le script PHP présenté plus haut, nous pourrions utiliser le gestionnaire d'événement suivant pour onComplete :

function gestionReponse(xhr, json)
{
    if (xhr.status == 200)
    {
        if (json)
        {
            var str = '';
            json.each(function (personne)
                {
                    str += personne.prenom + ' ' 
                           + personne.nom + ' ; ';
                });
            $('resultat').innerHTML = str;
        }
        else
        {
            $('resultat').innerHTML = 'NO JSON';
        }
    }
    else
    {
        $('resultat').innerHTML = xhr.status 
                    + ' : ' + xhr.statusText;
    }
}

Ce qui provoquera l'affichage des noms de quelques auteurs de PHP :

Rasmus Lerdorf ; Andi Gutmans ; Zeev Suraski ;


Note : onComplete est toujours appelée en dernier, y compris si vous avez défini d'autres gestionnaires d'événements, tels ceux que je présenterai par la suite.
Attention donc à ne pas effectuer vos traitements en double !


onSuccess / onFailure : Succès, ou Echec

Dans notre exemple présentant l'usage de l'événement onComplete, nous avons vu que, avec celui-ci, c'est à nous de déterminer si la requête Ajax s'est déroulée avec succès ou non.

Nous nous sommes basé sur le code statut HTTP renvoyé par le serveur, en considérant qu'un code égane à 200 signifiait que la requête avait réussi, tout autre code signifiant l'échec.

Mais la RFC HTTP/1.1 défini comme "Successful" tous les codes HTTP en 2xx.

Dans le cas où nous souhaitons nous aligner sur la définition de "succès" de la RFC HTTP, et/ou ne voulons pas tester nous-même le code statut renvoyée par le serveur, prototype.js nous permet de nous brancher sur deux événements autres que onComplete :

onSuccess

Si nous définissons un gestionnaire d'événement sur onSuccess, celui-ci sera appelé lorsque le statut HTTP renvoyé par le serveur sera de la forme 2xx (donc, entre autres, 200).

onFailure

Le gestionnaire d'événements branché sur onFailure, quant à lui, sera appelé dans tous les autres cas - donc, notamment, dans les cas où le serveur a renvoyé un code erreur tel 403 "Forbidden" ou 404 "Not Found".

Exemple : onSuccess et onFailure

Par exemple, nous pouvons utiliser l'implémentation suivante de la fonction gestionClic, qui défini des gestionnaires d'événements pour onSuccess et onFailure :

function gestionClic()
{
    var url = './test2.php';
    var myAjax = new Ajax.Request(
        url, 
        {
          method: 'post',
          onSuccess: function (xhr, json)
            {
              if (json)
              {
                var str = '';
                json.each(function (personne)
                    {
                      str += personne.prenom + ' ' 
                             + personne.nom + ' ; ';
                    });
                $('resultat').innerHTML = str;
              }
              else
              {
                $('resultat').innerHTML = 'NO JSON';
              }
            },
          onFailure: function (xhr)
            {
              $('resultat').innerHTML = xhr.status 
                         + ' : ' + xhr.statusText;
            },
          onComplete: function (xhr)
            {
              $('resultat').innerHTML += '<br />FIN';
            }
        });
} // gestionClic()

Dans le cas où la requête se déroule comme prévu, nous afficherons, comme précédemment, les noms de trois auteurs de PHP :

Rasmus Lerdorf ; Andi Gutmans ; Zeev Suraski ;

Et, dans le cas où un problème survient, nous afficherons le code HTTP renvoyé par le serveur, ainsi que le message correspondant.

Par exemple, si la page demandée est introuvable :

404 : Not Found


Enfin, cet exemple illustre le fait que la fonction définie pour onComplete et toujours appelée en dernier, même si des gestionnaires ont été définis pour onSuccess et/ou onComplete : dans un cas comme dans l'autre apparaît aussi le mot

FIN


onException : Si une exception est levée

Dans certaines conditions, votre requête Ajax peut lever une exception - si le problème qui se pose est plus grave qu'une simple erreur au sens d'erreur HTTP.

Pour gérer ces cas là, prototype.js permet de définir un gestionnaire sur l'événement onException :

function gestionClic()
{
    var url = 'http://www.google.fr';
    var myAjax = new Ajax.Request(
        url, 
        {
          method: 'post',
          onException: function (xhr, e)
            {
              $('resultat').innerHTML += 'Exception : ' + e;
            },
          onComplete: function (xhr)
            {
              $('resultat').innerHTML += '<br />FIN';
            }
        });
} // gestionClic()

Note : l'objet XHR n'autorise pas les requêtes vers un domaine autre que celui sur lequel se trouve la page appelante ; une tentative de requête "cross-domain", comme ici, provoque la levée d'une exception.

Ce script provoquera l'affichage suivant :

Exception : Permission refusée d'appeler la méthode XMLHttpRequest.open

Cet exemple illustre le fait que, lorsque l'exécution de la requête Ajax lêve une exception, celle-ci ne se termine pas ; le gestionnaire d'événement branché sur onComplete n'est donc pas appelé (Le message FIN n'est pas affiché).


Les autres événements

Prototype.js permet de définir quelques autres gestionnaires d'événements, sur des événements que nous utilisons moins souvent - de manière générale :

Pour suivre le cycle de vie de l'objet XHR, nous pouvons aussi brancher des gestionnaires sur les événements suivants :

  • onUninitialized : à la création de l'objet XHR
  • onLoading : après l'appel de la méthode open de l'objet XHR
  • onLoaded : une fois que la requête est envoyée
  • onInteractive : pendant que la réponse est en cours de réception

Note : selon la taille de la réponse Ajax, de votre connexion, de votre navigateur, ... l'événement onInteractive peut être déclenché plusieurs fois !


Il est aussi possible de définir des gestionnaires d'événements spécifiques à des codes statut HTTP, en définissant des gestionnaires d'événements sur des événements dont le nom est de la forme "onXXX", où XXX correspond au code HTTP qui vous intéresse.

Par exemple, pour définir un gestionnaire d'événement spécifique au cas d'une erreur 404 - "Not Found", nous le brancherons sur l'événement on404.

Attention : si un événement onXXX est levé, celui-ci prend la priorité sur les événements onSuccess (dans le cas où XXX est un code en 2xx) et onFailure (pour les autres codes statut, tels 4xx et 5xx).
Cela signifie que si vous branchez un gestionnaire sur on200 et un second sur onSuccess, le second ne sera pas appelé dans les cas où le serveur renvoit un code HTTP 200 - "OK" ; dans la plupart des cas de succès, donc.


Exemple plus poussé

Avant de terminer, j'aimerais présenter un exemple définissant des gestionnaires pour la plupart des types d'événements que nous avons vu au cours de cette article - ceci en particulier dans le but de voir dans quel ordre ils sont appelés les uns par rapport aux autres.

Pour commencer, définissons une petite fonction utilitaire :

function echoJSON(json)
{
    if (json)
    {
        var str = '';
        json.each(function (personne)
            {
                str += personne.prenom + ' ' + personne.nom + ' ; ';
            });
        return str + '<br />';
    }
    return 'NO JSON';
}

Cette fonction retourne la concaténation des noms et prénoms de chacune des personnes reçues en paramètre sous la forme de données JSON, ou "NO JSON" dans le cas où aucune donnée JSON n'a été reçue.

Code Source de l'exemple

Et maintenant, la version "lourde" de notre sempiternelle fonction gestionClic :

function gestionClic()
{
  var url = './test2.php';
  $('resultat').innerHTML = '';
  var myAjax = new Ajax.Request(
      url, 
      {
        method: 'post', 
        onUninitialized: function (xhr)
          { // Création de l'objet XHR
            $('resultat').innerHTML += 'Uninitialized<br /><br />';
          },
        onLoading: function (xhr)
          { // Après appel méthode open
            $('resultat').innerHTML += 'Loading<br /><br />';
          },
        onLoaded: function (xhr)
          { // Requête envoyée
            $('resultat').innerHTML += 'Loaded<br /><br />';
          },
        onInteractive: function (xhr, json)
          { // Réponse en cours de réception
            $('resultat').innerHTML += 'Interactive<br />'
                + echoJSON(json) + '<br />';
          },
        on200: function (xhr, json)
          { // Réponse HTTP "OK"
            $('resultat').innerHTML += 'HTTP 200 "OK"<br />'
                + echoJSON(json) + '<br />';
          },
        onSuccess: function (xhr, json)
          { // Réponse HTTP == 2xx
            $('resultat').innerHTML += 'Success<br />'
                + echoJSON(json) + '<br />';
          },
        on404: function (xhr)
          { // Réponse HTTP "OK"
            $('resultat').innerHTML += 'HTTP 404 "Not Found"<br /><br />';
          },
        onFailure: function (xhr)
          { // Réponse HTTP != 2xx
            $('resultat').innerHTML += 'Failure : ' 
                + xhr.status + ' :' + xhr.statusText + '<br /><br />';
          },
        onException: function (xhr, exception)
          {
            $('resultat').innerHTML += 'Exception : ' + exception + '<br />';
          },
        onComplete: function (xhr, json)
          { // Requête totalement terminée
            $('resultat').innerHTML += 'Complete<br />' 
                + echoJSON(json) + '<br />';
          }
        
      });
} // gestionClic()

Quelques points à noter en particulier :

  • Des gestionnaires spécifiques ont été définis pour les codes statut HTTP 200 (OK) et 404 (Not Found).
  • Aussi souvent que possible, nous tentons d'afficher des données JSON renvoyées via un éventuel en-tête HTTP X-JSON.
  • Nous avons défini des gestionnaires d'événements pour les cas génériques de succès (onSuccess) et d'échec (onFailure).
  • Et, enfin, un gestionnaire est branché sur l'événement onComplete, censé toujours être déclenché en toute fin de requête.


Résultats

Avant de commencer, notez que les résultats proposés ci-dessous ont été, sauf indication contraire, obtenus sous Firefox 2.0[1].

Succès

Dans le cas où le requête se passe bien, voici un extrait du résultat obtenu :

Loading

Loaded

Interactive
Rasmus Lerdorf ; Andi Gutmans ; Zeev Suraski ;

Interactive
Rasmus Lerdorf ; Andi Gutmans ; Zeev Suraski ;

[...]

Interactive
Rasmus Lerdorf ; Andi Gutmans ; Zeev Suraski ;

HTTP 200 "OK"
Rasmus Lerdorf ; Andi Gutmans ; Zeev Suraski ;

Complete
Rasmus Lerdorf ; Andi Gutmans ; Zeev Suraski ;

En premier lieu, l'événement onLoading est levé, suivi, une seconde plus tard, de l'événement onLoaded.
Autrement dit, onLoading est levé dès la requête Ajax lancée, et onLoaded seulement une fois que le serveur a commencé à renvoyer une réponse.

Suit immédiatement une série d'événements onInteractive : la réponse est de grande taille, Firefox lève un événement onInteractive à chaque fois qu'il a reçu un paquet de données.
Les en-tête HTTP ont été reçus avant les données du corps de la réponse. Les données JSON passées via l'en-tête X-JSON sont donc disponibles avant que le corps de la réponse ne le soit en intégralité.

Une fois la réponse terminée, puisque tout s'est bien passé (code statut HTTP 200), l'événement on200 est levé.

Un événement on2XX ayant été généré, onSuccess ne l'est pas...

On arrive à la fin du traitement : le gestionnaire branché sur onComplete est appelé.

Note : Quelques tests sous Internet Explorer 7 et Konqueror semblent indiquer que l'événement onInteractive n'est levé qu'une seule fois sous ces deux navigateurs - du moins, sur cet exemple.

Note 2 : Si la réponse du serveur est suffisamment rapide à venir, il arrive parfois, selon le navigateur, que les gestionnaires d'événements soient appelés dans un peu n'importe quel ordre... Si vous êtes curieux, essayez en supprimant les temporisations dans notre script PHP.


Echec avec un statut HTTP géré spécifiquement

Voici un exemple de résultat obtenu pour une requête ayant échoué sur une erreur 404 (Page non trouvée) :

Loading

Loaded

Interactive
NO JSON
HTTP 404 "Not Found"

Complete
NO JSON

Le principe général est un peu le même, mais quelques points sont à noter :

  • La page n'est pas trouvée ; aucune donnée JSON n'a donc été renvoyée.
  • La page n'étant pas trouvée, aucun contenu (ou seulement un message d'erreur explicatif) n'a été renvoyé par le serveur ; onInteractive est donc levé un nombre de fois beaucoup moins important que précédemment.
  • Nous avons défini un gestionnaire d'événement spécifique pour on404 ; c'est donc lui qui est appelé, et non celui branché sur onFailure.


Echec avec un statut HTTP non géré spécifiquement

De la même manière, avec une requête qui échoue sur une erreur 403 (Accès interdit) :

Loaded

Interactive
NO JSON
Failure : 403 :Forbidden

Complete
NO JSON
Loading

La différence entre cet exemple et le précédent est que nous n'avons pas défini de gestionnaire pour on403 ; c'est donc le gestionnaire défini pour onFailure qui est appelé.


Conclusion

Pour terminer :

Nous avons vu au cours de cet article que prototype.js permet de prendre en compte de nombreux événements... Mais, avec l'habitude, on se rend compte que les plus fréquemment utilisés en situation réelle sont onSuccess et onFailure : ce qui compte, en général, est de déterminer si un traitement a réussi (et d'exploiter son résultat, dans ce cas), ou échoué (et d'en informer l'utilisateur).



Note

[1] Et vous devez commencer à être habitué au fait que l'on obtient pas toujours le même comportement sous tous les navigateurs, lorsque l'on développe en JavaScript - ou lorsque l'on développe pour le Web, de manière plus générale...