Cours Vue.js · Module 1: Vue et Vite

Structure de l'application de mission

17 min de lecture
Dans cette leçon5

Dans la leçon précédente, tu as monté ton premier panneau vivant : des conteneurs ref(), des moustaches dans le template, un bouton branché sur @click et une classe qui suit l'état de la mission. Le panneau s'est réveillé - mais il s'est réveillé un peu par magie. Tu as saisi du code et l'image est simplement apparue à l'écran. Tu ne sais toujours pas qui l'a posée là, ni à quel instant.

Dans une station de recherche, ce "simplement" ne passe pas. L'ingénieur de quart doit savoir quel interrupteur allume le système, dans quel ordre les modules se réveillent et où chercher la panne quand l'écran du panneau reste noir. Aujourd'hui nous allons démonter la séquence d'allumage d'une application Vue en gestes séparés - et après cette leçon, plus aucun d'entre eux ne sera de la magie.

Ouvre le projet qu'a généré l'assistant de la leçon sur Vite. Il contient trois fichiers que tu n'as pas encore touchés une seule fois : index.html à la racine, src/main.js et src/App.vue. Aucun des trois n'affiche quoi que ce soit à lui seul. Ce n'est qu'à trois qu'ils forment la chaîne qui transforme ton code en image sur le moniteur du Centre de Contrôle.

Une mise au point d'intendance avant de démarrer. Si tu as accepté TypeScript dans l'assistant, le fichier de démarrage s'appelle main.ts ; sans TypeScript, il s'appelle main.js. C'est le même fichier, le même rôle et le même contenu, seule l'extension diffère. Dans cette leçon j'écris main.js, parce que c'est ainsi qu'il est nommé dans les exercices que tu recevras juste après.

index.html - la plateforme d'atterrissage

Commençons par le début de la chaîne, c'est-à-dire par ce que le navigateur reçoit en premier. Le navigateur ne sait pas ouvrir un fichier .vue - ce n'est pas un format qu'il connaît. Il sait faire une seule chose : charger une page HTML. C'est pourquoi chaque projet Vue possède exactement un fichier HTML, et c'est lui la toute première chose qui arrive à l'écran.

À l'intérieur, quatre éléments méritent ton attention. La balise <meta charset="UTF-8"> fixe l'encodage des caractères, pour que les accents français ne se transforment pas en charabia. La balise <meta name="viewport"> dit aux appareils mobiles de ne pas faire semblant d'avoir un écran large - sans cette ligne, le panneau affiché sur la tablette du module d'habitation ressemblera à une version rétrécie de la page de bureau. La balise <title> est l'inscription sur l'onglet du navigateur. Les deux dernières lignes sont pourtant les plus importantes : un <div> vide portant un attribut id de valeur app, et une balise <script> avec l'attribut type="module".

Ce <div> vide est la plateforme d'atterrissage. Vue ne collera pas l'interface n'importe où - il lui faut un élément unique, clairement désigné, dans lequel il a le droit de commander. L'attribut type="module" signifie de son côté que le fichier indiqué est un module ES, autrement dit qu'on a le droit d'y employer l'instruction import. Sans cet attribut, le navigateur s'arrêterait à la première ligne de main.js et signalerait une erreur de syntaxe.

1<!DOCTYPE html>
2<html lang="fr">
3  <head>
4    <meta charset="UTF-8" />
5    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6    <title>NOVA LAB - Mission Control</title>
7  </head>
8  <body>
9    <div id="app"></div>
10    <script type="module" src="/src/main.js"></script>
11  </body>
12</html>

Relis ce fichier et remarque ce qui ne s'y trouve pas. Pas une seule balise Vue, pas une paire de moustaches, aucun nom de composant. C'est du HTML ordinaire, exactement celui que tu écrivais dans le premier monde - Vue n'y change rien. Le <div> central est vide et le restera sur le disque ; le contenu n'y apparaîtra que dans le navigateur, à l'instant du démarrage de l'application.

