Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
JavaScript

TP 19 : Application météo avec fetch et async/await

TP du chapitre 19 : réalisez une application météo avec les API gratuites Open-Meteo. Fonction getJson avec gestion de toutes les erreurs et délai maximal, recherche de ville par géocodage, météo actu...

0 chapitres 0 QCM Accès gratuit

À propos de ce cours

TP du chapitre 19 : réalisez une application météo avec les API gratuites Open-Meteo. Fonction getJson avec gestion de toutes les erreurs et délai maximal, recherche de ville par géocodage, météo actuelle, prévisions sur 24 heures et 7 jours, états de chargement et d'erreur, suggestions pendant la saisie avec délai et annulation des requêtes, villes rapides et unités °C/°F. Avec recette en 12 scénarios, publication sur GitHub Pages et barème.

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.

TP 19 : application météo avec recherche de ville, météo actuelle, prévisions sur 24 heures et 7 jours

Objectifs :

  • Lire la documentation d'une API et construire des requêtes avec URLSearchParams.
  • Écrire des fonctions async qui utilisent fetch, vérifient reponse.ok et 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).
Règle du TP : un commit à la fin de chaque partie (au minimum 8 commits), avec un message clair, et le site publié sur GitHub Pages à la fin.
Partie 1 Préparer le projet
  1. Créez sur GitHub un dépôt public tp19-meteo-fetch avec un README.md, clonez-le et ouvrez-le dans VS Code.
  2. 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
  1. 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>
  1. 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;
}
  1. 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 };
}
  1. Dans app.js, écrivez console.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&current=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:

  1. Créez et exportez une classe ErreurApi qui hérite de Error, avec une propriété statut. Elle portera un message compréhensible par l'utilisateur.
  2. Écrivez et exportez getJson(url, signalAnnulation) qui :
    • appelle fetch avec un délai maximal de 8 secondes (AbortSignal.timeout(8000)), combiné si besoin au signal d'annulation reçu avec AbortSignal.any([...]);
    • relance telle quelle une erreur AbortError (annulation volontaire) ;
    • transforme une TimeoutError en ErreurApi("Le service météo met trop de temps à répondre. Réessayez.");
    • transforme toute autre erreur de fetch en ErreurApi("Impossible de joindre le service : vérifiez votre connexion Internet.");
    • si reponse.ok est faux, lève ErreurApi("Le service a répondu avec l'erreur 500.", 500);
    • sinon renvoie reponse.json().

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
  1. Dans api.js, écrivez rechercherVilles(nom, nombre = 5, signal). Elle construit l'URL du géocodage avec URLSearchParams (name, count, language: "fr", format: "json") et renvoie un tableau d'objets simples { nom, region, pays, latitude, longitude }, ou un tableau vide (donnees.results ?? []).
  2. Écrivez chargerMeteo(ville, unite = "celsius"), qui renvoie la réponse complète des prévisions (paramètres de la partie 2.1, plus temperature_unit).
  3. Dans affichage.js, écrivez afficherMeteo(meteo, ville) qui remplit la carte : lieu (« Sfax, Tunisie »), heure de mise à jour (Intl.DateTimeFormat), température arrondie, unité lue dans meteo.current_units.temperature_2m, description et icône (decrireCode()), ressenti, humidité et vent. Utilisez uniquement textContent.
  4. 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 #message une alerte jaune : « Aucune ville trouvée pour « Zzzz ». ».
  5. 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.

Piège des fuseaux horaires : 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
  1. Écrivez afficherChargement(actif) (affiche ou masque #chargement et masque l'ancien message) et afficherMessage(texte, type).
  2. Dans app.js, la fonction montrerVille(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 latitude par 999: 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.

  1. 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.
  2. Annuler la requête devenue inutile : gardez un AbortController dans une variable ; avant chaque nouvelle recherche, appelez controleur?.abort(), puis créez-en un nouveau et passez son signal à rechercherVilles(). Ignorez les erreurs AbortError.
  3. Affichez chaque suggestion sous la forme « Sousse, Sousse, Tunisie » ([nom, region, pays].filter(Boolean).join(", ")), avec role="option" et tabIndex = 0 pour l'accessibilité.
  4. 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
  1. 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 avec data-ville).
  2. Unité : les deux boutons °C / °F changent la variable unite ("celsius" ou "fahrenheit"), mettent à jour les classes active et aria-pressed, puis redemandent la météo de la ville affichée avec le paramètre temperature_unit. L'unité affichée vient de la réponse (current_units) : aucune conversion à faire vous-même.
  3. 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énarioRésultat attenduRésultat
R1Ouverture de la pageMétéo de Tunis affichée
R2Recherche « sfax » + Entrée« Sfax, Tunisie », 24 heures, 7 jours
R3Saisie rapide « Sousse »Une seule requête de géocodage, suggestion affichée
R4Clic sur une suggestionMétéo de la ville, suggestions masquées
R5Recherche « Zzzz »Alerte « Aucune ville trouvée… »
R6Bouton °F, puis °CTempératures et unités converties par l'API
R7Mode Offline puis rechercheMessage de connexion, pas d'erreur non gérée
R8Erreur 400 provoquéeMessage avec le code d'erreur
R9Les 24 heuresCommencent à l'heure actuelle de la ville
R10Les joursBons noms de jours (pas de décalage)
R11Ville saisie : <img src=x onerror=alert(1)>Aucune alerte (pas de XSS)
R12Affichage 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.all et 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.geolocation au 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.md avec les structures des réponses (partie 2.1) et le tableau de recette complété.
CritèrePoints
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
Total20
Bonus+2
Conseil : gardez l'onglet Réseau des DevTools ouvert pendant tout le TP, filtré sur Fetch/XHR. Vous y voyez chaque requête, son adresse, son statut et sa réponse : c'est le meilleur outil pour comprendre ce que fait votre code.