CentraleSupélecDépartement informatique
Gâteau du Glouton
3 rue Joliot-Curie
F-91192 Gif-sur-Yvette cedex
AJAX: pages encore plus dynamiques

Le modèle classique du Web

Dans le modèle standard, chaque requète HTTP génère le chargement d'une nouvelle page: le navigateur n'est rien d'autre qu'un "afficheur" de pages, et tout le travail revient au serveur.

Avantages:

  • simplicité du client
  • le serveur fait tout le travail de génération des pages : modèle assez sûr (sécurité)

Inconvénients:

  • transmission fréquente de pages entières, donc consommation de bande passante
  • attente du chargement des pages parfois long, à un moment la fenêtre affiche une page blanche...
  • ergonomie dégradée par rapport à une application native

Le modèle AJAX

AJAX veut dire "Asynchronous Javascript and XML". Dans ce modèle, le code javascript d'une page peut effectuer des requètes HTTP, sans change de page. On travaille alors en mode asynchrone: le code javascript tourne en tâche de fond, et les requètes HTTP faite en javascript ne sont pas blocantes (on leur associe une fonction de callback).

Dans ce modèle, plutôt que quelque chose de statique une page est vraiment un "programme" javascript qui va faire des appels sur le web de façon autonome.

Avantages:

  • La page n'échange que les données qui ont besoin de changer: un e-mail, une image, etc.On a donc une bande passante et temps de transfert réduits
  • La page peut rester opérationnelle pendant l'attente d'une réponse à une requête

Inconvénients:

  • Nécessité d'avoir un moteur JavaScript performant dans le navigateur → course actuelle aux optimisations
  • Sécurité: attention à ne pas faire confiance au client !
  • Contenu pas indexable par les moteurs de recherche
  • Bouton « retour » non fonctionnel par défaut

Exemples

Les services google sont de façon générale des applications AJAX. Par exemple:

  • GMail ne charge que les messages, pas la page entière
  • Google Maps ne charge que des portions carrées des cartes/images satellite

(«tuiles»). Même lorsque des dalles sont en cours de chargement on peut continuer à déplacer/zoomer sur la carte

  • Le moteur de recherche affiche des suggestions au cours de la frappe. L'obtention se fait en arrière-plan, ce qui n'empêche pas de continuer à taper (fonction de callback!)

Mais essentiellement tous les services modernes du web le sont aussi (au moins en partie):

  • Flickr: les images sont chargeés dynamiquement
  • Twitter: le flux des tweets est rafraîchi à la volée
  • Tous les jeux tels agar.io sont de l'AJAX pur
  • ...

Et en particulier, la grosse majorité propose des interface pour faire des requêtes directement:

Notez que pour utiliser ces services, il vous sera en général demandé de vous créer un ID sur le site. C'est gratuit, si vous utilisez le service dans des limites raisonnables.

Requêtes HTPP en AJAX

L'objet essentiel qui permet de faire des requêtes HTTP en javascript est XMLHttpRequest. On doit faire un certain nombre de choix:

  • mode synchrone ou asynchrone
  • type de requête: POST, GET, PUT
  • format d'échange: XML ou JSON

Mode synchrone

Tant que la requête n'a pas abouti, la fenêtre reste bloquée et l'utilisateur ne peut pas interagir avec elle.

Mode asynchrone

Dans ce mode, la requête est "instantanée", et l'exécution du code javasript peut continuer sans interruption. L'interaction de l'utilisateur avec la page n'est pas impactée.

L'idée est d'utiliser une fonction de callback qui sera utilisée pour traiter la requête lorsqu'elle aura abouti.

Des précautions sont à prendre dans ce mode:

  • rien ne garantit que les résultats de la précédente requête sont arrivés
  • l'ordre des réponses peut même être inversé

D'où la nécessité de prendre en compte ces aléas: ils ne doivent pas perturber le fonctionnement de l'application

Par exemple, si on tape « a » puis « b » dans le champ de recherche Google, si le résultat de la recherche pour « a » arrive après celui pour « ab », il ne faut pas afficher les résultats pour « a » !

GET et POST

Il s'agit de deux modes principaux pour passer des paramètres à une requête sur le web.

GET

  • Les paramètres éventuels sont dans l'URL: "url?para1=val1&para2=val2&..."
  • L'URL peut être mémorisée dans les favoris/marque-pages, elle peut être indexée par les moteurs de recherche
  • ne doit pas provoquer de mise à jour sur le serveur, uniquement l'obtention de données

POST

  • En plus de l'URL on peut envoyer des données au serveur (XML, JSON...)
  • convient bien à la modification de données

