Skip to content

5. Chapitre 4 - Introduction à [Vue.js]

5.1. Sources

Ce chapitre prend appui sur la documentation officielle de [Vue.js] : vuejs.org, et en particulier son guide (vuejs.org/guide/introduction.html).

5.2. D’AngularJS 1.x à [Vue.js]

Le document original (2014) utilisait AngularJS 1.x : un framework JavaScript (pas encore TypeScript) organisé autour de $scope, de contrôleurs, de directives (ng-repeat, ng-if…) et d’un mécanisme de détection des changements fondé sur la « boucle de digestion » ($digest) et la surveillance de variables ($watch).

[Vue.js] (créé par Evan You en 2014, après une expérience chez Google autour d’AngularJS) est, comme [Angular], un framework complet : contrairement à [React] (une simple bibliothèque d’interface), il fournit lui-même un système de composants, une gestion de l’état réactif, et - via ses paquets officiels séparés, non utilisés dans ce document - un routeur ([vue-router]) et un gestionnaire d’état global ([Pinia]). [Vue.js] se décrit lui-même comme « progressif » : un cœur centré sur la couche vue (proche, en esprit, du $scope/template d’AngularJS 1.x), complété d’un format de composant qui lui est propre - les fichiers [.vue] (cf. plus loin). Ce chapitre présente ces notions une par une, avec de petits exemples indépendants de l’étude de cas - le chapitre suivant les retrouvera à l’œuvre dans le client [RdvMedecins] complet, en [Vue.js] 3.

5.3. Le principe d’une application [Vue.js] : le « Single Page Application »

Une application [Vue.js] est, comme une application [Angular] ou [React], une application monopage (« Single Page Application », SPA) : le serveur ne délivre au navigateur qu’une seule page HTML, contenant un unique fichier JavaScript qui embarque toute la logique de l’application. Une fois cette page chargée, il n’y a plus jamais de rechargement de page complet : chaque interaction de l’utilisateur (changer de vue, envoyer un formulaire…) est traitée par le code JavaScript déjà présent dans le navigateur, qui met à jour l’affichage et dialogue avec le serveur en arrière-plan via des requêtes HTTP asynchrones (le plus souvent en JSON). C’était déjà le principe du document original avec AngularJS 1.x, et des variantes [Angular] et [React] de ce cours - il n’a pas changé : seul le framework qui gère l’affichage diffère d’une variante à l’autre.

5.4. Les composants : fichiers [.vue] et le [Composition API]

Comme en [Angular] et en [React], un composant est l’unité de base d’une application [Vue.js] : il produit ce qui doit s’afficher à l’écran, à partir de l’état qu’il détient et des propriétés qu’il reçoit. La différence tient à la forme : un composant [Angular] est une classe décorée, associée à un template HTML séparé (ou inline) ; un composant [React] est une fonction qui retourne du [JSX] ; un composant [Vue.js] est un fichier « single-file » [.vue], qui réunit trois blocs bien distincts dans un seul fichier : un [<template>] (le HTML, proche de celui d’[Angular]), un [<script setup>] (la logique, en TypeScript) et, en option, un [<style>] (les styles propres à ce composant).

Voici un tout petit composant, indépendant de l’étude de cas, équivalent au [CompteurComponent] ([Angular]) et au [Compteur.tsx] ([React]) déjà présentés dans ce cours :

<!-- Compteur.vue -->
<script setup lang="ts">
import { ref } from 'vue';

const valeur = ref(0);

function incrementer(): void {
  valeur.value++;
}
</script>

<template>
  <p>Valeur : {{ valeur }}</p>
  <button @click="incrementer">+1</button>
</template>

