Chapitre 14: Requêtes HTTP
Comme mentionné dans le chapitre 11, les communications sur le World Wide Web se passent via le protocole HTTP. Une simple requête pourrait ressembler à ça :
GET /files/fruit.txt HTTP/1.1 Host: eloquentjavascript.net User-Agent: Le Navigateur Imaginaire
Ce qui demande au serveur eloquentjavascript.net
le fichier
files/fruit.txt
. En plus, la requête spécifie que la version de HTTP utilisée
est 1.1 (la version 1.0 est encore utilisée et fonctionne légèrement
différemment). La ligne Host
et User-Agent
suivent un même modèle : elles
commencent par un mot qui identifie l’information qu’elle contient, suivi par
deux points et l’information elle-même. Ces lignes sont appelées « en-têtes
». L’en-tête User-Agent
dit au serveur quel navigateur (ou autre type de
programme) a lancé la requête. D’autres en-têtes sont souvent utilisés tout le
long, par exemple pour déclarer quels types de documents le client peut
comprendre, ou pour spécifier le langage qu’il préfère.
Après avoir reçu la requête ci-dessus, le serveur peut envoyer la réponse suivante :
HTTP/1.1 200 OK Last-Modified: Mon, 23 Jul 2007 08:41:56 GMT Content-Length: 24 Content-Type: text/plain pommes, oranges, bananes
La première ligne indique encore la version du protocole HTTP utilisée, suivie
par l’état de la requête. Dans ce cas, le code d’état est 200
, ce qui
signifie « OK, rien d’anormal ne s’est produit, je vous envoie les fichiers ».
Viennent ensuite quelques en-têtes indiquant (dans ce cas) la dernière fois que
le fichier a été modifié, sa longueur et son type (texte brut). Après
l’en-tête, vous obtenez une ligne blanche suivie par le fichier lui-même.
En plus des requêtes commençant par GET
, indiquant que le client veut
seulement récupérer le document, le mot Post peut aussi être utilisé pour
indiquer que des informations seront envoyées avec la requête dont on attend
que le serveur les traite d’une manière ou d’une autre.1
Lorsque vous cliquez sur un lien, soumettez un formulaire ou encouragez de quelque manière votre navigateur à aller sur une nouvelle page, il fera une requête HTTP et déchargera immédiatement l’ancienne page pour afficher le nouveau document. Dans les situations classiques, c’est exactement ce que vous voulez ― c’est la manière dont le Web fonctionne traditionnellement. Parfois cependant, un programme JavaScript veut communiquer avec le serveur sans avoir à recharger la page. Le bouton « Load » de la console, par exemple, peut charger des fichiers sans quitter la page.
Pour être capable de faire des choses comme celle-là, le programme JavaScript doit faire une requête HTTP lui-même. Les navigateurs actuels fournissent une interface pour faire cela. Comme pour ouvrir une fenêtre, cette interface est sujette à certaines restrictions. Pour empêcher les scripts de faire quoi que ce soit d’effrayant, il est uniquement permis de faire une requête HTTP sur le domaine d’où vient la page actuelle.
Un objet utilisé pour faire une requête HTTP peut, dans la
plupart des navigateurs, être créé en faisant new XMLHttpRequest()
. Les
versions plus anciennes d’Internet Explorer qui inventa originellement cette
technique nécessitent de faire new ActiveXObject("Msxml2.XMLHTTP")
, ou pour
des versions encore plus anciennes new ActiveXObject("Microsoft.XMLHTTP")
.
ActiveXObject
est l’interface d’Internet Explorer à différentes
spécificités de ce navigateur. Nous sommes déjà habitués à écrire des fonctions
pour prendre en charge les incompatibilités, alors faisons le encore une fois :
function makeHttpObject() { try {return new XMLHttpRequest();} catch (erreur) {} try {return new ActiveXObject("Msxml2.XMLHTTP");} catch (erreur) {} try {return new ActiveXObject("Microsoft.XMLHTTP");} catch (erreur) {} throw new Error("La création de l’objet pour les requêtes HTTP n’a pas pu avoir lieu."); } show(typeof(makeHttpObject()));
La fonction encapsulatrice essaie de créer l’objet des trois manières en
utilisant try
et catch
pour détecter celles qui échouent. Si aucune des
manières ne fonctionne, ce qui peut être le cas avec les plus vieux navigateurs
ou les navigateurs avec des paramètres de sécurité stricts, une erreur est
signalée.
Maintenant, pourquoi cet objet est-il appelé XML HTTP request ? C’est un nom un peu trompeur. XML est un moyen de stocker des données textuelles. Il utilise des balises et des attributs comme HTML, mais est plus structuré et flexible ― pour stocker vos propres sortes de données vous pouvez définir vos propres types de balises XML. Ces objets requêtes HTTP ont certaines fonctionnalités intégrées pour s’occuper de la récupération de documents XML, raison pour laquelle ils ont XML dans leur nom. Ils peuvent cependant gérer également d’autres types de documents, et d’après mon expérience sont utilisés aussi souvent pour des requêtes non-XML.
Maintenant que nous avons notre objet HTTP, nous pouvons l’utiliser pour fabriquer une requête.
var requete = makeHttpObject(); requete.open("GET", "files/fruit.txt", false); requete.send(null); print(requete.responseText);
La méthode open
est utilisée pour configurer la requête. Dans ce cas, nous
choisissons de fabriquer une requête GET
pour notre fichier fruit.txt
.
L’URL donnée ici est facultative, elle ne contient pas la partie http://
ou
le nom d’un serveur, ce qui signifie qu’elle va chercher le fichier sur le
serveur d’où vient le document courant. Le troisième paramètre, false
, sera
examiné dans un moment. Après que open
ait été appelé, la véritable requête
peut être faite avec la méthode send
. Lorsque la requête est une requête
POST
, les données à envoyer au serveur (comme une chaîne de caractères)
peuvent être passées par cette méthode. Pour les requêtes GET
, il y a juste à
passer null
.
Une fois que la requête a été faite, la propriété responseText
de l’objet
requête contient le contenu du document récupéré. Les en-têtes que le serveur a
renvoyés peuvent être inspectés avec les fonctions getResponseHeader
et
getAllResponseHeaders
. La première cherche un en-tête particulier tandis
que la seconde nous donne une chaîne de caractères contenant tous les en-têtes.
Ceux-ci peuvent être utiles en certaines occasions pour obtenir des
informations supplémentaires sur le document.
print(requete.getAllResponseHeaders()); show(requete.getResponseHeader("Last-Modified"));
Si pour une quelconque raison, vous voulez ajouter des en-têtes à la requête
qui est envoyée au serveur, vous pouvez utiliser la méthode
setRequestHeader
. Elle prend deux chaînes de caractères en arguments, le
nom et la valeur de l’en-tête.
Le code de réponse, qui était 200
dans l’exemple, peut être trouvé dans la
propriété status
. Si quelque chose se passe mal, ce code obscur l’indiquera
immédiatement. Par exemple, 404
signifie que le fichier que vous avez demandé
n’existe pas. Le statusText
contient une description légèrement moins
énigmatique de l’état.
show(requete.status); show(requete.statusText);
Lorsque vous voulez vérifier si une requête a fonctionné, comparer status
avec 200
est en général suffisant. En théorie, le serveur pourrait retourner
le code 304
dans certaines situations pour indiquer que l’ancienne version du
document stockée par le navigateur dans son « cache » est encore à jour.
Cependant, il semble que les navigateurs vous protègent de cela en définissant
status
à 200
même lorsqu’il vaut 304
. Il faut également savoir que si
vous faites une requête à travers un protocole non-HTTP2, comme FTP, status
ne sera pas utilisable parce que le protocole n’utilise pas les codes d’états
HTTP.
Lorsqu’une requête est faite comme dans l’exemple suivant, l’appel à la méthode
send
ne rend pas la main tant que la requête n’est pas terminée. C’est
pratique car cela signifie que responseText
est disponible après l’envoi de
send
et que l’on peut immédiatement l’utiliser. Il y a cependant un problème.
Lorsque le serveur est lent ou que le fichier est lourd, faire une requête peut
prendre un certain temps. Tant qu’elle est en train d’être faite, le programme
attend, ce qui fait que le navigateur tout entier attend. Jusqu’à ce que le
programme s’achève, l’utilisateur ne peut rien faire, même pas faire défiler la
page. Les pages qui tournent sur un réseau local rapide et fiable pourraient
s’en sortir en faisant des requêtes comme cela. Les pages sur l’immense et
imprévisible Internet ne peuvent pas, pour ce qui les concerne, en faire
autant.
Lorsque le troisième argument de open
est true
, la requête est définie pour
être « asynchrone ». Cela signifie que send
rendra la main immédiatement
pendant que la requête se fera en arrière-plan.
requete.open("GET", "files/fruit.xml", true); requete.send(null); show(requete.responseText);
Mais attendez un moment, et…
print(requete.responseText);
« Attendez un moment » peut être implémenté avec setTimeout
ou quelque chose
du même genre, mais il existe un meilleur moyen. Un objet requête a une
propriété readyState
indiquant l’état dans lequel il se trouve. Il passera
à 4
lorsque le document aura été complètement chargé, et aura une valeur
inférieure avant cela3. Pour réagir au changement de cet état, nous pouvons
définir la propriété onreadystatechange
de l’objet par une fonction. Cette
fonction sera appelée à chaque fois que l’état change.
requete.open("GET", "files/fruit.xml", true); requete.send(null); requete.onreadystatechange = function() { if (requete.readyState == 4) show(requete.responseText.length); };
Lorsque le fichier récupéré par l’objet requête est un document XML, la
propriété responseXML
de la requête contiendra une représentation de ce
document. Cette représentation fonctionne de la même manière que l’objet DOM
examiné dans le chapitre 12, mis à part qu’il n’a pas de fonctionnalités spécifiques
au HTML telles que style
ou innerHTML
. responseXML
nous fournit un objet
document dont la propriété documentElement
fait référence à la balise
extérieure du document XML.
var catalogue = requete.responseXML.documentElement; show(catalogue.childNodes.length);
De tels documents XML peuvent être utilisés pour échanger des informations
structurées avec le serveur. Leur forme ― des balises contenant d’autres
balises ― est souvent très adaptée pour le stockage de choses qu’il serait
difficile de représenter seulement avec du texte plat. Cependant, l’interface
DOM est plutôt mal fichue pour extraire des informations, et les documents XML
sont connus pour être verbeux : Le document fruit.xml
a l’air imposant alors
qu’il dit seulement « les pommes sont rouges, les oranges sont orange et les
bananes sont jaunes ».
Les programmeurs JavaScript ont trouvé une alternative à XML appelée JSON. Elle utilise la notation JavaScript élémentaire des
valeurs pour représenter les informations hiérarchisées sous une forme plus
minimaliste. Un document JSON est un fichier contenant un seul objet JavaScript
ou un tableau, pouvant lui-même contenir d’autres objets, des tableaux, des
chaînes de caractères, des nombres, des booléens ou la valeur null. Par
exemple, regardez fruit.json
:
requete.open("GET", "files/fruit.json", true); requete.send(null); requete.onreadystatechange = function() { if (requete.readyState == 4) print(requete.responseText); };
Un morceau de texte comme celui-ci peut être converti en valeur JavaScript
normale en utilisant la fonction eval
. Des parenthèses doivent être
ajoutées autour de lui avant d’appeler eval
, car sinon JavaScript pourrait
interpréter un objet (entouré d’accolades) comme un bloc de code et engendrer
une erreur.
function evalJSON(json) { return eval("(" + json + ")"); } var fruit = evalJSON(requete.responseText); show(fruit);
Lorsque vous exécutez eval
sur un morceau de texte, vous devez garder à
l’esprit que cela signifie que vous permettez à ce bout de texte d’exécuter
arbitrairement n’importe quel code. Comme JavaScript ne nous permet de faire
des requêtes que sur notre propre domaine, vous connaîtrez généralement de
manière précise le genre de texte que vous récupérez et cela ne pose pas de
problème. Dans d’autres situations, cela peut se révéler dangereux.
Écrivez une fonction appelée serializeJSON
qui, lorsqu’on lui fournit une
valeur JavaScript, crée une chaîne de caractères avec la représentation JSON de
la valeur. Les valeurs simples comme les nombres et les booléens peuvent
simplement être données à la fonction String
pour les convertir en chaînes de
caractères. Les objets et les tableaux peuvent être traités par récursion.
Il faut reconnaître que les tableaux peuvent être sournois car ils sont du type
« object
». Vous pouvez utiliser instanceof Array
, mais cela fonctionnera
uniquement pour les tableaux créés dans la même fenêtre ― les autres
utiliseront le prototype d’Array
des autres fenêtres et instanceof
renverra
false
. Une astuce est de convertir la propriété constructor
en chaîne de
caractères et de voir si elle contient « function Array
».
Quand vous convertissez une chaîne, vous devez faire attention à échapper ses
caractères. Si vous utilisez des guillemets doubles autour de la chaîne, les
caractères à échapper sont \"
, \\
, \f
, \b
, \n
, \t
, \r
, et \v
4.
function serializeJSON(valeur) { function isArray(valeur) { return /^\s*function Array/.test(String(valeur.constructor)); } function serializeArray(valeur) { return "[" + map(serializeJSON, valeur).join(", ") + "]"; } function serializeObject(valeur) { var proprietes = []; forEachIn(valeur, function(nom, valeur) { proprietes.push(serializeString(nom) + ": " + serializeJSON(valeur)); }); return "{" + proprietes.join(", ") + "}"; } function serializeString(valeur) { var caracteresSpeciaux = {"\"": "\\\"", "\\": "\\\\", "\f": "\\f", "\b": "\\b", "\n": "\\n", "\t": "\\t", "\r": "\\r", "\v": "\\v"}; var valeurAvecEchappements = valeur.replace(/[\"\\\f\b\n\t\r\v]/g, function(c) {return caracteresSpeciaux[c];}); return "\"" + valeurAvecEchappements + "\""; } var type = typeof valeur; if (type == "object" && isArray(valeur)) return serializeArray(valeur); else if (type == "object") return serializeObject(valeur); else if (type == "string") return serializeString(valeur); else return String(valeur); } print(serializeJSON(fruit));
L’astuce utilisée dans serializeString
est similaire à celle que nous avons
vue dans la fonction escapeHTML
du chapitre 10. Elle utilise un objet pour
chercher les substitutions nécessaires pour chacun des caractères. Certaines
d’entre elles, comme "\\\\"
, ont l’air assez étranges parce qu’il est
nécessaire de mettre deux antislash devant chaque antislash dans la chaîne de
résultat.
Notez également que les noms de propriétés sont entre guillemets comme des chaînes. Pour certaines d’entre elles ce n’est pas nécessaire, mais c’est préférable pour ceux qui incluent des espaces et d’autres choses curieuses, donc le code joue la sécurité et met tout entre guillemets.
Quand on fait de nombreuses requêtes, on ne souhaite pas, bien entendu, répéter
à chaque fois le même rituel open
, send
, onreadystatechange
. Voilà à quoi
peut ressembler une fonction encapsulatrice très simple :
function simpleHttpRequest(url, succes, echec) { var requete = makeHttpObject(); requete.open("GET", url, true); requete.send(null); requete.onreadystatechange = function() { if (requete.readyState == 4) { if (requete.status == 200) succes(requete.responseText); else if (echec) echec(requete.status, requete.statusText); } }; } simpleHttpRequest("files/fruit.txt", print);
La fonction accède à l’URL qu’on lui donne et appelle la fonction qu’on lui
donne comme second argument avec le contenu. Quand un troisième argument est
passé, il sert à indiquer une erreur ― un code d’état différent de 200
.
Pour pouvoir faire des requêtes plus complexes, on peut s’arranger pour que la
fonction accepte des paramètres supplémentaires pour préciser la méthode (GET
ou POST
), une chaîne facultative pour l’envoyer comme donnée, une façon
d’ajouter des en-têtes supplémentaires et ainsi de suite. Quand vous aurez
autant d’arguments, vous souhaiterez probablement les passer comme un « objet
d’arguments » comme nous l’avons vu dans le chapitre 9.
Certains sites web font un usage intensif de la communication entre les
programmes qui tournent côté client et ceux qui tournent côté serveur. Dans de
tels systèmes, il peut être pratique de considérer certaines requêtes HTTP
comme des appels à des fonctions qui s’exécutent sur le serveur. Le client fait
une requête vers des URL qui identifient les fonctions, leur donnant des
arguments sous forme de paramètres URL ou de données POST
. Le serveur appelle
alors la fonction, et met le résultat dans un document JSON ou XML qu’il
renvoie. Si vous écrivez quelques fonctions de support pratiques, ceci peut
rendre les appels côté serveur presque aussi simples qu’ils le sont côté
client… à l’exception bien sûr du retour des résultats qui ne sera pas aussi
instantané.
- Ce ne sont pas les seuls types de requêtes. Il y a aussi
HEAD
pour demander uniquement les en-têtes d’un document sans le contenu,PUT
pour ajouter un document sur un serveur etDELETE
pour supprimer un document. Ceux-ci ne sont pas utilisés par les navigateurs et ne sont souvent même pas pris en charge par les serveurs web. - La partie XML du nom
XMLHttpRequest
n’est pas la seule à être trompeuse ― l’objet peut aussi être utilisé pour une requête à travers des protocoles autres que HTTP,Request
est donc la seule partie significative qu’il nous reste. 0
(« non initialisé ») est l’état de l’objet avant qu’open
ne soit appelé dessus. Appeleropen
le passe à1
(« ouvert »). Appelersend
le fait poursuivre vers2
(« envoyé »). Lorsque le serveur répond, il passe à3
(« réception »). Enfin,4
signifie « chargé ».- Nous avons déjà rencontré
\n
, qui crée une nouvelle ligne.\t
est un caractère de tabulation,\r
un « retour chariot », que certains systèmes utilisent au lieu d’un\n
pour indiquer une fin de ligne.\b
(backspace),\v
(tabulation verticale), et\f
(saut de page) sont utiles quand on travaille avec de vieilles imprimantes mais moins utiles quand on parle de navigateurs Web.