XMLHttpRequest

Il est temps de mettre le principe de l'AJAX en pratique avec l'objetXMLHttpRequest. Cette technique AJAX est la plus courante et est définitivement incontournable.
Au cours de ce chapitre nous allons étudier deux versions de cet objet. Les bases seront tout d'abord étudiées avec la première version : nous verrons comment réaliser de simples transferts de données, puis nous aborderons la résolution des problèmes d'encodage. La deuxième version fera office d'étude avancée des transferts de données, les problèmes liés au principe de la same origin policy seront levés et nous étudierons l'usage d'un nouvel objet nomméFormData.

L'objet XMLHttpRequest

Présentation

L'objetXMLHttpRequesta été initialement conçu par Microsoft et implémenté dans Internet Explorer et Outlook sous forme d'un contrôle ActiveX . Nommé à l'origineXMLHTTPpar Microsoft, il a été par la suite repris par de nombreux navigateurs sous le nom que nous lui connaissons actuellement :XMLHttpRequest. Sa standardisation viendra par la suite par le biais du W3C.

Le principe même de cet objet est classique : une requête HTTP est envoyée à l'adresse spécifiée, une réponse est alors attendue en retour de la part du serveur ; une fois la réponse obtenue, la requête s'arrête et peut éventuellement être relancée.

XMLHttpRequest, versions 1 et 2

L'objet que nous allons étudier dans ce chapitre possède deux versions majeures. La première version est celle issue de la standardisation de l'objet d'origine et son support est assuré par tous les navigateurs. L'utilisation de cette première version est extrêmement courante, mais les fonctionnalités paraissent maintenant bien limitées, étant donné l'évolution des technologies.

La deuxième version introduit de nouvelles fonctionnalités intéressantes, comme la gestion du cross-domain (nous reviendrons sur ce terme plus tard), ainsi que l'introduction de l'objetFormData. Cependant, peu de navigateurs supportent actuellement son utilisation.

Alors, quelle version utiliser ?

Dans un cas général, la première version est très fortement conseillée ! Un site Web utilisant la deuxième version deXMLHttpRequestrisque de priver une partie de ses visiteurs des fonctionnalités AJAX fournies en temps normal. D'autant plus qu'il n'existe pas de polyfill pour ce genre de technologies (on ne parle pas ici d'imiter simplement le fonctionnement d'une seule méthode, mais d'une technologie complète).

En revanche, la deuxième version est quand même relativement bien supportée , si avez la possibilité d'ignorer IE9 vous pouvez alors foncer !

Première version : les bases

L'utilisation de l'objet XHR se fait en deux étapes bien distinctes :

  1. Préparation et envoi de la requête ;

  2. Réception des données.

Nous allons donc étudier l'utilisation de cette technologie au travers de ces deux étapes.

Préparation et envoi de la requête

Pour commencer à préparer notre requête, il nous faut tout d'abord instancier un objet XHR :

var xhr = new XMLHttpRequest();

La préparation de la requête se fait par le biais de la méthodeopen(), qui prend en paramètres cinq arguments différents, dont trois facultatifs :

  • Le premier argument contient la méthode d'envoi des données, les trois méthodes principales sontGET,POSTetHEAD.

  • Le deuxième argument est l'URL à laquelle vous souhaitez soumettre votre requête, par exemple :'http://mon_site_web.com'.

  • Le troisième argument est un booléen facultatif dont la valeur par défaut esttrue. Àtrue, la requête sera de type asynchrone, àfalseelle sera synchrone (la différence est expliquée plus tard).

  • Les deux derniers arguments sont à spécifier en cas d'identification nécessaire sur le site Web (à cause d'un .htaccess par exemple). Le premier contient le nom de l'utilisateur, tandis que le deuxième contient le mot de passe.

Voici une utilisation basique et courante de la méthodeopen():

xhr.open('GET', 'http://mon_site_web.com/ajax.php');