Commentons ce code :

  • ligne 3 : [import { ref } from 'vue';][ref] est la fonction [Vue.js] qui crée une valeur réactive : un rôle comparable à celui d’un signal côté [Angular], ou de [useState] côté [React] ;
  • ligne 5 : [const valeur = ref(0);][ref(0)] crée une « boîte » réactive initialisée à 0. Contrairement à [useState]() ([React]), qui renvoie une PAIRE (la valeur et un setter), [ref()] renvoie un SEUL objet, dont la valeur se lit et s’écrit via sa propriété [.value] - un mécanisme plus proche, en esprit, d’un [signal()] [Angular] (qu’on lit avec [monSignal()] et qu’on écrit avec [monSignal.set(...)]) que d’un couple [valeur, setValeur] ;
  • ligne 8 : [valeur.value++;] — modifier [.value] met à jour la valeur ET redessine automatiquement tout ce qui en dépend dans le [<template>] - exactement comme [monSignal.update(v => v + 1)] côté [Angular] ; contrairement à [React] (où les valeurs d’état sont immuables), rien n’empêche ici d’incrémenter [.value] directement (++), sans passer par une fonction de mise à jour séparée ;
  • lignes 12-13 : [Valeur : {{ valeur }}] — le [<template>] EST le template, comme le HTML séparé d’[Angular] - contrairement au [JSX] [React], qui est du JavaScript et se trouve mélangé au code de la fonction composant. Les doubles accolades [{{ }}] y insèrent une expression - le même rôle que [{{ valeur() }}] côté [Angular] ([Vue.js] ne demande PAS les parenthèses d’appel ([valeur], pas [valeur()]) : dans un [<template>], une [ref] est automatiquement « déballée », il n’y a jamais besoin d’écrire [.value]).

5.5. L’état local : [ref]/[reactive] et les props

En AngularJS 1.x, [Angular] détectait qu’une donnée avait changé grâce à une boucle de vérification systématique (le $digest), remplacée dans la variante [Angular] de ce cours par les signaux (signal(), computed()). [React] retient une approche différente : un composant re-exécute entièrement sa fonction à chaque changement d’état. [Vue.js] retient une troisième approche, hybride : comme [Angular], il suit précisément QUELLES valeurs réactives un composant utilise (grâce au [<template>] compilé et au système de « proxy » de [ref]/[reactive]), et ne met à jour QUE les parties du DOM qui en dépendent réellement - sans jamais re-exécuter tout le [<script setup>], et sans le coût du DOM virtuel utilisé par [React] pour obtenir un résultat comparable.

Fonction
Rôle
ref(valeurInitiale)
crée un état local à UNE seule valeur ; se lit/écrit via [.value] dans le [<script>] (jamais dans le [<template>])
reactive(objetInitial)
crée un état local à PLUSIEURS propriétés (un objet) ; se lit/écrit directement, SANS [.value], propriété par propriété
computed(() => ...)
dérive une valeur à partir d’autres [ref]/[reactive], recalculée automatiquement seulement quand ses dépendances changent - l’équivalent direct de [computed()] côté [Angular], et de [useMemo]() côté [React]
watch(source, callback)
réexécute [callback] chaque fois que [source] change - l’équivalent de [useEffect](..., [dep]) côté [React], ou de [effect()] côté [Angular]

Deux mécanismes complètent [ref]/[reactive] pour faire communiquer un composant avec son parent - on les retrouvera constamment dans le client [RdvMedecins] (chapitre suivant), exactement à la place qu’occupaient [input()]/[output()] côté [Angular], et les props/callback props côté [React] :

  • les props ([defineProps<...>()]) : un composant [Vue.js] déclare la forme de ses entrées avec [defineProps], une fonction spéciale reconnue par le compilateur (elle n’a besoin d’aucun [import]) - l’équivalent direct de [input.required<Medecin[]>()] côté [Angular], et d’un objet props déstructuré côté [React] ;
  • les événements ([defineEmits<...>()]) : plutôt qu’une simple fonction reçue en prop (le choix de [React]), [Vue.js] distingue explicitement les événements qu’un composant émet - la fonction [emit(‘nomEvenement’, valeur)] qu’elle renvoie est l’équivalent direct de [rechercher.emit(valeur)] côté [Angular] ([output<...>()]).

5.6. Pas de modules : chaque fichier est déjà autonome

Une application AngularJS 1.x s’organisait en modules (angular.module(...)). [Angular] a d’abord repris cette idée sous une forme plus stricte (les NgModule), avant de proposer les composants standalone comme mode par défaut. Comme [React], [Vue.js] n’a jamais eu besoin de cette notion : un fichier [.vue] exporte implicitement son composant (le [<script setup>] EST l’export par défaut du fichier, sans même le mot-clé [export] à écrire), et un autre fichier l’utilise avec un simple [import] - les modules ECMAScript standard, sans aucune couche supplémentaire propre au framework :

<!-- Parent.vue -->
<script setup lang="ts">
import Compteur from './Compteur.vue';
</script>

<template>
  <div>
    <Compteur />
  </div>
</template>

C’est le style utilisé dans tout le client [RdvMedecins] (chapitre suivant) : aucun fichier *.module.ts, aucun décorateur [imports: [...]] à tenir à jour - une simplification identique à celle déjà observée côté [React], par rapport à AngularJS 1.x et à [Angular] avec NgModule.