La valeur app dans l'attribut id n'a rien de magique. C'est un nom ordinaire, que tu choisis toi-même, et qui pourrait tout aussi bien être control-room ou nova. Il doit en revanche correspondre à ce que tu indiqueras dans un instant dans main.js - et c'est l'une des sources les plus fréquentes d'écran noir, sur laquelle nous reviendrons à la fin de cette leçon.

main.js - la séquence d'allumage

Le navigateur est arrivé à la balise <script> et a réclamé le fichier /src/main.js. En mode développement, c'est Vite qui traite cette demande : il trouve le fichier, le fait passer au besoin par le compilateur et renvoie un module prêt à l'emploi. C'est le premier fragment de ton propre code qui s'exécute réellement - tout ce que tu verras ensuite à l'écran commence dans ces quelques lignes.

Le fichier de démarrage entier tient d'habitude en quatre ou cinq lignes. Nous allons les suivre une par une, car chacune fait exactement une chose.

Anatomie de la ligne import

Les deux premières lignes font venir les outils. L'instruction import a toujours la même forme et se compose de quatre parties placées dans un ordre fixe : le mot import, la liste des noms à faire venir, le mot from, et à la fin la source écrite entre guillemets. Retiens ce quatuor, car tu l'écriras dans chaque fichier Vue que tu créeras.

La différence entre les deux lignes tient aux accolades. La fonction createApp est l'un des nombreux outils fournis par le paquet vue, alors on met son nom entre accolades - c'est ce qu'on appelle un export nommé, et les accolades disent "je choisis exactement ceci dans ce paquet". Le fichier .vue est différent : il ne met à disposition qu'une seule et unique chose, son composant, et il le fait sous forme d'export par défaut. Un export par défaut se fait venir sans accolades, et c'est toi qui lui donnes son nom - App n'est qu'une convention admise, rien ne l'impose.

Regarde encore la source elle-même. L'écriture 'vue' sans barre oblique est un nom de paquet, que Vite ira chercher dans le dossier node_modules installé par npm install. L'écriture './App.vue' commence par un point suivi d'une barre oblique, c'est donc un chemin relatif : cherche à côté de moi, dans le même dossier. L'extension .vue s'écrit ici explicitement, sinon l'outil ne devinera pas de quel fichier il s'agit.

1import { createApp } from 'vue'
2import App from './App.vue'

Après ces deux lignes, rien n'a changé à l'écran, et rien ne pouvait changer. Un import n'est que l'embarquement des outils à bord : tu as maintenant sous la main la fonction createApp et le composant App, mais tu n'as encore utilisé ni l'une ni l'autre. Le fichier App.vue lui-même est resté intact - tu ne le modifies pas en le faisant venir.

createApp() - la naissance de l'instance

Les outils en main, tu peux appeler l'application à l'existence. C'est le travail de la fonction createApp, dont l'unique tâche consiste à renvoyer une instance d'application Vue - un seul objet, qui est le poste de commandement du système entier. C'est lui qui sait quel composant se tient au sommet, c'est lui qui accueillera les modules supplémentaires et c'est lui que tu prieras de démarrer.

Entre les parenthèses, tu donnes le composant racine, celui par lequel commence toute la hiérarchie. Chez nous, c'est App. Tous les autres composants de la station pendront plus tard sous lui, mais createApp ne s'intéresse qu'à ce premier-là.

Avant de voir le code, mettons de l'ordre dans ce que createApp ne fait pas - car on confond volontiers cette fonction avec trois outils entièrement différents. Elle ne crée pas de base de données : la mémoire commune de la mission est l'affaire de Pinia et de sa propre fonction createPinia, que tu verras encore dans cette leçon. Elle ne compile pas le CSS : l'apparence se décrit dans la section <style> du composant, et c'est Vite qui la transforme en feuille de style finie lors de la construction du projet. Elle ne crée pas non plus le routage : la navigation entre les écrans de la station est gérée par Vue Router et sa fonction createRouter, sur laquelle nous reviendrons au Centre de Navigation. Ce sont trois outils distincts et aucun d'eux ne se cache dans createApp.

1const app = createApp(App)