Cette ligne de code prépare une requête afin que cette dernière contacte la pageajax.phpsur le nom de domainemon_site_web.compar le biais du protocolehttp(vous pouvez très bien utiliser d'autres protocoles, comme HTTPS ou FTP par exemple). Tout paramètre spécifié à la requête sera transmis par le biais de la méthodeGET.

Après préparation de la requête, il ne reste plus qu'à l'envoyer avec la méthodesend(). Cette dernière prend en paramètre un argument obligatoire que nous étudierons plus tard. Dans l'immédiat, nous lui spécifions la valeurnull:

xhr.send(null);

Après exécution de cette méthode, l'envoi de la requête commence. Cependant, nous n'avons spécifié aucun paramètre ni aucune solution pour vérifier le retour des données, l'intérêt est donc quasi nul.

Synchrone ou asynchrone ?

Vous savez très probablement ce que signifient ces termes dans la vie courante, mais que peuvent-ils donc désigner une fois transposés au sujet actuel ? Une requête synchrone va bloquer votre script tant que la réponse n'aura pas été obtenue, tandis qu'une requête asynchrone laissera continuer l'exécution de votre script et vous préviendra de l'obtention de la réponse par le biais d'un événement.

Quelle est la solution la plus intéressante ?

Il s'agit sans conteste de la requête asynchrone. Il est bien rare que vous ayez besoin que votre script reste inactif simplement parce qu'il attend une réponse à une requête. La requête asynchrone vous permet de gérer votre interface pendant que vous attendez la réponse du serveur, vous pouvez donc indiquer au client de patienter ou vous occuper d'autres tâches en attendant.

Transmettre des paramètres

Intéressons-nous à un point particulier de ce cours ! Les méthodes d'envoiGETetPOSTvous sont sûrement familières, mais qu'en est-il deHEAD? En vérité, il ne s'agit tout simplement pas d'une méthode d'envoi, mais de réception : en spécifiant cette méthode, vous ne recevrez pas le contenu du fichier dont vous avez spécifié l'URL, mais juste son en-tête (son header, d'où leHEAD). Cette utilisation est pratique quand vous souhaitez simplement vérifier, par exemple, l'existence d'un fichier sur un serveur.

Revenons maintenant aux deux autres méthodes qui sont, elles, conçues pour l'envoi de données !

Comme dit précédemment, il est possible de transmettre des paramètres par le biais de la méthodeGET. La transmission de ces paramètres se fait de la même manière qu'avec une URL classique, il faut les spécifier avec les caractères ? et & dans l'URL que vous passez à la méthodeopen():

xhr.open('GET', 'http://mon_site_web.com/ajax.php?param1=valeur1&param2=valeur2');

Il est cependant conseillé, quelle que soit la méthode utilisée (GETouPOST), d'encoder toutes les valeurs que vous passez en paramètre grâce à la fonctionencodeURIComponent(), afin d'éviter d'écrire d'éventuels caractères interdits dans une URL :

var value1 = encodeURIComponent(value1),
value2 = encodeURIComponent(value2);
xhr.open('GET', 'http://mon_site_web.com/ajax.php?param1=' + value1 + '&param2=' + value2);

Votre requête est maintenant prête à envoyer des paramètres par le biais de la méthodeGET!

En ce qui concerne la méthodePOST, les paramètres ne sont pas à spécifier avec la méthodeopen()mais avec la méthodesend():

xhr.open('POST', 'http://mon_site_web.com/ajax.php');
xhr.send('param1=' + value1 + '&param2=' + value2);

Cependant, la méthodePOSTconsiste généralement à envoyer des valeurs contenues dans un formulaire, il faut donc modifier les en-têtes d'envoi des données afin de préciser qu'il s'agit de données provenant d'un formulaire (même si, à la base, ce n'est pas le cas) :

xhr.open('POST', 'http://mon_site_web.com/ajax.php');
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send('param1=' + value1 + '&param2=' + value2);

La méthodesetRequestHeader()permet l'ajout ou la modification d'un en-tête, elle prend en paramètres deux arguments : le premier est l'en-tête concerné et le deuxième est la valeur à lui attribuer.

Réception des données

La réception des données d'une requête se fait par le biais de nombreuses propriétés. Cependant, les propriétés à utiliser diffèrent selon que la requête est synchrone ou non.

Requête asynchrone : spécifier la fonction de callback

Dans le cas d'une requête asynchrone, il nous faut spécifier une fonction de callback afin de savoir quand la requête s'est terminée. Pour cela, l'objet XHR possède un événement nomméreadystatechangeauquel il suffit d'attribuer une fonction :

xhr.addEventListener('readystatechange', function() {
// Votre code…
});

Cependant, cet événement ne se déclenche pas seulement lorsque la requête est terminée, mais plutôt, comme son nom l'indique, à chaque changement d'état. Il existe cinq états différents représentés par des constantes spécifiques à l'objetXMLHttpRequest:

Constante

Valeur

Description

UNSENT

0

L'objet XHR a été créé, mais pas initialisé (la méthodeopen()n'a pas encore été appelée).

OPENED

1

La méthodeopen()a été appelée, mais la requête n'a pas encore été envoyée par la méthodesend().

HEADERS_RECEIVED

2

La méthodesend()a été appelée et toutes les informations ont été envoyées au serveur.

LOADING

3

Le serveur traite les informations et a commencé à renvoyer les données. Tous les en-têtes des fichiers ont été reçus.

DONE

4

Toutes les données ont été réceptionnées.

L'utilisation de la propriétéreadyStateest nécessaire pour connaître l'état de la requête. L'état qui nous intéresse est le cinquième (la constanteDONE), car nous voulons simplement savoir quand notre requête est terminée. Il existe deux manières pour vérifier que la propriétéreadyStatecontient bien une valeur indiquant que la requête est terminée, la première (que nous utiliserons pour une question de lisibilité) consiste à utiliser la constante elle-même :

xhr.addEventListener('readystatechange', function() {
if (xhr.readyState === XMLHttpRequest.DONE) { // La constante DONE appartient à l'objet XMLHttpRequest, elle n'est pas globale
// Votre code…
}
});

Tandis que la deuxième manière de faire consiste à utiliser directement la valeur de la constante, soit 4 pour la constanteDONE:

xhr.addEventListener('readystatechange', function() {
if (xhr.readyState === 4) {
// Votre code…
}
});

De cette manière, notre code ne s'exécutera que lorsque la requête aura terminé son travail. Toutefois, même si la requête a terminé son travail, cela ne veut pas forcément dire qu'elle l'a mené à bien, pour cela nous allons devoir consulter le statut de la requête grâce à la propriétéstatus. Cette dernière renvoie le code correspondant à son statut, comme le fameux 404 pour les fichiers non trouvés. Le statut qui nous intéresse est le 200, qui signifie que tout s'est bien passé :

xhr.addEventListener('readystatechange', function() {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
// Votre code…
}
});

À noter qu'il existe aussi une propriété nomméestatusTextcontenant une version au format texte du statut de la requête, en anglais seulement. Par exemple, un statut 404 vous donnera le texte suivant : « Not Found ».

Nous avons ici traité le cas d'une requête asynchrone, mais sachez que pour une requête synchrone il n'y a qu'à vérifier le statut de votre requête, tout simplement.

Traitement des données

Une fois la requête terminée, il vous faut récupérer les données obtenues. Ici, deux possibilités s'offrent à vous :

  1. Les données sont au format XML, vous pouvez alors utiliser la propriétéresponseXML, qui permet de parcourir l'arbre DOM des données reçues.

  2. Les données sont dans un format autre que le XML, il vous faut alors utiliser la propriétéresponseText, qui vous fournit toutes les données sous forme d'une chaîne de caractères. C'est à vous qu'incombe la tâche de faire d'éventuelles conversions, par exemple avec un objet JSON :var response = JSON.parse(xhr.responseText);.

Les deux propriétés nécessaires à l'obtention des données sontresponseTextetresponseXML. Cette dernière est particulière, dans le sens où elle contient un arbre DOM que vous pouvez facilement parcourir. Par exemple, si vous recevez l'arbre DOM suivant :

<?xml version="1.0" encoding="utf-8"?>
<table>
<line>
<cel>Ligne 1 - Colonne 1</cel>
<cel>Ligne 1 - Colonne 2</cel>
<cel>Ligne 1 - Colonne 3</cel>
</line>
<line>
<cel>Ligne 2 - Colonne 1</cel>
<cel>Ligne 2 - Colonne 2</cel>
<cel>Ligne 2 - Colonne 3</cel>
</line>
<line>
<cel>Ligne 3 - Colonne 1</cel>
<cel>Ligne 3 - Colonne 2</cel>
<cel>Ligne 3 - Colonne 3</cel>
</line>
</table>

vous pouvez récupérer toutes les balises<cel>de la manière suivante :

var cels = xhr.responseXML.getElementsByTagName('cel');
Récupération des en-têtes de la réponse

Il se peut que vous ayez parfois besoin de récupérer les valeurs des en-têtes fournis avec la réponse de votre requête. Pour cela, vous pouvez utiliser deux méthodes. La première se nommegetAllResponseHeaders()et retourne tous les en-têtes de la réponse en vrac. Voici ce que cela peut donner :

Date: Sat, 17 Sep 2011 20:09:46 GMT
Server: Apache
Vary: Accept-Encoding
Content-Encoding: gzip
Content-Length: 20
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=utf-8

La deuxième méthode,getResponseHeader(), permet la récupération d'un seul en-tête. Il suffit d'en spécifier le nom en paramètre et la méthode retournera sa valeur :

var xhr = new XMLHttpRequest();
xhr.open('HEAD', 'http://mon_site_web.com/', false);
xhr.send(null);
alert(xhr.getResponseHeader('Content-type')); // Affiche : « text/html; charset=utf-8 »

Mise en pratique

L'étude de cet objet étant assez segmentée, nous n'avons pas encore eu l'occasion d'aborder un quelconque exemple. Pallions ce problème en créant une page qui va s'occuper de charger le contenu de deux autres fichiers selon le choix de l'utilisateur.

Commençons par le plus simple et créons notre page HTML qui va s'occuper de charger le contenu des deux fichiers :

<p>
Veuillez choisir quel est le fichier dont vous souhaitez voir le contenu :
</p>
<p>
<input type="button" value="file1.txt" />
<input type="button" value="file2.txt" />
</p>
<p id="fileContent">
<span>Aucun fichier chargé</span>
</p>

Comme vous pouvez le constater, le principe est très simple, nous allons pouvoir commencer notre code JavaScript. Créons tout d'abord une fonction qui sera appelée lors d'un clic sur un des deux boutons, elle sera chargée de s'occuper du téléchargement et de l'affichage du fichier passé en paramètre :

function loadFile(file) {
var xhr = new XMLHttpRequest();
// On souhaite juste récupérer le contenu du fichier, la méthode GET suffit amplement :
xhr.open('GET', file);
xhr.addEventListener('readystatechange', function() { // On gère ici une requête asynchrone
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { // Si le fichier est chargé sans erreur
document.getElementById('fileContent').innerHTML = '<span>' + xhr.responseText + '</span>'; // Et on affiche !
}
});
xhr.send(null); // La requête est prête, on envoie tout !
}

Il ne nous reste maintenant plus qu'à mettre en place les événements qui déclencheront tout le processus. Ça commence à être du classique pour vous, non ?

(function() { // Comme d'habitude, une IIFE pour éviter les variables globales
var inputs = document.getElementsByTagName('input'),
inputsLen = inputs.length;
for (var i = 0; i < inputsLen; i++) {
inputs[i].addEventListener('click', function() {
loadFile(this.value); // À chaque clique, un fichier sera chargé dans la page
});
}
})();

Et c'est tout bon ! Il ne vous reste plus qu'à essayer le résultat de ce travail !

Essayer le code complet

Cela fonctionne plutôt bien, n'est-ce pas ? Peut-être même trop bien, on ne se rend pas compte que l'on utilise ici de l'AJAX tellement le résultat est rapide. Enfin, on ne va pas s'en plaindre !

Pourquoi ce code ne fonctionne-t-il pas chez moi alors que l'exemple fonctionne ?

Et oui, c'est fort probable que l'exemple précédent ne fonctionne pas en local, alors qu'il fonctionne parfaitement sur un serveur. Voyons pourquoi.

XHR et les tests locaux

Nous avons vu précédemment qu'il fallait utiliser la propriété status  pour savoir si la requête HTTP avait abouti. En ce cas, la valeur de status  est200 . Oui, mais... si vous testez en local, il n'y a pas de requête HTTP ! Et donc, status vaudra0 . Pour qu'un code XHR fonctionne en local, il faut donc gérer le cas où status  peut valoir 0  :

if (xhr.readyState === XMLHttpRequest.DONE && (xhr.status === 200 || xhr.status === 0)) {}

Mais attention, évitez de laisser cette condition lorsque votre script sera sur votre serveur, car la valeur 0  est une valeur d'erreur. Autrement dit, si une fois en ligne votre requête rencontre un problème, 0  sera peut-être également retourné. Je dis peut-être, car 0 n'est pas une valeur autorisée comme code HTTP. C'est toutefois documenté par le W3C comme étant une valeur retournée dans certains cas, mais c'est un peu complexe.

Gestion des erreurs

Cet exercice vous a sûrement clarifié un peu l'esprit quant à l'utilisation de cet objet, mais il reste un point qui n'a pas été abordé. Bien qu'il ne soit pas complexe, mieux vaut vous le montrer, notamment afin de ne jamais l'oublier : la gestion des erreurs !

Le code de l'exercice que nous venons de réaliser ne sait pas prévenir en cas d'erreur, ce qui est assez gênant au final, car l'utilisateur pourrait ne pas savoir si ce qui se passe est normal. Nous allons donc mettre en place un petit bout de code pour prévenir en cas de problème, et nous allons aussi faire en sorte de provoquer une erreur afin que vous n'ayez pas à faire 30 000 chargements de fichiers avant d'obtenir une erreur. :-°

Commençons par fournir un moyen de générer une erreur en chargeant un fichier inexistant (nous aurons donc une erreur 404) :

<p>
<input type="button" value="file1.txt" />
<input type="button" value="file2.txt" />
<br /><br />
<input type="button" value="unknown.txt" />
</p>

Maintenant, occupons-nous de la gestion de l'erreur dans notre événementreadystatechange:

xhr.addEventListener('readystatechange', function() { // On gère ici une requête asynchrone
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { // Si le fichier est chargé sans erreur
document.getElementById('fileContent').innerHTML = '<span>' + xhr.responseText + '</span>'; // On l'affiche !
} else if (xhr.readyState === XMLHttpRequest.DONE && xhr.status != 200) { // En cas d'erreur !
alert('Une erreur est survenue !\n\nCode :' + xhr.status + '\nTexte : ' + xhr.statusText);
}
});

Essayer le code complet

Et voilà ! Vous pouvez d'ores et déjà commencer à vous servir de l'AJAX comme bon vous semble sans trop de problèmes !

Résoudre les problèmes d'encodage

Avant de commencer, disons-le purement et simplement : vous allez détester cette sous-partie ! Pourquoi ? Tout simplement parce que nous allons aborder un problème qui gêne un grand nombre d'apprentis développeurs Web : l'encodage des caractères. Nous allons toutefois essayer d'aborder la chose de la manière la plus efficace possible afin que vous n'ayez pas trop de mal à comprendre le problème.

L'encodage pour les nuls

Nombreux sont les développeurs débutants qui préfèrent ignorer le principe de l'encodage des caractères, car le sujet est un peu difficile à assimiler. Nous allons ici l'étudier afin que vous puissiez comprendre pourquoi vous allez un jour ou l'autre rencontrer des erreurs assez étranges avec l'AJAX. Tout d'abord, qu'est-ce que l'encodage des caractères ?

Il s'agit d'une manière de représenter les caractères en informatique. Lorsque vous tapez un caractère sur votre ordinateur, il est enregistré au format binaire dans la mémoire de l'ordinateur. Ce format binaire est un code qui représente votre caractère, ce code ne représente qu'un seul caractère, mais peut très bien désigner des caractères très différents selon les normes utilisées.

Une histoire de normes

Comme vous l'avez compris, chaque caractère est représenté par un code binaire, qui n'est au final qu'un simple nombre. Ainsi, lorsque l'informatique a fait ses débuts, il a fallu attribuer un nombre à chaque caractère utilisé, ce qui donna naissance à la norme ASCII . Cette norme n'était pas mal pour un début, mais était codée sur seulement 7 bits, ce qui limitait le nombre de caractères représentables par cette norme à 128. Alors, dit comme ça, cela peut paraître suffisant pour notre alphabet de 26 lettres, mais que fait-on des autres caractères, comme les caractères accentués ? En effet, ces trois lettres sont bien trois caractères différents : e, é, è. Tout ça sans compter les différents caractères comme les multiples points de ponctuation, les tirets, etc. Bref, tout ça fait que la norme ASCII pouvait convenir pour un américain, mais de nombreuses autres langues que l'anglais ne pouvaient pas s'en servir en raison de son manque de « place ».

La solution à ce problème s'est alors imposée avec l'arrivée des normes ISO 8859 . Le principe est simple, la norme ASCII utilisait 7 bits, alors que l'informatique de nos jours stocke les informations par octets ; or 1 octet équivaut à 8 bits, ce qui fait qu'il reste 1 bit non utilisé. Les normes ISO 8859 ont pour but de l'exploiter afin de rajouter les caractères nécessaires à d'autres langues. Cependant, il n'est pas possible de stocker tous les caractères de toutes les langues dans seulement 8 bits (qui ne font que 256 caractères après tout), c’est pourquoi il est écrit « les normes 8859 » : il existe une norme 8859 (voire plusieurs) pour chaque langue. Pour information, la norme française est l' ISO 8859-1 .

Avec ces normes, n'importe qui peut maintenant rédiger un document dans sa langue maternelle. Les normes sont encore utilisées de nos jours et rendent de fiers services. Cependant, il y a un problème majeur ! Comment faire pour utiliser deux langues radicalement différentes (le français et le japonais, par exemple) dans un même document ? Une solution serait de créer une nouvelle norme utilisant plus de bits afin d'y stocker tous les caractères existants dans le monde, mais il y a un défaut majeur : en passant à plus de 8 bits, le stockage d'un seul caractère ne se fait plus sur 1 octet mais sur 2, ce qui multiplie le poids des fichiers textes par deux, et c'est absolument inconcevable !

La solution se nomme UTF-8 . Cette norme est très particulière, dans le sens où elle stocke les caractères sur un nombre variable de bits. Autrement dit, un caractère classique, comme la lettre A, sera stocké sur 8 bits (1 octet donc), mais un caractère plus exotique comme le A en japonais () est stocké sur 24 bits (3 octets), le maximum de bits utilisables par l'UTF-8 étant 32, soit 4 octets. En clair, l'UTF-8 est une norme qui sait s'adapter aux différentes langues et est probablement la norme d'encodage la plus aboutie de notre époque.

L'encodage et le développement Web

Comprendre l'encodage des caractères est une chose, mais savoir s'en servir en est une autre. Nous allons faire simple et rapide, et étudier quelles sont les étapes pour bien définir son encodage des caractères sur le Web.

Le monde du Web est stupide, il faut spécifier quel est l'encodage que vous souhaitez utiliser pour vos fichiers, alors que les navigateurs pourraient le détecter d'eux-mêmes. Prenons l'exemple d'un fichier PHP contenant du HTML et listons les différentes manières pour définir le bon encodage sur la machine du client :

  • Une étape toujours nécessaire est de bien encoder ses fichiers. Cela se fait dans les paramétrages de l'éditeur de texte que vous utilisez.

  • Le serveur HTTP (généralement Apache) peut indiquer quel est l'encodage utilisé par les fichiers du serveur. Cela est généralement paramétré de base, mais vous pouvez redéfinir ce paramétrage avec un fichier .htaccess contenant la ligne :AddDefaultCharset UTF-8. N'hésitez pas à lire le cours « Le .htaccess et ses fonctionnalités » du Site du Zéro écrit par kozo si vous ne savez pas ce que c'est.

  • Le langage serveur (généralement le PHP) peut aussi définir l'encodage utilisé dans les en-têtes du fichier. Si un encodage est spécifié par le PHP, alors il va remplacer celui indiqué par Apache. Cela se fait grâce à la ligne suivante :<?php header('Content-Type: text/html; charset=utf-8'); ?>.

  • Le HTML permet de spécifier l'encodage de votre fichier, mais cela n'est généralement que peu nécessaire, car les encodages spécifiés par Apache ou le PHP font que le navigateur ignore ce qui est spécifié par le document HTML ; cela dit, mieux vaut le spécifier pour le support des très vieux navigateurs. Cela se fait dans la balise<head>avec la ligne suivante :<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />.

Bref, beaucoup de manières de faire pour pas grand-chose, un bon paramétrage du serveur HTTP (Apache dans notre cas) est généralement suffisant, à condition d'avoir des fichiers encodés avec la norme spécifiée par le serveur, bien sûr. Alors, pourquoi vous avoir montré ça ? Parce que vous risquez d'avoir des problèmes d'encodage avec l'AJAX et que ce petit récapitulatif des manières de faire pour la spécification d'un encodage pourra sûrement vous aider à les résoudre.

L'AJAX et l'encodage des caractères

Enfin nous y sommes ! Entrons dans le vif du sujet et voyons ce qui ne va pas !

Le problème

Eh oui, il n'y a qu'un seul problème, mais il est de taille, bien que facile à régler une fois que l'on a bien compris le concept. Le voici : lorsque vous faites une requête AJAX, toutes les données sont envoyées avec un encodage UTF-8, quel que soit l'encodage du fichier HTML qui contient le script pour la requête AJAX !

Mais en quoi est-ce un problème ?

Eh bien, cela pose problème si vous travaillez autrement qu'en UTF-8 côté serveur. Car si le fichier PHP appelé par la requête AJAX est encodé, par exemple, en ISO 8859-1, alors il se doit de travailler avec des données ayant le même encodage, ce que ne fournira pas une requête AJAX.

Concrètement, quel problème cela pose-t-il ? Le serveur tombe en rade ?

Non, loin de là ! Mais vous allez vous retrouver avec des caractères étranges en lieu et place de certains caractères situés dans le texte d'origine, tout particulièrement pour les caractères accentués.

Comme vous le savez, l'ISO 8859-1 n'utilise que 8 bits pour l'encodage des caractères, tandis que l'UTF-8 peut aller jusqu'à 32. À première vue, ces deux normes n'ont aucune ressemblance, et pourtant si ! Leurs 7 premiers bits respectifs assignent les mêmes valeurs aux caractères concernés, ainsi la lettre A est représentée par ces 7 bits quelle que soit la norme utilisée, celle de l'ISO ou l'UTF-8 : 100 0001.

La différence se situe en fait pour les caractères que l'on va qualifier « d'exotiques », comme les caractères accentués. Ainsi, un e avec accent circonflexe (ê) a la valeur binaire suivante en ISO 8859-1 : 1110 1010, ce qui en UTF-8 équivaut à un caractère impossible à afficher. Bref, pas très pratique.

Mais les choses se corsent encore plus lorsque la conversion est faite depuis l'UTF-8 vers une autre norme, comme l'ISO 8859-1, car l'UTF-8 utilisera parfois 2 octets (voire plus) pour stocker un seul caractère, ce que les autres normes interprèteront comme étant deux caractères. Par exemple, la même lettre ê encodée en UTF-8 donne le code binaire suivant : 1100 0011 1010 1010. L'ISO 8859-1 va y voir 2 octets puisqu'il y a 16 bits, la première séquence de 8 bits (1100 0011) va donc être traduite avec le caractère Ã, et la deuxième séquence (1010 1010) avec ª.

Bref, tout cela signifie que si votre fichier HTML client est en ISO 8859-1 et qu'il envoie par l'AJAX le caractère ê à une page PHP encodée elle aussi en ISO 8859-1, alors les données qui seront lues par le serveur seront les suivantes : ê.

Comprendre la démarche de l'AJAX

Afin que vous compreniez encore mieux le problème posé par l'AJAX, il est bon de savoir quelles sont les étapes d'encodage d'une requête avec des fichiers en ISO 8859-1 (que nous allons abréger ISO) :

  • La requête est envoyée, les données sont alors converties proprement de l'ISO à l'UTF-8. Ainsi, le ê en ISO est toujours un ê en UTF-8, l'AJAX sait faire la conversion d'encodage sans problème.

  • Les données arrivent sur le serveur, c'est là que se pose le problème : elles arrivent en UTF-8, alors que le serveur attend des données ISO, cette erreur d'encodage n'étant pas détectée, le caractère ê n'est plus du tout le même vis-à-vis du serveur, il s'agit alors des deux caractères ê.

  • Le serveur renvoie des données au format ISO, mais celles-ci ne subissent aucune modification d'encodage lors du retour de la requête. Les données renvoyées par le serveur en ISO seront bien réceptionnées en ISO.

Ces trois points doivent vous faire comprendre qu'une requête AJAX n'opère en UTF-8 que lors de l'envoi des données, le problème d'encodage ne survient donc que lorsque les données sont réceptionnées par le serveur, et non pas quand le client reçoit les données renvoyées par le serveur.

Deux solutions

Il existe deux solutions pour éviter ce problème d'encodage sur vos requêtes AJAX.

La première, qui est de loin la plus simple et la plus pérenne, consiste à ce que votre site soit entièrement encodé en UTF-8, comme ça les requêtes AJAX envoient des données en UTF-8 qui seront reçues par un serveur demandant à traiter de l'UTF-8, donc sans aucun problème. Un site en UTF-8 implique que tous vos fichiers textes soient encodés en UTF-8, que le serveur indique au client le bon encodage, et que vos ressources externes, comme les bases de données, soient aussi en UTF-8.
Cette solution est vraiment la meilleure dans tous les sens du terme, mais est difficile à mettre en place sur un projet Web déjà bien entamé. Si vous souhaitez vous y mettre (et c'est même fortement conseillé), nous vous conseillons de lire le cours « Passer du latin1 à l'unicode » écrit par vyk12 sur le Site du Zéro.

La deuxième solution, encore bien souvent rencontrée, est plus adaptée si votre projet est déjà bien entamé et que vous ne pouvez vous permettre de faire une conversion complète de son encodage. Il s'agit de décoder les caractères reçus par le biais d'une requête AJAX avec la fonction PHPutf8_decode().

Admettons que vous envoyiez une requête AJAX à la page suivante :

<?php
header('Content-Type: text/plain; charset=iso-8859-1'); // On précise bien qu'il s'agit d'une page en ISO 8859-1
echo $_GET['parameter'];
?>

Si la requête AJAX envoie en paramètre la chaîne de caractères « Drôle de tête », le serveur va alors vous renvoyer ceci :

Drôle de tête

La solution consiste donc à décoder l'UTF-8 reçu pour le convertir en ISO 8859-1, la fonctionutf8_decode()intervient donc ici :

<?php
header('Content-Type: text/plain; charset=iso-8859-1'); // On précise bien qu'il s'agit d'une page en ISO 8859-1
echo utf8_decode($_GET['parameter']);
?>

Et là, aucun problème :

Drôle de tête

Et quand je renvoie les données du serveur au client, je dois encoder les données en UTF-8 ?

Absolument pas, car l'AJAX applique une conversion UTF-8 uniquement à l'envoi des données, comme étudié un peu plus haut. Donc si vous affichez des données en ISO 8859-1, elles arriveront chez le client avec le même encodage.

Deuxième version : usage avancé

La deuxième version du XHR ajoute de nombreuses fonctionnalités intéressantes. Pour ceux qui se posent la question, le XHR2 ne fait pas partie de la spécification du HTML5. Cependant, cette deuxième version utilise de nombreuses technologies liées au HTML5, nous allons donc nous limiter à ce qui est utilisable (et intéressant) et nous verrons le reste plus tard, dans la partie consacrée au HTML5.

Tout d'abord, faisons une petite clarification :

  • L'objet utilisé pour la deuxième version est le même que celui utilisé pour la première, à savoirXMLHttpRequest.

  • Toutes les fonctionnalités présentes dans la première version sont présentes dans la deuxième.

Maintenant que tout est clair, entrons dans le vif du sujet : l'étude des nouvelles fonctionnalités.

Les requêtes cross-domain

Les requêtes cross-domain sont des requêtes effectuées depuis un nom de domaine A vers un nom de domaine B. Elles sont pratiques, mais absolument inutilisables avec la première version du XHR en raison de la présence d'une sécurité basée sur le principe de la same origin policy . Cette sécurité est appliquée aux différents langages utilisables dans un navigateur Web, le JavaScript est donc concerné. Il est important de comprendre en quoi elle consiste et comment elle peut-être « contournée », car les requêtes cross-domain sont au cœur du XHR2.

Une sécurité bien restrictive

Bien que la same origin policy soit une sécurité contre de nombreuses failles, elle est un véritable frein pour le développement Web, car elle a pour principe de n'autoriser les requêtes XHR qu'entre les pages Web possédant le même nom de domaine. Si, par exemple, vous vous trouvez sur votre site personnel dont le nom de domaine estmon_site_perso.comet que vous tentez de faire une requête XHR vers le célèbre nom de domaine de chez Googlegoogle.com, vous allez alors rencontrer une erreur et la requête ne sera pas exécutée, car les deux noms de domaine sont différents.

Cette sécurité s'applique aussi dans d'autres cas, comme deux sous-domaines différents. Afin de vous présenter rapidement et facilement les différents cas concernés ou non par cette sécurité, voici un tableau largement réutilisé sur le Web. Il illustre différents cas où les requêtes XHR sont possibles ou non. Les requêtes sont exécutées depuis la page http://www.example.com/dir/page.html :

URL appelée

Résultat

Raison

http://www.example.com/dir/page.html

Succès

Même protocole et même nom de domaine

http://www.example.com/dir2/other.html

Succès

Même protocole et même nom de domaine, seul le dossier diffère

http://www.example.com:81/dir/other.html

Échec

Même protocole et même nom de domaine, mais le port est différent (80 par défaut)

https://www.example.com/dir/other.html

Échec

Protocole différent (HTTPS au lieu de HTTP)

http://en.example.com/dir/other.html

Échec

Sous-domaine différent

http://example.com/dir/other.html

Échec

Si l'appel est fait depuis un nom de domaine dont les « www » sont spécifiés, alors il
faut faire de même pour la page appelée

Alors, certes, cette sécurité est impérative, mais il se peut que parfois nous possédions deux sites Web dont les noms de domaine soient différents, mais dont la connexion doit se faire par le biais des requêtes XHR. La deuxième version du XHR introduit donc un système simple et efficace permettant l'autorisation des requêtes cross-domain.

Autoriser les requêtes cross-domain

Il existe une solution implémentée dans la deuxième version du XHR, qui consiste à ajouter un simple en-tête dans la page appelée par la requête pour autoriser le cross-domain. Cet en-tête se nommeAccess-Control-Allow-Originet permet de spécifier un ou plusieurs domaines autorisés à accéder à la page par le biais d'une requête XHR.

Pour spécifier un nom de domaine, il suffit d'écrire :

Access-Control-Allow-Origin: http://example.com

Ainsi, le domaine example.com aura accès à la page qui retourne cet en-tête. Il est impossible de spécifier plusieurs noms de domaine mais il est possible d'autoriser tous les noms de domaine à accéder à votre page, pour cela utilisez l'astérisque * :

Access-Control-Allow-Origin: *

Il ne vous reste ensuite plus qu'à ajouter cet en-tête aux autres en-têtes de votre page Web, comme ici en PHP :

<?php
header('Access-Control-Allow-Origin: *');
?>

Cependant, prenez garde à l'utilisation de cet astérisque, ne l'utilisez que si vous n'avez pas le choix, car, lorsque vous autorisez un nom de domaine à faire des requêtes cross-domain sur votre page, c'est comme si vous désactiviez une sécurité contre le piratage vis-à-vis de ce domaine.

Nouvelles propriétés et méthodes

Le XHR2 fournit de nombreuses propriétés supplémentaires ; quant aux méthodes, il n'y en a qu'une seule de nouvelle.

Éviter les requêtes trop longues

Il se peut que, de temps en temps, certaines requêtes soient excessivement longues. Afin d'éviter ce problème, il est parfaitement possible d'utiliser la méthodeabort()couplée àsetTimeout(), cependant le XHR2 fournit une solution bien plus simple à mettre en place. Il s'agit de la propriététimeout, qui prend pour valeur un temps en millisecondes. Une fois ce temps écoulé, la requête se terminera.

xhr.timeout = 10000; // La requête se terminera si elle n'a pas abouti au bout de 10 secondes
Forcer le type de contenu

Vous souvenez-vous lorsque nous avions abordé le fait qu'il fallait bien spécifier le type MIME de vos documents afin d'éviter que vos fichiers XML ne soient pas parsés ? Eh bien, sachez que si vous n'avez pas la possibilité de le faire (par exemple, si vous n'avez pas accès au code de la page que vous appelez), vous pouvez réécrire le type MIME reçu afin de parser correctement le fichier. Cette astuce se réalise avec la nouvelle méthodeoverrideMimeType(), qui prend en paramètre un seul argument contenant le type MIME exigé :

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com');
xhr.overrideMimeType('text/xml');
// L'envoi de la requête puis le traitement des données reçues peuvent se faire
Accéder aux cookies et aux sessions avec une requête cross-domain

Cela n'a pas été présenté plus tôt, mais il est effectivement possible pour une page appelée par le biais d'une requête XHR (versions 1 et 2) d'accéder aux cookies ou aux sessions du navigateur. Cela se fait sans contrainte, vous pouvez, par exemple, accéder aux cookies comme vous le faites d'habitude :

<?php
echo $_COOKIE['cookie1']; // Aucun problème !
?>

Cependant, cette facilité d'utilisation est loin d'être présente lorsque vous souhaitez accéder à ces ressources avec une requête cross-domain, car aucune valeur ne sera retournée par les tableaux$_COOKIEet$_SESSION.

Pourquoi ? Les cookies et les sessions ne sont pas envoyés ?

Eh bien non ! Rassurez-vous, il ne s'agit pas d'une fonctionnalité conçue pour vous embêter, mais bien d'une sécurité, car vous allez devoir autoriser le navigateur et le serveur à gérer ces données.

Quand nous parlons du serveur, nous voulons surtout parler de la page appelée par la requête. Vous allez devoir y spécifier l'en-tête suivant pour autoriser l'envoi des cookies et des sessions :

Access-Control-Allow-Credentials: true

Mais, côté serveur, cela ne suffira pas si vous avez spécifié l'astérisque * pour l'en-têteAccess-Control-Allow-Origin. Il vous faut absolument spécifier un seul nom de domaine, ce qui est malheureusement très contraignant dans certains cas d'applications (bien qu'ils soient rares).

