Cours Vue.js · Module 4: Computed et watch

Projet : tableau de bord de mission avec Computed et Watch

5 min de lecture
Dans cette leçon3

Dans trois jours, NOVA LAB envoie un transport vers Valles Marineris. Aujourd'hui, les opérateurs calculent le temps d'arrivée sur papier, signalent les alarmes de température moteur par radio et rédigent à la main le rapport pour le commandement en fin de service. Dr Nova veut un seul panneau qui fasse tout cela tout seul : calculer les paramètres de vol, surveiller les systèmes critiques et rédiger le rapport.

Projet final du Centre de Calcul ! Construis un tableau de bord avancé de mission spatiale qui calcule les paramètres de vol en temps réel, surveille les systèmes critiques et génère automatiquement des rapports. Tu utiliseras les propriétés calculées pour les calculs automatiques et les watchers pour la surveillance réactive des changements.

Résumé du module

Dans ce module tu as appris :

  • Les propriétés calculées (computed) - calculs automatiques dépendant de données réactives. Mises en cache - elles ne recalculent que lorsqu'une dépendance change. Appelées sans parenthèses : {{ fuelStatus }}.
  • Les computed avec getter/setter - propriétés calculées bidirectionnelles. Le getter lit, le setter analyse et découpe les données. Utile pour la conversion d'unités et l'analyse de formats.
  • watch() - observation manuelle des changements avec un contrôle total. Source explicite, accès à oldValue/newValue, options immediate, deep, once. Watch sur plusieurs sources simultanément.
  • watchEffect() - suivi automatique des dépendances. Pas besoin de déclarer ce qu'il faut observer - Vue le détecte automatiquement. S'exécute immédiatement. Nettoyage via onCleanup.
  • Arrêter les watchers - chaque watcher retourne une fonction d'arrêt. Pause/reprise dans Vue 3.5+. Arrêt conditionnel dans le callback.

Ce qui fonctionne déjà dans le starter

Le starter sous la leçon n'est pas une page blanche. Il garde l'état de la mission dans des ref : le carburant, la vitesse, la température du moteur, la distance parcourue et la distance cible, le drapeau de surveillance et le journal. Deux computed calculent le temps d'arrivée estimé et le pourcentage du trajet accompli, et la computed inscriptible speedKmH convertit la vitesse entre km/s et km/h. Le watcher de température est créé dans la fonction startMonitoring, et stopMonitoring le ferme grâce à la poignée enregistrée. Un watchEffect ajoute au journal une alerte de carburant bas. Le starter utilise une échelle simplifiée, en degrés Celsius avec des seuils de 400 et 500. Les exigences ci-dessous décrivent la version cible en kelvins : adapte donc les seuils et ajoute les éléments manquants.

Exigences du projet

Construis un Mission Control Dashboard qui comprend :

1. Propriétés calculées pour les calculs de mission

Données d'entrée : vitesse (km/s), distance jusqu'à la cible (km), consommation de carburant (litres/s), capacité du réservoir (litres). Calcule automatiquement : temps d'arrivée estimé, consommation totale de carburant, pourcentage de carburant restant, statut de mission (NOMINAL/WARNING/CRITICAL).

Calcule le statut avec une seule computed, en testant les conditions de la plus grave à la moins grave : d'abord CRITICAL, puis WARNING, et NOMINAL en dernier. L'ordre des conditions décide quel état l'emporte. Le getter doit seulement calculer, donc aucune entrée de journal à l'intérieur.

2. Watch sur les paramètres critiques