La forme de cette ligne est identique à celle du ref() de la leçon précédente : le mot const, un nom, le signe égal, un appel de fonction avec un argument. La variable app détient désormais l'instance de l'application.

Et voici l'observation la plus importante de toute la leçon : il n'y a toujours rien à l'écran. L'instance existe dans la mémoire du navigateur, elle connaît son composant racine, mais elle n'a pas la moindre idée de l'endroit de la page où elle doit se montrer. Le fichier index.html n'a pas changé, son <div> est encore vide. L'application est assemblée, mais elle n'a pas encore démarré.

mount() - le branchement sur la plateforme

Il manque un dernier geste : désigner la plateforme. C'est le rôle de la méthode mount appelée sur l'instance de l'application. Tu lui passes un sélecteur CSS - le même genre d'écriture que celle que tu employais dans les feuilles de style. Le signe # signifie "l'élément portant cet identifiant", donc '#app' se lit "l'élément dont l'id vaut app", c'est-à-dire exactement ce <div> vide du fichier HTML.

À partir de cet appel, Vue prend possession de l'élément désigné : il y insère le résultat du template du composant racine et veille lui-même, dès lors, sur son contenu. C'est l'instant du démarrage du système.

Retiens aussi ce que mount ne fait pas, car les méprises vont toujours dans le même sens ici. Il ne crée aucun fichier HTML - le fichier index.html doit exister avant, et c'est toi qui l'écris. Il n'installe pas les paquets, puisque c'est le travail de npm install de la leçon sur Vite. Il ne démarre pas non plus le serveur de développement : le serveur se lance par la commande npm run dev et tourne bien avant que ton code ne s'exécute.

1import { createApp } from 'vue'
2import App from './App.vue'
3
4const app = createApp(App)
5
6app.mount('#app')

Voilà le fichier de démarrage complet de l'application Vue la plus simple - quatre instructions et pas une de plus. Lance le projet et le navigateur t'affichera ce que contient le template de App.vue. Remarque que rien n'a changé en dehors de ce seul fichier : index.html est identique, App.vue est identique, et pourtant le système a démarré.

On peut aussi monter l'instance sans l'enregistrer dans une variable, en chaînant createApp(App).mount('#app'). Le résultat est le même et tu croiseras assez souvent cette écriture dans les exemples de la documentation, mais au NOVA LAB je te recommande la version en deux temps avec la variable app. La raison est pratique : dans un instant tu vas ajouter des modules de la station à cette instance, et pour cela il te faut une poignée que le chaînage jette tout simplement.

Quand le sélecteur ne correspond pas à la plateforme

Puisque le sélecteur n'est qu'un texte ordinaire, personne ne t'interdit d'y écrire n'importe quoi - et c'est de là que vient la panne classique du premier jour. L'application se compile, la console ne crie à aucune faute de syntaxe, et la page reste vide.

1// index.html contient <div id="app"></div>
2// mais le selecteur ci-dessous designe autre chose
3app.mount('#control-room')

Vue cherche l'élément #control-room, ne le trouve pas et n'a nulle part où poser l'interface, donc un avertissement signalant l'absence d'élément cible apparaît dans la console du navigateur. Et rien n'a changé pour autant dans ton composant - App.vue est parfaitement correct, et tu n'en verras pourtant pas un seul caractère. Alors quand l'écran du panneau reste noir commence par comparer deux endroits : la valeur de l'id dans index.html et le texte passé à l'appel de mount.

app.use() - ajouter les modules de la station

Un panneau ne fait pas une station. Tôt ou tard, le système de mission a besoin d'une mémoire commune dont tous les modules se servent à la fois, et d'une navigation qui bascule les écrans du Centre de Contrôle. Ce genre de choses ne s'accroche pas raisonnablement à un seul composant - elles doivent valoir dans l'application entière.

Vue les appelle des plugins ; dans notre vocabulaire, ce sont des modules de la station qu'on ajoute à l'instance avant le démarrage. On les branche avec la méthode use appelée sur l'instance de l'application : tu écris le nom de la variable, un point, use, et entre les parenthèses tu donnes le module prêt à l'emploi. Fais tout de suite connaissance avec les deux que l'assistant de la leçon sur Vite a inscrits dans ton projet.