AJAX avec jQuery

Dû à des implémentations différentes suivant les navigateurs, il n'est pas recommandé d'utiliser directement l'objet XMLHttpRequest. À la place, on utilise... La librairie jQuery bien sûr. Celle-ci masque les incompatibilités entre navigateurs.

Une requête AJAX se fait comme suit:

$.ajax({
  url: "http://example.com/myService",
  data: {para1 : "val1", para2 : "val2", ...},
  dataType: "xml",
  type: "GET",
  success: processSuccess,
  error: processError,
});

function processSuccess(data) {
  ...
}

function processError(jqXHR, textStatus, errorThrown) {
  console.log(errorThrown + " : " + textStatus);
}

On donne deux fonctions de callback: une pour traiter les données dans elles sont arrivées, et une pour traiter les erreurs éventuelles. Cette fonction attends en argument l'objet httpRequest, et deux chaines de caractères qui contiennent explicitent l'erreur.

On retrouve le type de l'appel qui peut être GET ou POST, et les paramètres de requête peuvent être donnés dans l'attribut data

Le dataType décrit le type de réponse attendu, qui peut être:

  • xml : l'appel rend alors un objet DOM correspondant
  • json : l'appel renvoie un object javascript correspondant
  • jsonp : comme "json", mais encapsulé dans une balises <script> créée à la volée (voir plus bas)
  • text : l'appel renvoie une chaine de caractère.

Racourcis

.get()

Un raccourci est la fonction .get() que vous avez déjà rencontrée. Cette méthode effectue une requête HTTP GET de façon asynchrone.

.load()

Cette fonction charge un fichier (HTML) et l'insère dans l'arbre DOM. Voir la doc pour plus d'information. Dans sa version simple, elle s'utilise comme suit:

$("#result").load("monfichier.html");

Le contenu de monfichier.html va être placer à l'intérieur du noeud d'id #result.

Requêtes inter-domaine

Par défaut, les requêtes inter-domaines sont interdites: ''Same-Origin Policy''. Donc on ne peut pas faire en javascript une HttpRequest à un site arbitraire. La raison est simple: comme on peut incorporer à une page des fichiers javascripts de provenance quelconque, ces fichiers pourraient contenir des scripts malicieux, qui par exemple récupéreraient des identifiants et mots de passe.

Contourner la restriction