5.7. Partager de la logique : les composables et [provide]/[inject]

Comme en [NestJS] (chapitre 2), le client [RdvMedecins] a besoin de logique partagée entre plusieurs composants : parler au serveur, connaître l’utilisateur connecté, changer de langue. [Angular] résout ce besoin avec des services injectables ([@Injectable]({ providedIn: ‘root’ }) + inject(...)) ; [React] combine des hooks personnalisés et le [Context]. [Vue.js] retient une combinaison analogue à celle de [React], mais avec un vocabulaire et un mécanisme qui lui sont propres :

  • un composable - une fonction dont le nom commence, par convention, par [use] (comme un hook [React]), qui peut elle-même appeler d’autres fonctions [Vue.js] ([ref], [inject]…) - regroupe une logique réutilisable, exactement comme une méthode de service côté [Angular] ;
  • le couple [provide]/[inject] partage une valeur (état + fonctions) à TOUT le sous-arbre de composants, sans avoir à la faire redescendre manuellement, prop par prop, à travers chaque niveau intermédiaire - le rôle que jouait [providedIn: ‘root’] côté [Angular], et le [Context] côté [React]. Contrairement à [React], qui a besoin d’un composant [Provider] à IMBRIQUER visuellement dans l’arbre JSX autour du composant racine, [Vue.js] fournit une valeur avec un simple appel de fonction, [app.provide(cle, valeur)], sur l’instance de l’application (cf. main.ts, chapitre suivant) - sans aucune imbrication de composants nécessaire.

Un petit exemple, indépendant de l’étude de cas, équivalent en esprit au [MonService] ([Angular]) et au [mon-contexte.tsx] ([React]) déjà présentés dans ce cours :

// mon-composable.ts
import { inject, reactive, type InjectionKey } from 'vue';

interface MonStore {
  saluer: () => string;
}

const MonStoreKey: InjectionKey<MonStore> = Symbol('MonStore');

export function creerMonStore(): MonStore {
  return reactive<MonStore>({
    saluer: () => 'Bonjour',
  });
}

export function useMonStore(): MonStore {
  const store = inject(MonStoreKey);
  if (!store) {
    throw new Error('useMonStore() doit être appelé après app.provide(MonStoreKey, ...)');
  }
  return store;
}