Le premier est Pinia, la base de données centrale de la mission. La fonction createPinia en crée une instance neuve - comme createApp crée l'instance de l'application - et le résultat de cet appel part directement dans use. Le second est le routeur, que l'on fait venir du dossier ./router généré par l'assistant. C'est déjà un objet tout fait, tu le passes donc sans le moindre appel.

1import { createApp } from 'vue'
2import { createPinia } from 'pinia'
3import App from './App.vue'
4import router from './router'
5
6const app = createApp(App)
7
8app.use(createPinia())
9app.use(router)
10
11app.mount('#app')

Regarde ce qui n'a pas changé ici : la ligne du createApp et la ligne du mount sont exactement les mêmes que dans la version minimale, et App.vue ne sait toujours rien de tout cela. Seules deux lignes se sont ajoutées au milieu - et c'est là toute l'idée des plugins. Tu étends le système sans le reconstruire.

L'ordre compte ici et il n'y en a qu'un de valable : tous les appels de use doivent se tenir avant mount. Monter, c'est démarrer, et on n'ajoute pas de modules à une fusée après l'allumage des moteurs. Si tu intervertissais ces lignes, le premier rendu se ferait sans les plugins - les composants ne trouveraient ni la mémoire commune ni les éléments de navigation, et des erreurs de noms non résolus atterriraient dans la console. La méthode use renvoie d'ailleurs la même instance, il serait donc techniquement possible d'enchaîner les appels, mais je te recommande de les écrire l'un sous l'autre : en situation d'avarie, la liste des modules branchés saute aux yeux.

Si ton projet ne contient pas de dossier router ou si tu n'as pas installé Pinia, supprime simplement l'import correspondant avec sa ligne use - l'application fonctionnera sans eux, comme dans la version minimale. Les deux modules auront de toute façon leurs propres lieux : le Centre de Navigation s'occupe du routeur, et la Base de Données de la Mission s'occupe de Pinia.

App.vue - la charpente de la station

Reste le dernier des trois fichiers. App.vue est le composant racine, le seul que tu montes directement ; tous les autres pendront ensuite à l'intérieur de lui. De ce rôle découle son contenu : on n'y garde que ce qui est commun à la station entière et ne disparaît pas quand on change d'écran. En pratique, c'est la barre supérieure avec le nom et la navigation, la zone principale pour le contenu, et le pied de page.

Pour bâtir une telle charpente, les balises que tu connais du premier monde suffisent : <header>, <nav>, <main> et <footer>. Une seule chose est nouvelle - la façon de brancher les liens. Dans la leçon précédente, tu as retenu qu'on passe à @click un nom de fonction sans parenthèses. C'est vrai tant que la fonction n'a besoin d'aucun argument. Quand tu veux lui transmettre quelque chose, tu écris un appel normal, avec des parenthèses et une valeur dedans, par exemple @click="changeSection('Systems')". Vue traite cela comme une expression et l'emballe lui-même dans une fonction appelée au clic - c'est pourquoi elle ne se déclenche pas dès l'affichage de la page.

1<template>
2  <div class="nova-lab">
3    <header>
4      <h1>NOVA LAB</h1>
5      <nav>
6        <a href="#" @click="changeSection('Dashboard')">Dashboard</a>
7        <a href="#" @click="changeSection('Systems')">Systems</a>
8        <a href="#" @click="changeSection('Crew')">Crew</a>
9      </nav>
10    </header>
11
12    <main class="content">
13      <h2>{{ currentSection }}</h2>
14      <p>Section content will be loaded here.</p>
15    </main>
16
17    <footer>
18      <p>NOVA LAB Mission Control 2087</p>
19    </footer>
20  </div>
21</template>

Toute la charpente est du HTML ordinaire et sémantique - rien n'y a changé en dehors de trois arobases et d'une paire de moustaches. L'en-tête, la navigation, la zone principale et le pied de page auraient exactement la même allure sur une page dépourvue du moindre framework. Vue n'a ajouté ici que deux choses : une réaction au clic et un seul endroit où il substitue la valeur courante.

