⚡ AJAX – Le Web Asynchrone
Guide dense : XHR, Fetch, async/await, CORS & Intégration Django.
Principes & Concepts
Le "Pourquoi". Synchrone (bloquant) vs. Asynchrone (non-bloquant). "X" (XML) vs. "J" (JSON).
Asynchrone JSONL'Historique : XMLHttpRequest
L'ancêtre. onreadystatechange, readyState (0-4), status (200, 404). Basé sur les événements.
Le Moderne : fetch()
Basé sur les Promises. Chaînage .then(), objet Response, headers, body.
L'Évolution : async/await
Sucre syntaxique pour les Promises. Rend le code asynchrone lisible. Gestion try...catch.
Formats de Données
JSON (le standard), FormData (fichiers), text/html, XML (l'ancêtre).
Gestion des Erreurs (Avancée)
Erreur Réseau (.catch()) vs. Erreur HTTP (404/500). Le piège response.ok.
🚫 CORS (Cross-Origin)
Le "cauchemar". Same-Origin Policy. Access-Control-Allow-Origin. Requêtes Preflight (OPTIONS).
Concepts Avancés
Annuler (AbortController), Debouncing (barre de recherche), Throttling, Polling.
🐍 Intégration Django
Le mur du CSRF Token. JsonResponse, json.loads(request.body), X-CSRFToken.
Le Problème : Le Web Synchrone (Bloquant)
Avant AJAX (début des années 2000), pour obtenir 1 seule nouvelle donnée (ex: la météo), le navigateur devait recharger la page entière. L'utilisateur voyait un écran blanc, le serveur devait tout recalculer (header, footer, menus...). C'était lent et consommateur de ressources.
Le JavaScript "Synchrone" est également "bloquant". Le thread principal du navigateur ne fait qu'une chose à la fois. Si une tâche est longue, l'UI est gelée.
// 1. Le bouton est cliqué
button.addEventListener('click', () => {
// 2. Tâche *bloquante* (l'UI est GELÉE)
alert("Vous ne pouvez rien faire...");
// 3. (Seulement après 5s...) L'UI est libérée
console.log("Fin");
});
La Solution : Le Web Asynchrone (Non-Bloquant)
AJAX (Asynchronous JavaScript and XML) est une *technique* (pas un langage) qui permet au JavaScript de faire une requête HTTP au serveur *en arrière-plan*, sans geler l'UI et sans recharger la page.
Le navigateur "délègue" la tâche (ex: fetch) et continue sa vie (l'UI reste fluide). Quand le serveur répond, il exécute une fonction "callback" (ou résout une Promise).
// 1. Le bouton est cliqué
button.addEventListener('click', () => {
// 2. Tâche *non-bloquante* (déléguée)
// L'UI reste 100% réactive
fetch('/api/data')
.then(res => res.json())
.then(data => {
// 4. (Bien plus tard) Ce code s'exécute
console.log(data);
});
// 3. Ce code s'exécute IMMÉDIATEMENT
console.log("Requête lancée, l'UI est fluide.");
});
Le "X" (XML) vs "J" (JSON)
Le nom est historique. Le "X" signifie XML. À l'époque, les données étaient échangées en XML (verbeux). Aujourd'hui, 99% des requêtes AJAX utilisent JSON, car il est léger et se "mappe" parfaitement avec les objets JavaScript (JSON.parse, JSON.stringify).
C'est l'API d'origine (créée par Microsoft !), disponible dans tous les navigateurs. Elle est basée sur les "événements" (events). Elle est verbeuse et complexe, mais essentielle à connaître pour maintenir du vieux code (ou pour gérer des uploads avec progression, ce que fetch fait mal).
Exemple de requête GET
function loadData() {
// 1. Créer l'objet
const xhr = new XMLHttpRequest();
// 2. Configurer la requête (Méthode, URL, Asynchrone=true)
xhr.open('GET', 'https://api.example.com/data', true);
// 3. Définir le "listener" (ce qui se passe à chaque étape)
xhr.onreadystatechange = function() {
// 4. Vérifier si la requête est terminée (4) ET réussie (200)
if (xhr.readyState === 4 && xhr.status === 200) {
// 5. Traiter la réponse (c'est du texte)
const data = JSON.parse(xhr.responseText);
console.log(data);
}
// Gérer les autres cas (erreurs)
else if (xhr.readyState === 4) {
console.error("Erreur HTTP: " + xhr.status);
}
};
// (Alternative plus simple que .onreadystatechange)
// xhr.onload = function() {
// if (xhr.status === 200) { /* ... */ }
// };
// 6. Envoyer la requête
xhr.send();
}
readyState (L'état de la requête)
La propriété qui indique où en est la requête.
| État | Valeur | Description |
|---|---|---|
| 0 | UNSENT | .open() n'a pas été appelé. |
| 1 | OPENED | .open() a été appelé. |
| 2 | HEADERS_RECEIVED | Les en-têtes de réponse ont été reçus. |
| 3 | LOADING | La réponse (responseText) est en cours de réception. |
| 4 | DONE | La requête est terminée. |
status (Le code de réponse HTTP)
Le statut HTTP de la réponse (disponible à readyState 3 ou 4).
| Code | Description |
|---|---|
| 200 | OK (Réussite) |
| 201 | Created (ex: POST réussi) |
| 304 | Not Modified (Cache) |
| 400 | Bad Request (Erreur client) |
| 401 | Unauthorized (Authentification requise) |
| 403 | Forbidden (Droits insuffisants) |
| 404 | Not Found (Ressource non trouvée) |
| 500 | Internal Server Error (Erreur serveur) |
fetch()L'API fetch() (native dans les navigateurs) remplace XHR. Elle est basée sur les Promises, ce qui la rend beaucoup plus propre et composable. (async/await est du sucre syntaxique au-dessus de fetch).
Exemple (GET)
Le "double .then()" est le piège classique. Le premier .then() reçoit la *réponse* (statut, headers), le second .then() reçoit le *corps* parsé.
fetch('https://api.example.com/data')
// 1. La Promise est résolue avec un objet "Response"
.then(response => {
// 2. ON DOIT VÉRIFIER LES ERREURS HTTP MANUELLEMENT
if (!response.ok) { // response.ok = true si status 200-299
throw new Error("Erreur HTTP: " + response.status);
}
// 3. .json() retourne une *autre* Promise
return response.json();
})
// 4. La 2ème Promise est résolue avec les données
.then(data => {
console.log(data);
})
// 5. Gère les erreurs RÉSEAU (ou le 'throw' ci-dessus)
.catch(error => {
console.error("Erreur Fetch:", error);
});
Exemple (POST avec JSON)
Pour envoyer des données, on passe un 2ème argument (objet options) à fetch.
const userData = {
username: "alice",
role: "admin"
};
fetch('https://api.example.com/users', {
// 1. Méthode HTTP
method: 'POST',
// 2. Headers (Comment on envoie)
headers: {
'Content-Type': 'application/json',
// (On ajoutera 'X-CSRFToken' ici pour Django)
},
// 3. Corps de la requête (converti en string)
body: JSON.stringify(userData)
})
.then(response => response.json())
.then(data => {
console.log("Réponse du serveur:", data);
})
.catch(error => {
console.error("Erreur:", error);
});
async/awaitasync/await (ES2017) est du "sucre syntaxique" au-dessus des Promises. Il ne change pas le fonctionnement de fetch, mais il permet d'écrire du code asynchrone qui se lit comme du code synchrone (bloquant), en "mettant en pause" la fonction (pas le navigateur !). La gestion des erreurs se fait obligatoirement avec try...catch.
Exemple (GET)
Notez comme la logique est plate, sans "pyramide" .then().
// 1. La fonction DOIT être 'async'
async function getData() {
// 2. 'try...catch' est obligatoire
try {
// 3. 'await' met en pause la fonction, pas le navigateur
const response = await fetch('https://api.example.com/data');
// 4. On vérifie toujours .ok !
if (!response.ok) {
throw new Error("Erreur HTTP: " + response.status);
}
// 5. 'await' pour la 2ème Promise (.json())
const data = await response.json();
console.log(data);
return data;
} catch (error) {
// 6. Gère les erreurs Réseau ET le 'throw'
console.error("Erreur:", error);
}
}
Exemple (POST avec JSON)
La logique reste identique.
async function postData(userData) {
try {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log("Réponse:", data);
} catch (error) {
console.error("Impossible de poster:", error);
}
}
Le "payload" est la donnée qu'on envoie (le body). Le type doit être spécifié dans le header Content-Type pour que le serveur sache comment l'interpréter.
application/json
Utilisé 99% du temps pour envoyer des objets de données. Le serveur (ex: Django) s'attend à recevoir une chaîne de caractères JSON.
const data = { id: 1, name: "Alice" };
fetch(url, {
method: 'POST',
// 1. Dire au serveur: "C'est du JSON"
headers: { 'Content-Type': 'application/json' },
// 2. Convertir l'objet JS en string JSON
body: JSON.stringify(data)
});
// Côté serveur (Django/Python):
// import json
// data = json.loads(request.body)
multipart/form-data (via FormData)
Utilisé pour envoyer des formulaires qui contiennent des fichiers (<input type="file">) ou des données binaires. C'est l'équivalent JS d'un formulaire HTML classique.
const myForm = document.querySelector('#myForm');
const myFile = document.querySelector('#myFileInput');
const formData = new FormData(); // Pas besoin de myForm, on peut le créer vide
// 1. Ajouter des champs (clé, valeur)
formData.append('username', 'alice');
formData.append('userFile', myFile.files[0]); // Ajoute le fichier
fetch(url, {
method: 'POST',
body: formData
// 2. PRO-TIP: NE PAS METTRE 'Content-Type' ICI !
// Le navigateur doit l'ajouter lui-même
// (pour inclure le "boundary" du multipart)
});
// Côté serveur (Django):
// request.POST.get('username')
// request.FILES.get('userFile')
text/plain, text/html, application/xml
Moins courants pour l'envoi, mais fréquents en réception.
text/html: Utile pour récupérer un morceau de page HTML (ex: HTMX, un "partial") et l'insérer dans un<div>. On utiliseresponse.text().application/xml: L'ancêtre. Utilisé par les vieilles API (SOAP). On utiliseresponse.text()puis un parseur XML.text/plain: Simple texte.
// Récupérer un template HTML
async function getPartial() {
const response = await fetch('/mon-template-partial/');
const html = await response.text(); // Pas .json() !
document.querySelector('#container').innerHTML = html;
}
C'est le point le plus important. Un fetch() ne "rejette" (.catch()) QUE s'il y a une erreur réseau. Un 404 ou 500 n'est *pas* une erreur réseau, c'est une réponse HTTP valide.
Erreur Réseau (.catch())
La Promise fetch() est rejetée. Le .catch() (ou try...catch) est activé.
Causes :
- CORS : Le serveur n'autorise pas la requête (le cas le plus fréquent).
- Réseau : Pas de Wi-Fi, DNS introuvable, serveur éteint.
- Annulation : La requête a été annulée (ex:
AbortController).
try {
// Le serveur 'non-existent-domain.xxx' n'existe pas
await fetch('https://non-existent-domain.xxx');
} catch (error) {
// On entre ICI
console.error(error); // TypeError: Failed to fetch
}
Erreur HTTP (response.ok)
La Promise fetch() est réalisée (fulfilled) ! Le .catch() n'est PAS activé.
Causes :
404 Not Found(L'URL est fausse)403 Forbidden(Pas les droits)500 Internal Server Error(Le serveur a planté)
On DOIT vérifier response.ok (qui vaut true si status est 200-299).
Le "Golden Pattern"
async function getData() {
try {
const response = await fetch('/api/url-valide-mais-erreur-500/');
// 1. Gérer l'erreur HTTP
if (!response.ok) {
// Tenter de lire l'erreur JSON du serveur (si elle existe)
const errorData = await response.json().catch(() => null);
const message = errorData?.detail || `Erreur HTTP ${response.status}`;
throw new Error(message);
}
const data = await response.json();
console.log(data);
} catch (error) {
// 2. Gérer l'erreur Réseau OU le 'throw' ci-dessus
console.error("Échec de l'opération:", error.message);
}
}
Le Problème : Same-Origin Policy (SOP)
Par défaut, pour des raisons de sécurité, les navigateurs interdisent à un script (ex: https://mon-site.com) de faire un fetch vers une "Origine" différente (ex: https://api-externe.com).
Une "Origine" = Protocole + Domaine + Port.
CORS (Cross-Origin Resource Sharing) est le mécanisme qui permet au *serveur* (api-externe.com) de dire au navigateur : "C'est bon, j'autorise mon-site.com à me parler."
L'erreur fatale : ...has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present...
SOLUTION : C'est TOUJOURS au serveur (backend) de corriger. Le frontend ne peut rien faire (sauf proxy).
1. Requête Simple (ex: GET)
Le navigateur envoie la requête GET.
Le serveur doit répondre en incluant cet en-tête :
HTTP/1.1 200 OK Content-Type: application/json Access-Control-Allow-Origin: https://mon-site.com (ou: Access-Control-Allow-Origin: *)
2. Requête "Preflight" (ex: POST, PUT, DELETE)
Pour les requêtes "complexes" (POST, PUT, ou avec Content-Type: application/json), le navigateur envoie d'abord une "demande de permission" : la requête OPTIONS (Preflight).
Étape A : Le navigateur demande (Preflight)
OPTIONS /api/users Host: api-externe.com Origin: https://mon-site.com Access-Control-Request-Method: POST Access-Control-Request-Headers: Content-Type
Étape B : Le serveur autorise
HTTP/1.1 204 No Content Access-Control-Allow-Origin: https://mon-site.com Access-Control-Allow-Methods: POST, GET, OPTIONS Access-Control-Allow-Headers: Content-Type
Si cette réponse est OK, le navigateur envoie *automatiquement* la vraie requête POST.
Annuler un fetch (AbortController)
Permet d'annuler une requête en cours. Essentiel pour les barres de recherche (annuler la requête précédente quand l'utilisateur tape une nouvelle lettre).
let controller = new AbortController();
async function search(query) {
// 1. Annuler la requête en cours
controller.abort();
// 2. Créer un nouveau contrôleur
controller = new AbortController();
const signal = controller.signal;
try {
// 3. Passer le 'signal' au fetch
const res = await fetch(`/api/search?q=${query}`, { signal });
const data = await res.json();
// ...
} catch (err) {
if (err.name === 'AbortError') {
console.log('Recherche annulée.');
} else {
console.error('Erreur:', err);
}
}
}
Debouncing (Barre de recherche)
Ne pas lancer la requête à *chaque* touche. Attendre que l'utilisateur ait *fini* de taper (ex: 300ms après la dernière touche).
let debounceTimer;
input.addEventListener('input', (e) => {
// 1. Vider le timer précédent
clearTimeout(debounceTimer);
const query = e.target.value;
// 2. Lancer un nouveau timer
debounceTimer = setTimeout(() => {
// 3. Exécuter le fetch seulement si 300ms se sont écoulées
fetch(`/api/search?q=${query}`);
}, 300);
});
Polling (Sondage)
Interroger le serveur à intervalle régulier pour voir s'il y a de nouvelles données. C'est l'ancêtre des WebSockets, mais encore très utilisé pour des notifications simples.
async function checkMessages() {
try {
const res = await fetch('/api/new-messages');
const data = await res.json();
if (data.new) {
// ... afficher notif ...
}
} catch (e) {
console.error('Erreur Polling');
}
}
// 1. Lancer la vérification toutes les 30 secondes
setInterval(checkMessages, 30000);
// 2. Lancer immédiatement au chargement
checkMessages();
L'intégration d'AJAX (POST) avec Django se heurte toujours à une erreur 403 Forbidden à cause du CSRF Token (Cross-Site Request Forgery). C'est une protection, pas un bug. Voici comment la gérer.
Côté JavaScript (Frontend)
Le token CSRF est dans un cookie (csrftoken) ou dans le template ({{ csrf_token }}). Le plus simple est de le lire depuis le cookie et de l'ajouter à *chaque* fetch POST.
Snippet N°1 : Récupérer le Cookie CSRF
(Ce code est à inclure dans votre JS global)
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
const csrftoken = getCookie('csrftoken');
Snippet N°2 : Envoyer le fetch avec le Token
async function postToDjango(data) {
const response = await fetch('/api/ma-vue/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// 1. AJOUTER LE TOKEN ICI
'X-CSRFToken': csrftoken
},
body: JSON.stringify(data)
});
return response.json();
}
Côté Django (Backend - views.py)
La vue doit gérer le JSON. request.POST ne fonctionne pas avec application/json (il ne marche qu'avec FormData ou x-www-form-urlencoded).
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
import json
# @require_POST # S'assurer que c'est bien du POST
def ma_vue_json(request):
# 1. Ne s'attendre qu'à du POST
if request.method == 'POST':
try:
# 2. Lire le JSON depuis request.body
data = json.loads(request.body)
# 3. Traiter les données
username = data.get('username')
if not username:
return JsonResponse({'status': 'erreur', 'message': 'Username manquant'}, status=400)
# ... faire quelque chose avec 'username' ...
# 4. Renvoyer une réponse JSON
return JsonResponse({'status': 'succès', 'message': f'Utilisateur {username} traité'})
except json.JSONDecodeError:
return JsonResponse({'status': 'erreur', 'message': 'JSON invalide'}, status=400)
# 5. Rejeter les autres méthodes
return JsonResponse({'status': 'erreur', 'message': 'Méthode GET non autorisée'}, status=405)
# Note: csrf_exempt désactive le token. À N'UTILISER QU'EN DÉSESPOIR DE CAUSE
# ou pour des API publiques.