Commentons ce code :

  • ligne 8 : [const MonStoreKey: InjectionKey<MonStore> = Symbol('MonStore');] — la clé d’injection : un [Symbol] plutôt qu’une simple chaîne de caractères, pour éviter toute collision accidentelle avec une autre clé du même nom - [InjectionKey<T>] est un simple alias de [Symbol] côté type, qui permet à [inject()] de connaître automatiquement le type de la valeur renvoyée ;
  • ligne 10 : [export function creerMonStore(): MonStore {] — la fabrique, appelée UNE SEULE fois (dans [main.ts], cf. chapitre suivant), AVANT que le moindre composant existe - c’est pourquoi elle reçoit ses dépendances éventuelles en paramètres de fonction ordinaires, plutôt que par [inject()] ;
  • ligne 11 : [return reactive<MonStore>({][reactive(...)] enveloppe l’objet dans un proxy JavaScript : toute lecture d’une de ses propriétés dans un [<template>] enregistre automatiquement une dépendance ; toute écriture redessine tout ce qui en dépend - contrairement à [ref()], on lit et on écrit directement la propriété ([store.saluer]), sans [.value] ;
  • lignes 16-22 : [export function useMonStore(): MonStore { …] — le composable que les composants appellent ([const mon = useMonStore();]), exactement comme ils auraient appelé [inject(MonService)] côté [Angular], ou [useMonContexte()] côté [React] ; l’erreur levée si [inject(MonStoreKey)] renvoie [undefined] signale un oubli de câblage (le [app.provide(...)] correspondant n’a pas été appelé) - exactement le même filet de sécurité que côté [React] (Context valant null par défaut).

5.8. Communiquer avec le serveur : [fetch]

[HttpClient] ([Angular]) était l’évolution directe du service $http d’AngularJS 1.x utilisé par le document original. Comme [React], [Vue.js] n’est qu’un framework d’interface et ne fournit, dans son cœur, aucun client HTTP : le client [RdvMedecins] utilise directement [fetch], l’API standard du navigateur (disponible sans aucune dépendance supplémentaire) :

1
2
3
const reponseHttp = await fetch('http://localhost:8080/bonjour');
const reponse = (await reponseHttp.json()) as { message: string };
console.log(reponse.message);

Une différence importante avec $http et [HttpClient] : [fetch] renvoie directement une Promise (le mécanisme d’asynchronisme standard de JavaScript), pas un Observable - pas de bibliothèque RxJS, pas de [.subscribe]({ next, error }), pas de [.pipe(map(...))]. On peut l’utiliser avec [.then(...)], ou, plus lisible, avec async/await (comme ci-dessus). Une erreur réseau ou une réponse en erreur se traite alors avec un simple try/catch, plutôt qu’avec la branche error d’un abonnement RxJS - exactement comme côté [React].

Le composable [useRdvService()] du client [RdvMedecins] (chapitre suivant) encapsule systématiquement cet appel [fetch] suivi de [.json()], pour un motif précis expliqué à ce moment-là (l’enveloppe [Reponse]<T> commune à toutes les réponses du serveur).

5.9. Le rendu conditionnel et les listes : [v-if]/[v-else] et [v-for]

AngularJS 1.x utilisait des directives structurelles (ng-if, ng-repeat) pour afficher un élément conditionnellement ou répéter un bloc de template pour chaque élément d’une liste. La variante [Angular] de ce cours utilise la syntaxe intégrée [@if]/[@for] ; le client [React] exprime la même chose avec l’opérateur ternaire et [.map(...)], en JavaScript pur. [Vue.js] revient, sur ce point précis, plus près d’[Angular] : son [<template>] possède son propre « langage » de directives, très proche de celui d’AngularJS 1.x dans l’esprit (ng-if devient [v-if], ng-repeat devient [v-for]) :

1
2
3
4
5
6
<p v-if="utilisateurConnecte">Bienvenue !</p>
<p v-else>Veuillez vous connecter.</p>

<ul>
  <li v-for="item in listeItems" :key="item.id">{{ item.nom }}</li>
</ul>

Commentons ce code :

  • lignes 1-2 : [v-if="utilisateurConnecte" … v-else …][v-if]/[v-else] remplacent [@if]/[@else] ([Angular]) et ng-if (AngularJS 1.x) : seule la branche dont la condition est vraie est MONTÉE dans le DOM - exactement le même comportement que l’opérateur ternaire [React] ({ condition ? ... : ... }), mais exprimé comme un attribut HTML plutôt que comme une expression JavaScript imbriquée dans le [JSX] ;
  • ligne 5 : [v-for="item in listeItems" :key="item.id"][v-for] remplace [@for] ([Angular]) et ng-repeat (AngularJS 1.x), et joue exactement le même rôle que [.map(...)] côté [React] : il répète l’élément pour chaque entrée du tableau. [:key] (obligatoire, comme [key={item.id}] en [React] et track en [Angular]) indique à [Vue.js] comment reconnaître un élément déjà affiché lorsqu’il redessine la liste.

5.10. Récapitulatif : correspondances [Angular] -> [React] -> [Vue.js]

[Angular]/[React] (variantes précédentes de ce cours)
[Vue.js] (2026)

Composants standalone ([@Component]) / composants fonctionnels ([React])
Composants single-file ([.vue])

Signaux (signal(), input(), output(), computed()) / [useState](), props, [useMemo]() ([React])
[ref]()/[reactive](), [defineProps]/[defineEmits], [computed]()

effect() / [useEffect]() ([React])
[watch]()

Template séparé (.html) + décorateur (.ts) / [JSX] intégré (.tsx) ([React])
[<template>] + [<script setup>] intégrés au même fichier [.vue]

[@if], [@for] / opérateur ternaire, .map() ([React])
[v-if]/[v-else], [v-for]

Services injectables ([@Injectable]) + inject() / Hooks personnalisés + [Context] ([React])
Composables + [provide]/[inject]

[HttpClient] (Observables RxJS) / [fetch] (Promises) ([React])
[fetch] (Promises natives, comme [React])

[@angular/router] (table de routage vide) / Aucun routeur ([React])
Aucun routeur (même conclusion, poussée plus loin)

Angular CLI / [@angular/build] / [Vite] ([React])
[Vite] (avec [@vitejs/plugin-vue] plutôt que [@vitejs/plugin-react])

Ces notions suffisent pour aborder le client [RdvMedecins] du chapitre suivant : chaque nouvelle utilisation y sera malgré tout réexpliquée, au fil du code, à l’endroit précis où elle apparaît.