Vous devriez maintenant avoir une page PHP commençant par un code de ce genre :

<?php
header('Access-Control-Allow-Origin: http://example.com');
header('Access-Control-Allow-Credentials: true');
?>

Cependant, vous pourrez toujours tenter d'accéder aux cookies ou aux sessions, vous obtiendrez en permanence des valeurs nulles. La raison est simple : le serveur est configuré pour permettre l'accès à ces données, mais le navigateur ne les envoie pas. Pour pallier ce problème, il suffit d'indiquer à notre requête que l'envoi de ces données est nécessaire. Cela se fait après initialisation de la requête et avant son envoi (autrement dit, entre l'utilisation des méthodesopen()etsend()) avec la propriétéwithCredentials:

xhr.open( );
xhr.withCredentials = true; // Avec « true », l'envoi des cookies et des sessions est bien effectué
xhr.send( );

Maintenant, une petite question technique pour vous : nous avons une page Web nomméeclient.phpsituée sur un nom de domaine A. Depuis cette page, nous appelons la pageserver.phpsituée sur le domaine B grâce à une requête cross-domain. Les cookies et les sessions reçus par la pageserver.phpsont-ils ceux du domaine A ou bien ceux du domaine B ?

Bonne question, n'est-ce pas ? La réponse est simple et logique : il s'agit de ceux du domaine B. Si vous faites une requête cross-domain, les cookies et les sessions envoyés seront constamment ceux qui concernent le domaine de la page appelée. Cela s'applique aussi si vous utilisez la fonction PHPsetcookie()dans la page appelée : les cookies modifiés seront ceux du domaine de cette page, et non pas ceux du domaine d'où provient la requête.

Quand les événements s'affolent

La première version du XHR ne comportait qu'un seul événement, la deuxième en comporte maintenant huit si on compte l'événementreadystatechange! Pourquoi tant d'ajouts ? Parce que le XHR1 ne permettait clairement pas de faire un suivi correct de l'état d'une requête.

Les événements classiques

Commençons par trois événements bien simples :loadstart,loadetloadend. Le premier se déclenche lorsque la requête démarre (lorsque vous appelez la méthodesend()). Les deux derniers se déclenchent lorsque la requête se termine, mais avec une petite différence : si la requête s'est correctement terminée (pas d'erreur 404 ou autre), alorsloadse déclenche, tandis queloadendse déclenche dans tous les cas. L'avantage de l'utilisation deloadetloadend, c'est que vous pouvez alors vous affranchir de la vérification de l'état de la requête avec la propriétéreadyState, comme vous le feriez pour l'événementreadystatechange.