Passons à la seconde section du fichier. Il nous faut un conteneur portant le nom de la section active et une fonction qui remplace ce nom. La fonction reçoit un argument - la même valeur que celle transmise dans le template - et l'inscrit dans le conteneur à travers .value, conformément à la règle de la leçon précédente : dans le script toujours .value, dans le template jamais.

1<script setup>
2import { ref } from 'vue'
3
4const currentSection = ref('Dashboard')
5
6function changeSection(name) {
7  currentSection.value = name
8}
9</script>

Clique sur n'importe quel lien et le titre de la zone principale prendra le nom de la section choisie. Remarque ce qui reste en place pendant ce temps : la barre supérieure, la navigation et le pied de page ne bougent pas d'un cheveu, parce que Vue ne rafraîchit que le fragment qui se sert du conteneur modifié. C'est précisément pour cela que le composant racine est une charpente - ce qui est commun ne doit pas se recharger.

J'ai volontairement laissé de côté la section <style scoped>, puisque nous avons parlé des styles dans la leçon sur les modules .vue et qu'elle n'apporterait rien de neuf. Dans l'exercice que tu recevras dans un instant, tu peux laisser de côté les styles : ce sont la structure du template et la navigation qui sont vérifiées.

Pour l'instant nous ne basculons qu'une inscription, car c'est tout ce qu'il faut pour comprendre le rôle du composant racine. Au Centre de Navigation, cette même charpente deviendra une vraie navigation : les balises <a> céderont la place au composant RouterLink, et à l'emplacement du titre dans <main> viendra RouterView, qui échange des écrans entiers de la station au lieu d'une seule ligne de texte. Le conteneur currentSection cessera alors d'être nécessaire.

La séquence de démarrage du début à la fin

Tu as maintenant toutes les pièces, assemblons-les donc en une seule liste de contrôle. Voici le démarrage du système NOVA LAB, étape par étape, toujours dans le même ordre :

  1. Le navigateur charge index.html - le seul fichier HTML du projet.
  2. Il rencontre la balise <script type="module"> et réclame main.js - main.ts dans un projet TypeScript - et Vite lui sert ce fichier.
  3. Les instructions import font venir createApp du paquet vue ainsi que le composant App.
  4. L'appel createApp(App) crée l'instance de l'application - l'écran reste toujours vide.
  5. Les appels app.use() ajoutent les modules, si le projet en utilise.
  6. L'appel app.mount('#app') branche l'application sur le <div> vide d'index.html.
  7. Vue rend le template du composant App à l'intérieur de cet élément et le système est prêt au travail.

Cet ordre n'est pas une convention - il découle des dépendances. Tu ne monteras pas une application que tu n'as pas encore créée, et tu ne la créeras pas avant d'avoir fait venir createApp. La seule étape qui peut disparaître dans un projet simple est la cinquième - elle saute quand tu ne branches aucun module. Tout le reste a toujours la même allure, dans chaque projet Vue que tu ouvriras.

Retiens ceci : index.html est la plateforme d'atterrissage, main.js la séquence d'allumage, App.vue la charpente de la station - et app.mount('#app') l'unique appel où le système démarre pour de bon.