La restriction ne s'applique que lorsque la page est hébergée sur un serveur externe. En principe, il n'y a pas de restriction lorsque vous travaillez en local (c'est à dire si le fichier HTML se trouve sur votre ordinateur).

Mais si la page est hébergée en ligne, on va avoir le problème. Les méthodes standards pour y répondre sont les suivantes.

Avec un proxy sur le serveur

Une solution simple pour contourner la restriction est de demamder au serveur de faire la requête... Si cela résoud le problème (puisque le serveur peut faire ce qu'il veut), cela complexifie un peu le travail du serveur et rends l'appel à un service externe un peu moins fluide.

CORS

La technique CORS (Cross Origin Resource Sharing) est un entête HTTP particulier: Access-Control-Allow-Origin qui permet d'indiquer quels sont les sites externes autorisés pour les appels AJAX.

Encore une fois, il s'agit d'une configuration au niveau du serveur. Si c'est beaucoup moins lourd pour le serveur qu'un proxy, cela nécessite une configuration spéciale et requière d'avoir un navigateur qui comprend l'entête CORS (de fait, à l'heure actuelle tous les navigateurs le comprenne).

Le hack JSONP

Les deux techniques précédentes nécessitent de pouvoir configurer le serveur. Une solution alternative, purement AJAX, est d'utiliser la technique JSONP, pour JSON with Padding.

L'idée est la suivante: si javascript ne peut pas faire une requête à un serveur qui ne soit pas celui à l'origine de la page, le navigateur peut bien sûr charger des resources issues de sites quelconques: images, mais aussi fichiers CSS ou javascript.

En résumé, il y a deux façons d'obtenir une resource javascript sur internet à partir d'une page web:

  • Par une balise <script type="text/javascript" src="...">. La source peut être quelconque. Donc par exemple, on peut aller chercher une resource sur un serveur de google, de twitter, ...
  • Par un appel AJAX avec un objet HttpRequest, depuis javascript. Ceci n'est en général autorisé que pour faire un appel au domaine à laquelle la page appartient. Donc on est en général coincé: on ne peut pas faire appel à google par là.

L'astuce JSONP consiste à remarquer que l'on peut ajouter des noeuds quelconques à l'arbre DOM de la page... Donc en particulier rien ne nous interdit d'ajouter une balise <script> à la volée, avec donc une adresse potentiellement quelconque. C'est là que réside l'astuce: il suffit que le service web que l'on interroge puisse renvoyer la réponse attendue dans un fichier javascript...

Donc une application web peut renvoyer:

Par exemple, essayez les appels suivants à l'application openweathermap, qui fournit des données météorologiques:

Dans la dernière version, la réponse du serveur est un fichier javascript très simple qui contient un appel à la fonction demandée (vous pouvez choisir le nom que vous voulez), avec comme argument le contenu de la requête:

maFonctionQuiTraiteLaReponse({"coord":{"lon":-0.13,"lat":51.51},"weather": ...);

En bref

Une requête à une application web peut renvoyer

  • du XML
  • du JSON
  • ... mais aussi un petit fichier javascript qui appelle une fonction locale.

De façon générale, l'appel JSONP est le plus "portable", même si la majeure partie des services web qui proposent ce type d'accès à leur base de donnée le font avec les bons entêtes CORS.

Un exemple AJAX complet

Dans cet exemple, nous allons mettre en oeuvre des appels à l'application openweathermap vue plus haut afin d'obtenir la météo à Londres. Nous allons faire un appel avec le mode JSON, avec le mode XML, puis avec le mode JSONP.

JSON

La chaine de caractère rendue par le serveur à la requête

http://api.openweathermap.org/data/2.5/weather?q=London,uk&appid=22e21ef649526ef2b1be4db6d2b0857d&mode=json

ressemble à

{"coord": {"lon": -0.13,"lat": 51.51},"weather": [{"id": 800,"main": "Clear","description": "Sky is Clear","icon": "01d"}],"base": "cmc stations","main": {"temp": 277.211,"pressure": 1028.87,"humidity": 84,"temp_min": 277.211,"temp_max": 277.211,"sea_level": 1039.14,"grnd_level": 1028.87},"wind": {"speed": 7.08,"deg": 353.501},"clouds": {"all": 0},"dt": 1455538262,"sys": {"message": 0.0077,"country": "GB","sunrise": 1455520476,"sunset": 1455556528},"id": 2643743,"name": "London","cod": 200}

Donc la description du temps se trouve dans objectJsonEnQuestion.weather[0].description (voir la doc pour plus de détail).

Un exemple simple d'utilisation avec cet appel en mode JSON pourrait être le suivant:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Test AJAX</title>
    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js"></script>
    <script>
      function uneFonction(ville) {
        alert(ville.weather[0].description);
      }

      function traiteErreur(jqXHR, textStatus, errorThrown) {
        alert("Erreur " + errorThrown + " : " + textStatus);
      }

      $.ajax({
          url : "http://api.openweathermap.org/data/2.5/weather",
          data : { q : "London,uk", appid : "22e21ef649526ef2b1be4db6d2b0857d", mode : "json" },
          dataType : "json",
          success : uneFonction,
          error : traiteErreur
      });
    </script>
  </head>
  <body>
    <h1>Test AJAX</h1>
  </body>
</html>

XML

La chaine de caractère rendue par le serveur à la requête

http://api.openweathermap.org/data/2.5/weather?q=London,uk&appid=22e21ef649526ef2b1be4db6d2b0857d&mode=xml

ressemble à

<current>
  <city id="2643743" name="London">
    <coord lon="-0.13" lat="51.51"/>
    <country>GB</country>
    <sun rise="2016-02-15T07:14:35" set="2016-02-15T17:15:29"/>
  </city>
  <temperature value="277.211" min="277.211" max="277.211" unit="kelvin"/>
  <humidity value="84" unit="%"/>
  <pressure value="1028.87" unit="hPa"/>
  <wind>
    <speed value="7.08" name="Moderate breeze"/>
    <gusts/>
    <direction value="353.501" code="" name=""/>
  </wind>
  <clouds value="0" name="clear sky"/>
  <visibility/>
  <precipitation mode="no"/>
  <weather number="800" value="Sky is Clear" icon="01d"/>
  <lastupdate value="2016-02-15T12:19:55"/>
</current>

Notez d'abord combien c'est plus lisible (bien que plus verbeux). En tous les cas, la description du temps s'accède avec jQuery à

$(laVariableDOM).find("weather").attr("value")

Donc la même chose qu'au dessus mais en utilisant le mode XML donnerait:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Test AJAX</title>
    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js"></script>
    <script>
      function uneFonction(ville) {
        alert($(ville).find("weather").attr("value"));
      }

      function traiteErreur(jqXHR, textStatus, errorThrown) {
        alert("Erreur " + errorThrown + " : " + textStatus);
      }

      $.ajax({
          url : "http://api.openweathermap.org/data/2.5/weather",
          data : { q : "London,uk", appid : "22e21ef649526ef2b1be4db6d2b0857d", mode : "xml" },
          dataType : "xml",
          success : uneFonction,
          error : traiteErreur
      });
    </script>
  </head>
  <body>
    <h1>Test AJAX</h1>
  </body>
</html>

JSONP

Comme expliqué plus haut, JSONP permet de demander la réponse encapsulé dans un appel de fonction au serveur. Donc la chaine de caractère rendue par le serveur à la requête

http://api.openweathermap.org/data/2.5/weather?q=London,uk&appid=22e21ef649526ef2b1be4db6d2b0857d&callback=uneFonction

ressemble à

uneFonction({"coord": {"lon": -0.13,"lat": 51.51},"weather": [{"id": 800,"main": "Clear","description": "Sky is Clear","icon": "01d"}],"base": "cmc stations","main": {"temp": 277.211,"pressure": 1028.87,"humidity": 84,"temp_min": 277.211,"temp_max": 277.211,"sea_level": 1039.14,"grnd_level": 1028.87},"wind": {"speed": 7.08,"deg": 353.501},"clouds": {"all": 0},"dt": 1455538262,"sys": {"message": 0.0077,"country": "GB","sunrise": 1455520476,"sunset": 1455556528},"id": 2643743,"name": "London","cod": 200})

Il suffit donc en théorie pour utiliser le mode de faire cette requête comme on ferait un appel à un fichier javascript: son contenu va simplement être exécuté. La seul contrainte est donc de demander l'appel d'une fonction déjà définie...

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Test AJAX</title>
    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js"></script>
    <script>
      function uneFonction(ville) { alert(ville.weather[0].description); }
    </script>
    <script type="text/javascript" src="http://api.openweathermap.org/data/2.5/weather?q=London,uk&appid=22e21ef649526ef2b1be4db6d2b0857d&callback=uneFonction"></script>
  </head>
  <body>
    <h1>Test AJAX</h1>
  </body>
</html>

On définit une fonction uneFonction qui va chercher une information dans l'objet généré par le serveur (allez voir la doc). Puis on charge un fichier javascript, qui n'est autre que l'appel de cette fonction avec l'information donnée par le serveur:

uneFonction({"coord":{"lon":-0.13,"lat":51.51},"weather": ...);

Bien sûr, à ce niveau nous ne sommes pas encore AJAX... Mais maintenant, plutôt que d'écrire cette balise script en dûr dans le fichier html, on peut le faire à la volée. Soit à la main:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Test AJAX</title>
    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js"></script>
    <script>
      function uneFonction(ville) { alert(ville.weather[0].description); }

      function makeBalise() {
         var b = $("<script>",
                   { type : "text/javascript",
                     src  : "http://api.openweathermap.org/data/2.5/weather?q=London,uk&appid=22e21ef649526ef2b1be4db6d2b0857d&callback=uneFonction" });
        $("head").append(b); 
      }

      $(document).ready(makeBalise);
    </script>
  </head>
  <body>
    <h1>Test AJAX</h1>
  </body>
</html>

ou, de façon équivalente, avec un appel JSONP à .ajax():

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Test AJAX</title>
        <script type="text/javascript" src="https://code.jquery.com/jquery-1.12.0.min.js"></script>
    <script>
      function uneFonction(ville) {
         alert(ville.weather[0].description);
      }

      function traiteErreur(jqXHR, textStatus, errorThrown) {
        alert("Erreur " + errorThrown + " : " + textStatus);
      }

      function appelWeather() {
        $.ajax({ url: "http://api.openweathermap.org/data/2.5/weather",
                 data : { q : "London,uk", appid : "22e21ef649526ef2b1be4db6d2b0857d" },
                 dataType: "jsonp",
                 success: uneFonction,
                 error: traiteErreur });
      }

      $(document).ready(appelWeather);
    </script>
  </head>
  <body>
    <h1>Test AJAX</h1>
  </body>
</html>

Une mini-appli de météo temps-réel

On peut bien sûr faire un appel AJAX de façon interactive. Plutôt que d'appeler le serveur de façon péremptoire au début, on peut faire un petit formulaire pour interroger le serveur de façon interactive. Dans cet exemple: weather.html vous trouverez la méthode JSONP implémentée.

Dans le champ de texte, rentrez par exemple "Nice,fr", ou "Paris,fr", ou "Rome,it": en principe, le paragraphe en dessous devrait se mettre à jour en rapport avec la météo courante.