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'objetXMLHttpRequest
a
été initialement conçu par Microsoft et implémenté dans Internet Explorer et Outlook sous forme d'un contrôle
ActiveX
. Nommé à l'origineXMLHTTP
par 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
deXMLHttpRequest
risque 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 !
L'utilisation de l'objet XHR se fait en deux étapes bien distinctes :
Préparation et envoi de la requête ;
Réception des données.
Nous allons donc étudier l'utilisation de cette technologie au travers de ces deux étapes.
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
,POST
etHEAD
.
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, àfalse
elle 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.php
sur le nom de
domainemon_site_web.com
par 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.
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.
Intéressons-nous à un point particulier de ce cours ! Les méthodes
d'envoiGET
etPOST
vous 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¶m2=valeur2');
Il est cependant conseillé, quelle que
soit la méthode utilisée
(GET
ouPOST
), 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 + '¶m2=' + 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 + '¶m2=' + value2);
Cependant, la
méthodePOST
consiste 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 + '¶m2=' + 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.
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.
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éreadystatechange
auquel 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 |
---|---|---|
|
0 |
L'objet XHR a été créé, mais pas initialisé (la
méthode |
|
1 |
La
méthode |
|
2 |
La
méthode |
|
3 |
Le serveur traite les informations et a commencé à renvoyer les données. Tous les en-têtes des fichiers ont été reçus. |
|
4 |
Toutes les données ont été réceptionnées. |
L'utilisation de la
propriétéreadyState
est 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éreadyState
contient 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éestatusText
contenant 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.
Une fois la requête terminée, il vous faut récupérer les données obtenues. Ici, deux possibilités s'offrent à vous :
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.
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
sontresponseText
etresponseXML
.
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"?>
Ligne 1 - Colonne 1
Ligne 1 - Colonne 2
Ligne 1 - Colonne 3
Ligne 2 - Colonne 1
Ligne 2 - Colonne 2
Ligne 2 - Colonne 3
Ligne 3 - Colonne 1
Ligne 3 - Colonne 2
Ligne 3 - Colonne 3
vous pouvez récupérer toutes les
balises<cel>
de la manière suivante :
var cels = xhr.responseXML.getElementsByTagName('cel');
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 »
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 :
Veuillez choisir quel est le fichier dont vous souhaitez voir le contenu :
type="button" value="file1.txt"
type="button" value="file2.txt"
id="fileContent"
Aucun fichier chargé
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 !
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.
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.
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) :
type="button" value="file1.txt"
type="button" value="file2.txt"
type="button" value="unknown.txt"
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);
}
});
Et voilà ! Vous pouvez d'ores et déjà commencer à vous servir de l'AJAX comme bon vous semble sans trop de problèmes !
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.
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.
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.
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.
Enfin nous y sommes ! Entrons dans le vif du sujet et voyons ce qui ne va pas !
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 : ê.
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.
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.
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 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.
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.com
et 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 |
---|---|---|
|
Succès |
Même protocole et même nom de domaine |
|
Succès |
Même protocole et même nom de domaine, seul le dossier diffère |
|
Échec |
Même protocole et même nom de domaine, mais le port est différent (80 par défaut) |
|
Échec |
Protocole différent (HTTPS au lieu de HTTP) |
|
Échec |
Sous-domaine différent |
|
Échec |
Si
l'appel est fait depuis un nom de domaine dont les « www » sont spécifiés, alors il |
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.
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-Origin
et 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.
Le XHR2 fournit de nombreuses propriétés supplémentaires ; quant aux méthodes, il n'y en a qu'une seule de nouvelle.
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
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
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$_COOKIE
et$_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.php
située sur un
nom de domaine A. Depuis cette page, nous appelons la
pageserver.php
située sur le domaine B grâce à une requête
cross-domain. Les cookies et les sessions reçus par la pageserver.php
sont-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.
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.
Commençons par trois événements bien simples
:loadstart
,load
etloadend
.
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),
alorsload
se déclenche, tandis
queloadend
se déclenche dans tous les cas. L'avantage de l'utilisation
deload
etloadend
, 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
sonterror
etabort
. Le
premier se déclenche en cas de non-aboutissement de la requête (quandreadyState
n'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.
progress
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éesloaded
ettotal
.
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
FormData
Cet objet consiste à faciliter l'envoi des données par le biais de la
méthodePOST
des requêtes XHR. Comme nous l'avons dit plus tôt dans ce
chapitre, l'envoi des données par le biais dePOST
est 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'objetFormData
est 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'objetFormData
doit ê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'attributname
des é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 objetFormData
sera alors prérempli avec
toutes les valeurs de votre formulaire. Voici un exemple simple :
id="myForm"
id="myText" name="myText" type="text" value="Test ! Un, deux, un, deux !"
var xhr = new XMLHttpRequest();
xhr.open('POST', 'script.php');
var myForm = document.getElementById('myForm'),
form = new FormData(myForm);
xhr.send(form);
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 !
L'objetXMLHttpRequest
est 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
:GET
etPOST
. 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
objetsFormData
et de nouveaux événements.