Code de cette leçon : src/App.vue
1<template>
2  <div class="nova-lab">
3    <h1>Structure d'un projet Vue + Vite</h1>
4
5    <section class="structure">
6      <div class="folder-tree">
7        <div class="item folder">
8          <span class="name">nova-lab-project/</span>
9          <div class="children">
10            <div class="item file">index.html</div>
11            <div class="item file">package.json</div>
12            <div class="item file">vite.config.js</div>
13            <div class="item folder">
14              <span class="name">src/</span>
15              <div class="children">
16                <div class="item file active">App.vue</div>
17                <div class="item file">main.js</div>
18                <div class="item folder">
19                  <span class="name">components/</span>
20                  <div class="children">
21                    <div class="item file">SystemCard.vue</div>
22                    <div class="item file">NavBar.vue</div>
23                  </div>
24                </div>
25                <div class="item folder">
26                  <span class="name">views/</span>
27                </div>
28                <div class="item folder">
29                  <span class="name">assets/</span>
30                </div>
31              </div>
32            </div>
33            <div class="item folder">
34              <span class="name">public/</span>
35            </div>
36          </div>
37        </div>
38      </div>
39    </section>
40
41    <section class="files-info">
42      <h2>Fichiers clés</h2>
43      <div class="file-card" v-for="file in keyFiles" :key="file.name">
44        <h3>{{ file.icon }} {{ file.name }}</h3>
45        <p>{{ file.description }}</p>
46      </div>
47    </section>
48  </div>
49</template>
50
51<script setup>
52const keyFiles = [
53  {
54    icon: '',
55    name: 'main.js',
56    description: 'Point d\'entrée de l\'application - crée l\'instance Vue et la monte dans le DOM'
57  },
58  {
59    icon: '',
60    name: 'App.vue',
61    description: 'Composant principal de l\'application (composant racine)'
62  },
63  {
64    icon: '',
65    name: 'vite.config.js',
66    description: 'Configuration de Vite - alias, plugins, options de build'
67  },
68  {
69    icon: '',
70    name: 'index.html',
71    description: 'Fichier HTML avec l\'élément #app où l\'application se monte'
72  }
73]
74</script>
75
76<style>
77.nova-lab {
78  min-height: 100vh;
79  background: #0a0e27;
80  color: #e0e0e0;
81  padding: 2rem;
82  font-family: system-ui, sans-serif;
83}
84
85h1 {
86  color: #00ff88;
87  text-align: center;
88}
89
90section {
91  background: rgba(26, 30, 55, 0.8);
92  border: 1px solid #00b4d8;
93  border-radius: 12px;
94  padding: 1.5rem;
95  margin: 1.5rem auto;
96  max-width: 700px;
97}
98
99h2 {
100  color: #00b4d8;
101  margin-top: 0;
102}
103
104.folder-tree {
105  font-family: 'Monaco', 'Consolas', monospace;
106  font-size: 0.95rem;
107}
108
109.item {
110  padding: 0.25rem 0;
111}
112
113.item.folder > .name {
114  color: #00b4d8;
115  cursor: pointer;
116}
117
118.item.file {
119  color: #aaa;
120}
121
122.item.file.active {
123  color: #00ff88;
124  font-weight: bold;
125}
126
127.children {
128  margin-left: 1.5rem;
129  border-left: 1px solid #333;
130  padding-left: 0.75rem;
131}
132
133.files-info {
134  display: grid;
135  gap: 1rem;
136}
137
138.file-card {
139  background: #1a1e37;
140  padding: 1rem;
141  border-radius: 8px;
142  border-left: 3px solid #00b4d8;
143}
144
145.file-card h3 {
146  margin: 0 0 0.5rem;
147  color: #00ff88;
148  font-size: 1rem;
149}
150
151.file-card p {
152  margin: 0;
153  color: #aaa;
154  font-size: 0.9rem;
155}
156</style>

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

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Que fait la fonction createApp() dans Vue ?

  2. 2. À quoi sert la méthode app.mount('#app') ?

Tâches pratiques dans le jeu

  • Tri vertical

    Ordonne l'ordre de démarrage d'une application Vue :

  • Clique dans l'ordre

    Ordonne l'import correct de la fonction createApp depuis Vue :

  • Éditeur de code

    Complète le fichier main.js - importe createApp et App.vue, puis monte l'application sur l'élément #app.

  • Éditeur de code

    Complète App.vue avec un header, une section main et un footer. Ajoute une navigation entre les sections.

  • Clique dans l'ordre

    Ordonne la syntaxe de création et de montage d'une application Vue :

  • Tri horizontal

    Ordonne la syntaxe de connexion d'un plugin à une application Vue :

  • Éditeur de code

    Complète le fichier index.html avec un élément div#app et une balise <script type="module"> qui charge main.js.