Les deux événements suivants sonterroretabort. Le premier se déclenche en cas de non-aboutissement de la requête (quandreadyStaten'atteint même pas la valeur finale : 4), tandis que le deuxième s'exécutera en cas d'abandon de la requête avec la méthodeabort()ou bien avec le bouton « Arrêt » de l'interface du navigateur Web.

Vous souvenez-vous de la propriététimeout? Eh bien, sachez qu'il existe un événement du même nom qui se déclenche quand la durée maximale spécifiée dans la propriété associée est atteinte.

Le cas de l'événementprogress

Pour finir, nous allons voir l'utilisation d'un événement un peu plus particulier nomméprogress. Son rôle est de se déclencher à intervalles réguliers pendant le rapatriement du contenu exigé par votre requête. Bien entendu, son utilisation n'est nécessaire, au final, que dans les cas où le fichier rapatrié est assez volumineux. Cet événement a pour particularité de fournir un objet en paramètre à la fonction associée. Cet objet contient deux propriétés nomméesloadedettotal. Elles indiquent, respectivement, le nombre d'octets actuellement téléchargés et le nombre d'octets total à télécharger. Leur utilisation se fait de cette manière :

xhr.addEventListener('progress', function(e) {
element.innerHTML = e.loaded + ' / ' + e.total;
});

Au final, l'utilité de cet événement est assez quelconque, ce dernier a bien plus d'intérêt dans le cas d'un upload (mais cela sera abordé dans la partie consacrée au HTML5). Cela dit, il peut avoir son utilité dans le cas de préchargements de fichiers assez lourds. Ainsi, le préchargement de plusieurs images avec une barre de progression peut être une utilisation qui peut commencer à avoir son intérêt (mais, nous vous l'accordons, cela n'a rien de transcendant).

Cet événement n'étant pas très important, nous ne ferons pas un exercice expliqué pas à pas, toutefois, vous trouverez un lien vers un exemple en ligne dont le code est commenté, n'hésitez pas à y jeter un coup d’œil !

Essayer une adaptation de cet événement

L'objetFormData

Cet objet consiste à faciliter l'envoi des données par le biais de la méthodePOSTdes requêtes XHR. Comme nous l'avons dit plus tôt dans ce chapitre, l'envoi des données par le biais dePOSTest une chose assez fastidieuse, car il faut spécifier un en-tête dont on ne se souvient que très rarement de tête, on perd alors du temps à le chercher sur le Web.

Au-delà de son côté pratique en terme de rapidité d'utilisation, l'objetFormDataest aussi un formidable outil permettant de faire un envoi de données binaires au serveur. Ce qui, concrètement, veut dire qu'il est possible de faire de l'upload de fichiers par le biais des requêtes XHR. Cependant, l'upload de fichiers nécessite des connaissances approfondies sur le HTML5, cela sera donc traité plus tard. Nous allons tout d'abord nous contenter d'une utilisation relativement simple.

Tout d'abord, l'objetFormDatadoit être instancié :

var form = new FormData();

Une fois instancié, vous pouvez vous servir de son unique méthode :append(). Celle-ci ne retourne aucune valeur et prend en paramètres deux arguments obligatoires : le nom d'un champ (qui correspond à l'attributnamedes éléments d'un formulaire) et sa valeur. Son utilisation est donc très simple :

form.append('champ1', 'valeur1');
form.append('champ2', 'valeur2');

C'est là que cet objet est intéressant : pas besoin de spécifier un en-tête particulier pour dire que l'on envoie des données sous forme de formulaire. Il suffit juste de passer notre objet de typeFormDataà la méthodesend(), ce qui donne ceci sur un code complet :

var xhr = new XMLHttpRequest();
xhr.open('POST', 'script.php');
var form = new FormData();
form.append('champ1', 'valeur1');
form.append('champ2', 'valeur2');
xhr.send(form);

Et côté serveur, vous pouvez récupérer les données tout aussi simplement que d'habitude :

<?php
echo $_POST['champ1'] . ' - ' . $_POST['champ2']; // Affiche : « valeur1 - valeur2 »
?>

Revenons rapidement sur le constructeur de cet objet, car celui-ci possède un argument bien pratique : passez donc en paramètre un élément de formulaire et votre objetFormDatasera alors prérempli avec toutes les valeurs de votre formulaire. Voici un exemple simple :

<form id="myForm">
<input id="myText" name="myText" type="text" value="Test ! Un, deux, un, deux !" />
</form>
<script>
var xhr = new XMLHttpRequest();
xhr.open('POST', 'script.php');
var myForm = document.getElementById('myForm'),
form = new FormData(myForm);
xhr.send(form);
</script>

Ce qui, côté serveur, donne ceci :

<?php
echo $_POST['myText']; // Affiche : « Test ! Un, deux, un, deux ! »
?>

Voilà tout, cet objet est, mine de rien, bien pratique, même si vous ne savez pas encore faire d'upload de fichiers. Il facilite quand même déjà bien les choses !

En résumé
  • L'objetXMLHttpRequestest l'objet le plus utilisé pour l'AJAX. Deux versions de cet objet existent, la deuxième étant plus complète mais pas toujours disponible au sein de tous les navigateurs.

  • Deux modes sont disponibles : synchrone et asynchrone. Une requête de mode asynchrone sera exécutée en parallèle et ne bloquera pas l'exécution du script, tandis que la requête synchrone attendra la fin de la requête pour poursuivre l'exécution du script.

  • Deux méthodes d'envoi sont utilisables :GETetPOST. Dans le cas d'une méthodeGET, les paramètres de l'URL doivent être encodés avecencodeURIComponent().

  • Il faut faire attention à l'encodage, car toutes les requêtes sont envoyées en UTF-8 !

  • La version 2 du XHR introduit les requêtes cross-domain ainsi que les objetsFormDataet de nouveaux événements.