Surveille la température du moteur (plage d'entrée 0-5000K). Quand elle dépasse 3500K, affiche un "WARNING". Quand elle dépasse 4500K, affiche "CRITICAL" et réduis automatiquement la vitesse. Enregistre l'historique des changements de température (10 dernières valeurs).

C'est un travail pour watch, car la réaction est une action : un changement de vitesse et une entrée dans l'historique. Tiens l'historique comme dans le calculateur de trajectoire, avec unshift et pop. Réduire la vitesse dans le callback ne provoquera pas de boucle, car cela modifie une autre source que la température observée.

3. watchEffect sur le statut du système

Génère automatiquement un rapport texte dans un panneau latéral qui se met à jour à chaque changement des paramètres de mission. Le rapport doit inclure la vitesse actuelle, la température, la consommation de carburant et le temps d'arrivée estimé.

watchEffect ne retourne rien, donc écris le rapport dans un ref séparé, par exemple reportText. Si le rapport devait seulement être affiché, une computed suffirait. Ici, on s'entraîne à écrire un effet, alors ajoute au rapport l'heure de sa génération : c'est déjà une action, pas une valeur pure.

4. Cleanup et contrôle des watchers

Ajoute un bouton "Monitoring ON/OFF" qui active/désactive le watcher de température. Quand la surveillance est désactivée, les changements de température ne génèrent pas d'alarmes. Utilise la fonction stop() et la recréation du watcher.

La clé, c'est la poignée retournée par watch. Garde-la dans une variable en dehors de la fonction et, à la désactivation, appelle-la puis remets-la à zéro, pour que la prochaine activation crée un watcher tout neuf.

1let stopTempWatch = null
2
3function startMonitoring() {
4  stopTempWatch = watch(engineTemp, (newTemp) => {
5    // alarms and history
6  })
7}
8
9function stopMonitoring() {
10  if (stopTempWatch) {
11    stopTempWatch()
12    stopTempWatch = null
13  }
14}

Après l'appel de stopTempWatch(), la température reste réactive et le panneau continue de l'afficher : seule l'écoute des alarmes s'éteint. Vue 3.5 propose aussi pause() et resume(), mais dans ce projet on s'entraîne au cycle complet : arrêter le watcher, puis le recréer.

Je te recommande de construire le panneau par étapes : d'abord les computed et la vérification des chiffres, puis les alarmes, et enfin le rapport et l'interrupteur. Dans le prochain module, dans le Simulateur de Contrôle, tu découvriras les événements et les formulaires, grâce auxquels l'équipage pourra piloter un tel panneau. Enrichis le starter ci-dessous !

Souviens-toi : un bon tableau de bord de mission calcule avec des computed, réagit avec des watch, et chaque canal d'écoute qu'il ouvre, il sait aussi le fermer.

Code de cette leçon : App.vue
1<script setup>
2import { ref, computed, watch, watchEffect } from 'vue'
3
4// Mission Control Dashboard - projet final du module 4
5const fuelLevel = ref(100)
6const speed = ref(0)
7const engineTemp = ref(200)
8const distance = ref(0)
9const targetDistance = ref(1000)
10const monitoringActive = ref(true)
11
12const logs = ref([])
13
14// Computed : temps d'arrivée estimé
15const estimatedTime = computed(() => {
16  if (speed.value <= 0) return 'N/A'
17  const remaining = targetDistance.value - distance.value
18  return (remaining / speed.value).toFixed(1) + ' h'
19})
20
21// Computed : pourcentage d'achèvement de la mission
22const missionProgress = computed(() => {
23  return Math.min(100, (distance.value / targetDistance.value * 100)).toFixed(1)
24})
25
26// Computed avec getter/setter : vitesse en km/s et km/h
27const speedKmH = computed({
28  get: () => speed.value * 3600,
29  set: (val) => { speed.value = val / 3600 }
30})
31
32// Watch sur la température du moteur
33let stopTempWatch = null
34function startMonitoring() {
35  monitoringActive.value = true
36  stopTempWatch = watch(engineTemp, (newTemp) => {
37    if (newTemp > 500) {
38      logs.value.unshift('ALARME : température critique !')
39    } else if (newTemp > 400) {
40      logs.value.unshift('Attention : température moteur élevée')
41    }
42  })
43}
44
45function stopMonitoring() {
46  if (stopTempWatch) {
47    stopTempWatch()
48    monitoringActive.value = false
49    logs.value.unshift('Surveillance arrêtée')
50  }
51}
52
53startMonitoring()
54
55// watchEffect : rapport automatique
56watchEffect(() => {
57  if (fuelLevel.value < 20) {
58    logs.value.unshift('RAPPORT : niveau de carburant bas !')
59  }
60})
61
62function accelerate() { speed.value = Math.min(100, speed.value + 5); engineTemp.value += 15 }
63function decelerate() { speed.value = Math.max(0, speed.value - 5); engineTemp.value -= 10 }
64function travel() { distance.value += speed.value * 10; fuelLevel.value = Math.max(0, fuelLevel.value - 2) }
65</script>
66
67<template>
68  <div class="dashboard">
69    <h1>Mission Control Dashboard</h1>
70
71    <div class="stats">
72      <div class="stat"><span>Carburant</span><strong>{{ fuelLevel }}%</strong></div>
73      <div class="stat"><span>Vitesse</span><strong>{{ speed.toFixed(1) }} km/s</strong></div>
74      <div class="stat"><span>Température</span><strong>{{ engineTemp }}°C</strong></div>
75      <div class="stat"><span>Distance</span><strong>{{ distance.toFixed(0) }} / {{ targetDistance }} km</strong></div>
76      <div class="stat"><span>Progression</span><strong>{{ missionProgress }}%</strong></div>
77      <div class="stat"><span>ETA</span><strong>{{ estimatedTime }}</strong></div>
78    </div>
79
80    <div class="controls">
81      <button @click="accelerate">Accélérer</button>
82      <button @click="decelerate">Décélérer</button>
83      <button @click="travel">Voyager (+10h)</button>
84      <button @click="monitoringActive ? stopMonitoring() : startMonitoring()">
85        {{ monitoringActive ? 'Désactiver la surveillance' : 'Activer la surveillance' }}
86      </button>
87    </div>
88
89    <div class="logs" v-if="logs.length">
90      <h3>Logs système</h3>
91      <p v-for="(log, i) in logs" :key="i">{{ log }}</p>
92    </div>
93  </div>
94</template>
95
96<style scoped>
97.dashboard { background: #0a0a2e; color: #e0e0ff; padding: 2rem; min-height: 100vh; font-family: monospace; }
98h1 { color: #00d4ff; text-align: center; }
99.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin: 1rem 0; }
100.stat { background: #1a1a4e; padding: 1rem; border-radius: 8px; text-align: center; }
101.stat span { display: block; color: #888; font-size: 0.8rem; }
102.stat strong { font-size: 1.2rem; color: #00d4ff; }
103.controls { display: flex; gap: 0.5rem; flex-wrap: wrap; justify-content: center; margin: 1rem 0; }
104button { background: #333; color: white; border: none; padding: 0.5rem 1rem; cursor: pointer; border-radius: 4px; }
105button:hover { background: #555; }
106.logs { background: #111; padding: 1rem; border-radius: 8px; max-height: 200px; overflow-y: auto; }
107.logs h3 { color: #ffaa00; }
108.logs p { color: #aaa; font-size: 0.85rem; margin: 0.2rem 0; }
109</style>

Tu as repéré une erreur dans cette leçon ?