À propos de ce cours
Présentation du TP
- Chapitre : 19 – AJAX, fetch et les API web.
- Durée conseillée : 5 heures.
- Prérequis : les 6 leçons du chapitre 19, les promesses et
async/await(chapitre 18), le DOM et les événements, VS Code avec Live Server, un compte GitHub. - Sujet : réaliser une application « Météo des villes » qui interroge deux API publiques Open-Meteo (gratuites, sans clé) : la recherche de villes (géocodage) et les prévisions. L'utilisateur tape le nom d'une ville et choisit parmi des suggestions ; l'application affiche la météo actuelle, les prochaines 24 heures et les 7 prochains jours, en °C ou en °F.

Objectifs :
- Lire la documentation d'une API et construire des requêtes avec
URLSearchParams. - Écrire des fonctions
asyncqui utilisentfetch, vérifientreponse.oket gèrent toutes les erreurs. - Exploiter une réponse JSON complexe (objets, tableaux parallèles, unités).
- Gérer les états de l'interface : chargement, succès, erreur, aucun résultat.
- Annuler les requêtes inutiles et limiter le nombre d'appels (délai de saisie,
AbortController).
Partie 1 Préparer le projet
- Créez sur GitHub un dépôt public
tp19-meteo-fetchavec unREADME.md, clonez-le et ouvrez-le dans VS Code. - Créez cette arborescence :
tp19-meteo-fetch/
├── index.html
├── css/
│ └── style.css
└── js/
├── app.js ← programme principal (événements, enchaînement)
├── api.js ← toutes les requêtes fetch
├── affichage.js ← tout ce qui touche au DOM
└── codes.js ← traduction des codes météo
- Copiez la page
index.html(Bootstrap 5.3, chargée en module) :
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Météo – Application fetch</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.9/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<link href="css/style.css" rel="stylesheet" />
<script type="module" src="js/app.js"></script>
</head>
<body class="bg-body-tertiary">
<main class="container py-4" style="max-width: 820px">
<div class="d-flex justify-content-between align-items-center mb-3">
<h1 class="h3 m-0">⛅ Météo des villes</h1>
<div class="btn-group btn-group-sm" role="group" aria-label="Unité">
<button
class="btn btn-outline-primary active"
type="button"
data-unite="celsius"
aria-pressed="true"
>
°C
</button>
<button
class="btn btn-outline-primary"
type="button"
data-unite="fahrenheit"
aria-pressed="false"
>
°F
</button>
</div>
</div>
<form id="form-recherche" class="position-relative mb-2" role="search" autocomplete="off">
<div class="input-group">
<input
id="champ-ville"
class="form-control"
type="search"
placeholder="Rechercher une ville : Tunis, Sousse, Paris…"
aria-label="Nom de la ville"
aria-controls="suggestions"
required
minlength="2"
/>
<button class="btn btn-primary" type="submit">Rechercher</button>
</div>
<ul
id="suggestions"
class="list-group position-absolute w-100 shadow d-none"
role="listbox"
></ul>
</form>
<div id="villes-rapides" class="d-flex flex-wrap gap-2 mb-3">
<button class="btn btn-sm btn-outline-secondary" type="button" data-ville="Tunis">
Tunis
</button>
<button class="btn btn-sm btn-outline-secondary" type="button" data-ville="Sfax">
Sfax
</button>
<button class="btn btn-sm btn-outline-secondary" type="button" data-ville="Sousse">
Sousse
</button>
<button class="btn btn-sm btn-outline-secondary" type="button" data-ville="Djerba">
Djerba
</button>
<button class="btn btn-sm btn-outline-secondary" type="button" data-ville="Tozeur">
Tozeur
</button>
</div>
<div id="message" class="alert d-none" role="alert"></div>
<div id="chargement" class="text-center my-5 d-none">
<div class="spinner-border text-primary" role="status"></div>
<p class="mt-2 mb-0">Chargement de la météo…</p>
</div>
<section id="resultat" class="d-none" aria-live="polite">
<div class="card shadow-sm mb-3">
<div class="card-body d-flex flex-wrap justify-content-between align-items-center gap-3">
<div>
<h2 id="lieu" class="h4 mb-0"></h2>
<p id="maj" class="text-body-secondary small mb-2"></p>
<p class="display-4 fw-bold mb-0">
<span id="temperature"></span><span class="unite"></span>
</p>
<p id="description" class="lead mb-0"></p>
</div>
<div class="text-center">
<div id="icone" class="icone-actuelle" aria-hidden="true"></div>
<ul class="list-unstyled small mb-0 text-start">
<li>Ressenti : <b id="ressenti"></b></li>
<li>Humidité : <b id="humidite"></b></li>
<li>Vent : <b id="vent"></b></li>
</ul>
</div>
</div>
</div>
<h3 class="h6 text-uppercase text-body-secondary">Prochaines 24 heures</h3>
<div id="heures" class="bande-heures d-flex gap-2 mb-4 pb-2"></div>
<h3 class="h6 text-uppercase text-body-secondary">7 prochains jours</h3>
<ul id="jours" class="list-group mb-4"></ul>
</section>
<p class="text-center small text-body-secondary">
Données :
<a href="https://open-meteo.com/" target="_blank" rel="noopener">Open-Meteo.com</a>
</p>
</main>
</body>
</html>
- Copiez
css/style.css:
.icone-actuelle {
font-size: 4rem;
line-height: 1;
}
#suggestions {
z-index: 10;
top: 100%;
}
.bande-heures {
overflow-x: auto;
scroll-snap-type: x mandatory;
}
.heure {
flex: 0 0 76px;
scroll-snap-align: start;
text-align: center;
background: var(--bs-body-bg);
border: 1px solid var(--bs-border-color);
border-radius: 0.6rem;
padding: 0.4rem 0.2rem;
}
.heure .ico {
font-size: 1.5rem;
}
.jour {
display: grid;
grid-template-columns: 9.5rem 2rem 1fr auto;
align-items: center;
gap: 0.75rem;
}
.jour .ico {
font-size: 1.3rem;
}
- Copiez
js/codes.js, qui traduit les codes météo WMO d'Open-Meteo en texte et en icône :
// Codes météo WMO utilisés par Open-Meteo → texte et icône
const CODES = {
0: ["Ciel dégagé", "☀️"],
1: ["Plutôt dégagé", "🌤️"],
2: ["Partiellement nuageux", "⛅"],
3: ["Couvert", "☁️"],
45: ["Brouillard", "🌫️"],
48: ["Brouillard givrant", "🌫️"],
51: ["Bruine légère", "🌦️"],
53: ["Bruine", "🌦️"],
55: ["Bruine dense", "🌧️"],
61: ["Pluie faible", "🌦️"],
63: ["Pluie modérée", "🌧️"],
65: ["Pluie forte", "🌧️"],
71: ["Neige faible", "🌨️"],
73: ["Neige modérée", "🌨️"],
75: ["Neige forte", "❄️"],
80: ["Averses faibles", "🌦️"],
81: ["Averses", "🌧️"],
82: ["Averses violentes", "⛈️"],
95: ["Orage", "⛈️"],
96: ["Orage avec grêle", "⛈️"],
99: ["Orage avec forte grêle", "⛈️"],
};
export function decrireCode(code) {
const [texte, icone] = CODES[code] ?? ["Temps variable", "🌡️"];
return { texte, icone };
}
- Dans
app.js, écrivezconsole.log("TP 19 prêt");et lancez la page avec Live Server.
Commit : git add . puis git commit -m "Partie 1 : structure du projet" et git push.
Partie 2 Explorer les API et écrire getJson()
2.1 – Tester les deux adresses dans le navigateur et noter dans le README.md la structure de chaque réponse :
https://geocoding-api.open-meteo.com/v1/search?name=Sfax&count=5&language=fr&format=json
https://api.open-meteo.com/v1/forecast?latitude=34.74&longitude=10.76¤t=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m&hourly=temperature_2m,weather_code,precipitation_probability&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum&timezone=auto&forecast_days=7
- Que contient la réponse du géocodage pour
name=Zzzz? (Attention : il n'y a alors pas de cléresults. ) - Combien d'éléments contient
hourly.time? Où se trouve l'unité de température ?
2.2 – Dans js/api.js:
- Créez et exportez une classe
ErreurApiqui hérite deError, avec une propriétéstatut. Elle portera un message compréhensible par l'utilisateur. - Écrivez et exportez
getJson(url, signalAnnulation)qui :- appelle
fetchavec un délai maximal de 8 secondes (AbortSignal.timeout(8000)), combiné si besoin au signal d'annulation reçu avecAbortSignal.any([...]); - relance telle quelle une erreur
AbortError(annulation volontaire) ; - transforme une
TimeoutErrorenErreurApi("Le service météo met trop de temps à répondre. Réessayez."); - transforme toute autre erreur de
fetchenErreurApi("Impossible de joindre le service : vérifiez votre connexion Internet."); - si
reponse.okest faux, lèveErreurApi("Le service a répondu avec l'erreur 500.", 500); - sinon renvoie
reponse.json().
- appelle
Testez dans la console du navigateur : const { getJson } = await import("./js/api.js"); await getJson("https://api.open-meteo.com/v1/forecast?latitude=999&longitude=0"); doit lever une ErreurApi avec le statut 400.
Commit : "Partie 2 : getJson et gestion des erreurs".
Partie 3 Chercher une ville et afficher la météo actuelle
- Dans
api.js, écrivezrechercherVilles(nom, nombre = 5, signal). Elle construit l'URL du géocodage avecURLSearchParams(name,count,language: "fr",format: "json") et renvoie un tableau d'objets simples{ nom, region, pays, latitude, longitude }, ou un tableau vide (donnees.results ?? []). - Écrivez
chargerMeteo(ville, unite = "celsius"), qui renvoie la réponse complète des prévisions (paramètres de la partie 2.1, plustemperature_unit). - Dans
affichage.js, écrivezafficherMeteo(meteo, ville)qui remplit la carte : lieu (« Sfax, Tunisie »), heure de mise à jour (Intl.DateTimeFormat), température arrondie, unité lue dansmeteo.current_units.temperature_2m, description et icône (decrireCode()), ressenti, humidité et vent. Utilisez uniquementtextContent. - Dans
app.js, à l'envoi du formulaire : recherchez la ville (1 résultat), puis affichez sa météo. Si aucune ville n'est trouvée, affichez dans#messageune alerte jaune : « Aucune ville trouvée pour « Zzzz ». ». - Au démarrage, affichez directement la météo de Tunis.
Commit : "Partie 3 : recherche de ville et météo actuelle".
Partie 4 Les prévisions : 24 heures et 7 jours
4.1 – Les prochaines 24 heures. hourly.time contient 168 heures (7 × 24) à partir de minuit. Il faut commencer à l'heure actuelle :
// "2026-11-12T21:45" → "2026-11-12T21:00"
const heureActuelle = meteo.current.time.slice(0, 13) + ":00";
// les dates ISO se comparent correctement comme des textes
const debut = Math.max(0, heures.time.findIndex((heure) => heure >= heureActuelle));
const prochaines = heures.time.slice(debut, debut + 24);
- Pour chaque heure, créez une petite carte
.heure: l'heure (« Maint. » pour la première, puis « 22 h »…), l'icône, la température, la probabilité de pluie (« 💧 30 % »). Attention :heures.temperature_2m[i]doit utiliser l'indice dans le tableau complet (debut + rang). - La bande défile horizontalement sur téléphone (déjà prévu dans le CSS).
4.2 – Les 7 jours. Pour chaque jour de daily.time, une ligne de liste .jour: nom du jour (« Aujourd'hui », puis « Vendredi 13 nov. »), icône, description (avec les précipitations si elles sont supérieures à 0 : « Pluie faible · 4.2 mm »), températures max / min.
new Date("2026-11-13") est interprété à minuit UTC et peut afficher la veille. Lisez la date à midi : new Date(`${date}T12:00`). Mettez la première lettre en majuscule : texte.charAt(0).toUpperCase() + texte.slice(1). Commit : "Partie 4 : prévisions 24 h et 7 jours".
Partie 5 Chargement et erreurs
- Écrivez
afficherChargement(actif)(affiche ou masque#chargementet masque l'ancien message) etafficherMessage(texte, type). - Dans
app.js, la fonctionmontrerVille(ville)suit ce schéma :
async function montrerVille(ville) {
afficherChargement(true);
try {
const meteo = await chargerMeteo(ville, unite);
afficherMeteo(meteo, ville);
} catch (erreur) {
masquerResultat();
afficherMessage(erreur instanceof ErreurApi ? erreur.message : "Une erreur inattendue est survenue.");
console.error(erreur); // le détail technique reste pour le développeur
} finally {
afficherChargement(false); // exécuté dans tous les cas
}
}
Tests (DevTools → Réseau) :
- Offline: message « Impossible de joindre le service… » ;
- Slow 3G: l'indicateur de chargement est visible ;
- clic droit sur la requête de prévision → Block request URL, puis nouvelle recherche : message clair, aucune erreur rouge non gérée ;
- remplacez temporairement
latitudepar999: message « … erreur 400 ».
Commit : "Partie 5 : états de chargement et d'erreur".
Partie 6 Suggestions pendant la saisie
Quand l'utilisateur tape au moins 2 lettres, une liste de 5 villes possibles apparaît sous le champ (#suggestions) ; un clic (ou Entrée) sur une suggestion affiche sa météo.
- Ne pas envoyer une requête par lettre tapée : à chaque événement
input, annulez la minuterie précédente (clearTimeout) et programmez la recherche 300 ms plus tard (setTimeout). Ainsi, taper « Sousse » rapidement n'envoie qu'une requête. - Annuler la requête devenue inutile : gardez un
AbortControllerdans une variable ; avant chaque nouvelle recherche, appelezcontroleur?.abort(), puis créez-en un nouveau et passez sonsignalàrechercherVilles(). Ignorez les erreursAbortError. - Affichez chaque suggestion sous la forme « Sousse, Sousse, Tunisie » (
[nom, region, pays].filter(Boolean).join(", ")), avecrole="option"ettabIndex = 0pour l'accessibilité. - Masquez la liste : après un choix, sur la touche Échap, et sur un clic en dehors du formulaire.
Vérification : onglet Réseau, filtre Fetch/XHR. Tapez rapidement « Sousse » : une seule requête de géocodage. Tapez lentement avec Slow 3G: les requêtes précédentes apparaissent (canceled).
Commit : "Partie 6 : suggestions avec délai et annulation".
Partie 7 Villes rapides et unité °C / °F
- Villes rapides : les boutons Tunis, Sfax, Sousse, Djerba et Tozeur affichent directement la météo de la ville (un seul écouteur sur
#villes-rapides, délégation avecdata-ville). - Unité : les deux boutons °C / °F changent la variable
unite("celsius"ou"fahrenheit"), mettent à jour les classesactiveetaria-pressed, puis redemandent la météo de la ville affichée avec le paramètretemperature_unit. L'unité affichée vient de la réponse (current_units) : aucune conversion à faire vous-même. - Mettez à jour le titre de l'onglet :
document.title = `Météo – ${ville.nom}`.
Commit : "Partie 7 : villes rapides et unités".
Partie 8 Recette, publication et README
8.1 – Recette. Recopiez ce tableau dans le README.md et complétez la colonne « Résultat » :
| N° | Scénario | Résultat attendu | Résultat |
|---|---|---|---|
| R1 | Ouverture de la page | Météo de Tunis affichée | |
| R2 | Recherche « sfax » + Entrée | « Sfax, Tunisie », 24 heures, 7 jours | |
| R3 | Saisie rapide « Sousse » | Une seule requête de géocodage, suggestion affichée | |
| R4 | Clic sur une suggestion | Météo de la ville, suggestions masquées | |
| R5 | Recherche « Zzzz » | Alerte « Aucune ville trouvée… » | |
| R6 | Bouton °F, puis °C | Températures et unités converties par l'API | |
| R7 | Mode Offline puis recherche | Message de connexion, pas d'erreur non gérée | |
| R8 | Erreur 400 provoquée | Message avec le code d'erreur | |
| R9 | Les 24 heures | Commencent à l'heure actuelle de la ville | |
| R10 | Les jours | Bons noms de jours (pas de décalage) | |
| R11 | Ville saisie : <img src=x onerror=alert(1)> | Aucune alerte (pas de XSS) | |
| R12 | Affichage sur téléphone (DevTools, mode appareil) | Tout est lisible, la bande des heures défile |
8.2 – Publication. Publiez le site avec GitHub Pages (Settings → Pages → Deploy from a branch → main / root) et testez-le sur votre téléphone.
8.3 – README. Présentation, lien du site, capture d'écran, les deux API utilisées et leurs paramètres, mention de la source des données (Open-Meteo, licence CC BY 4.0), tableau de recette.
Commit : "Partie 8 : recette, README et publication".
Bonus Pour aller plus loin
- Comparer deux villes : un mode « Comparer » charge la météo de deux villes en parallèle avec
Promise.allet les affiche côte à côte. - Navigation au clavier : les flèches haut et bas parcourent les suggestions, Entrée choisit.
- Ambiance : la couleur de fond change selon le temps (ensoleillé, nuageux, pluvieux) et le moment de la journée (paramètre
is_day). - Qualité de l'air : ajoutez l'indice de qualité de l'air avec l'API
air-quality-api.open-meteo.com(voir la documentation d'Open-Meteo). - Aperçu du chapitre 21 : un bouton « 📍 Ma position » utilise
navigator.geolocationau lieu de la recherche.
Livrables et barème
- Le lien du dépôt GitHub (au minimum 8 commits) et le lien du site publié.
- Le
README.mdavec les structures des réponses (partie 2.1) et le tableau de recette complété.
| Critère | Points |
|---|---|
getJson(): reponse.ok, délai maximal, erreurs traduites, ErreurApi (partie 2) | 3 |
Recherche de ville et météo actuelle, URLSearchParams, aucune ville (partie 3) | 3 |
| Prévisions 24 heures et 7 jours, tableaux parallèles, dates correctes (partie 4) | 3 |
États de chargement et d'erreur, try / catch / finally (partie 5) | 3 |
| Suggestions : délai de 300 ms, annulation, accessibilité (partie 6) | 3 |
| Villes rapides et unités par l'API (partie 7) | 2 |
Qualité : modules, aucun innerHTML avec des données, recette, README, commits réguliers, site publié | 3 |
| Total | 20 |
| Bonus | +2 |