Cours Vue.js · Module 12: Tests et déploiement
Projet : Lancement de la mission Mars - application complète en production
Dans cette leçon7
L'application finale est prête au lancement ! Ton code s'envole vers Mars !
En cinq leçons, tu as assemblé la procédure de lancement à partir d'éléments séparés : tests de composants, tests de logique, E2E, build et déploiement. Chacun fonctionnait dans sa leçon, mais un vrai projet a besoin de tous à la fois, dans un seul dépôt, reliés de sorte qu'aucune étape ne dépende de la mémoire de l'opérateur. Ce projet est une configuration de production complète de la galerie - nous allons la parcourir fichier par fichier.
Une configuration Vite prête au décollage
Le fichier vite.config.js commence par trois plugins : vue() compile les composants, ViteImageOptimizer compresse les images, et visualizer dessine un rapport sur la taille des bundles et l'ouvre aussitôt dans le navigateur :
1// vite.config.js - Prêt pour la production
2import { defineConfig } from 'vite'
3import vue from '@vitejs/plugin-vue'
4import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
5import { visualizer } from 'rollup-plugin-visualizer'
6
7export default defineConfig({
8 plugins: [
9 vue(),
10 ViteImageOptimizer({
11 jpg: { quality: 80 },
12 png: { quality: 80 },
13 webp: { quality: 80 }
14 }),
15 visualizer({
16 open: true,
17 gzipSize: true
18 })
19 ],Les plugins ne touchent pas aux fichiers de src - ils travaillent sur ce qui part vers dist. La seconde partie du fichier configure le build : le minifieur terser, le découpage en bundles et le port du serveur de développement :
1 // vite.config.js - suite de defineConfig
2 build: {
3 outDir: 'dist',
4 sourcemap: false,
5 minify: 'terser',
6
7 terserOptions: {
8 compress: {
9 drop_console: true,
10 drop_debugger: true
11 }
12 },
13
14 rolldownOptions: {
15 output: {
16 codeSplitting: {
17 groups: [
18 { name: 'vue-vendor', test: /node_modules[\\/](vue|@vue|vue-router|pinia)[\\/]/ },
19 { name: 'utils', test: /node_modules[\\/]@vueuse[\\/]/ }
20 ]
21 }
22 }
23 }
24 },
25
26 server: {
27 port: 3000,
28 open: true
29 }
30})Les options drop_console et drop_debugger de terserOptions suppriment du code de production tous les appels console.log et les instructions debugger ; les fichiers sources restent intacts. Terser doit être installé à part (npm install -D terser), et le minifieur par défaut de Vite 8, Oxc, fait la même chose avec l'option dropConsole dans build.rolldownOptions.output.minify.compress. Le découpage est décrit par codeSplitting : chaque groupe a un nom de bundle et un test, une expression régulière comparée au chemin du module, dans laquelle [\\/] attrape à la fois la barre oblique et la barre oblique inverse des chemins Windows. Les anciennes configurations avec rollupOptions.output.manualChunks sous forme d'objet ne fonctionnent plus - Vite 8 interrompt le build.
Les scripts de package.json
Le fichier package.json rassemble les commandes de la procédure de lancement au même endroit - chaque étape du pipeline appellera ensuite l'un de ces scripts :
1// package.json
2{
3 "name": "gallery-app",
4 "version": "1.0.0",
5 "scripts": {
6 "dev": "vite",
7 "build": "vite build",
8 "preview": "vite preview",
9 "test": "vitest",
10 "test:ui": "vitest --ui",
11 "test:coverage": "vitest --coverage",
12 "lint": "eslint . --ext .vue,.js,.ts",
13 "format": "prettier --write .",
14 "type-check": "vue-tsc --noEmit",
15 "deploy": "npm run build && vercel --prod"
16 }
17}lint inspecte le code avec ESLint, format le met en forme avec Prettier, et type-check lance vue-tsc, c'est-à-dire la vérification des types TypeScript qui couvre aussi les fichiers .vue. deploy expédie le build avec vercel --prod, équivalent à vercel deploy --prod.
Chargement paresseux des composants
Un composant lourd, par exemple un graphique de télémétrie ouvert à la demande, n'a pas à se trouver dans le bundle principal. La fonction defineAsyncComponent du paquet vue prend une fonction avec un import() dynamique et renvoie un composant téléchargé lors de son premier rendu :
1import { defineAsyncComponent } from 'vue'
2
3const TelemetryChart = defineAsyncComponent(() =>
4 import('./components/TelemetryChart.vue')
5)Le build place TelemetryChart.vue dans un bundle séparé, et dans le template tu l'utilises comme n'importe quel composant. Pour les routes du router, un simple () => import() suffit.
Pipeline CI/CD
Le workflow répartit le travail en trois jobs. Le premier, test, se lance à chaque push et pull request et effectue un contrôle qualité complet :
1# .github/workflows/ci.yml
2name: CI/CD
3
4on:
5 push:
6 branches: [main, develop]
7 pull_request:
8 branches: [main]
9
10jobs:
11 test:
12 runs-on: ubuntu-latest
13
14 steps:
15 - uses: actions/checkout@v7
16
17 - name: Setup Node
18 uses: actions/setup-node@v7
19 with:
20 node-version: 24
21 cache: 'npm'
22
23 - name: Install dependencies
24 run: npm ci
25
26 - name: Lint
27 run: npm run lint
28
29 - name: Type check
30 run: npm run type-check
31
32 - name: Run tests
33 run: npm run test:coverage
34
35 - name: Upload coverage
36 uses: codecov/codecov-action@v7
37 with:
38 token: ${{ secrets.CODECOV_TOKEN }}L'ordre est voulu : les contrôles les moins coûteux passent en premier. cache: 'npm' conserve les paquets entre les exécutions, et l'action Codecov envoie le rapport de couverture avec un jeton issu des secrets du dépôt. Le job build attend un test au vert grâce au champ needs et enregistre dist comme artefact :
1 build:
2 runs-on: ubuntu-latest
3 needs: test
4
5 steps:
6 - uses: actions/checkout@v7
7
8 - name: Setup Node
9 uses: actions/setup-node@v7
10 with:
11 node-version: 24
12 cache: 'npm'
13
14 - name: Install dependencies
15 run: npm ci
16
17 - name: Build
18 run: npm run build
19
20 - name: Upload build artifacts
21 uses: actions/upload-artifact@v7
22 with:
23 name: dist
24 path: distUn artefact est un fichier que GitHub conserve entre les jobs. La version v3 des actions d'artefacts a cessé de fonctionner le 30 janvier 2025, c'est pourquoi les anciens workflows échouent aujourd'hui. Le dernier job ne déploie l'application que depuis la branche main, ce que vérifie la condition if :
1 deploy:
2 runs-on: ubuntu-latest
3 needs: build
4 if: github.ref == 'refs/heads/main'
5
6 steps:
7 - uses: actions/checkout@v7
8
9 - name: Download build artifacts
10 uses: actions/download-artifact@v8
11 with:
12 name: dist
13 path: dist
14
15 - name: Deploy to Vercel
16 uses: amondnet/vercel-action@v42
17 with:
18 vercel-token: ${{ secrets.VERCEL_TOKEN }}
19 vercel-org-id: ${{ secrets.ORG_ID }}
20 vercel-project-id: ${{ secrets.PROJECT_ID }}
21 vercel-args: '--prod'Les pull requests passent donc les tests et le build sans déploiement. L'artefact téléchargé est utile pour un hébergement statique, car Vercel construit par défaut le projet de son côté, à partir du code envoyé.
La capsule Docker
La version conteneurisée est le Dockerfile de la leçon précédente, avec des commentaires et un HEALTHCHECK qui vérifie toutes les 30 secondes que nginx répond :
1# Dockerfile - Build multi-étapes
2FROM node:24-alpine AS builder
3
4WORKDIR /app
5
6# Copier les fichiers package
7COPY package*.json ./
8
9# Installer les dépendances (le build a aussi besoin des devDependencies)
10RUN npm ci
11
12# Copier le code source
13COPY . .
14
15# Build
16RUN npm run build
17
18# Étape de production
19FROM nginx:alpine
20
21# Copier le build
22COPY /app/dist /usr/share/nginx/html
23
24# Copier la configuration nginx
25COPY nginx.conf /etc/nginx/conf.d/default.conf
26
27# Vérification de santé (health check)
28HEALTHCHECK \
29 CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1
30
31EXPOSE 80
32
33CMD ["nginx", "-g", "daemon off;"]Attention à une erreur fréquente : npm ci --only=production dans l'étape de build ignore les devDependencies, où se trouve Vite, et le build échouerait. De toute façon, seul dist arrive dans l'image finale.
nginx optimisé
La configuration du serveur ajoute la compression gzip, des en-têtes de sécurité et des règles de cache distinctes :
1# nginx.conf - Optimisé
2server {
3 listen 80;
4 server_name _;
5 root /usr/share/nginx/html;
6 index index.html;
7
8 # Gzip
9 gzip on;
10 gzip_vary on;
11 gzip_types text/plain text/css text/xml text/javascript
12 application/x-javascript application/xml+rss
13 application/javascript application/json;
14
15 # En-têtes de sécurité
16 add_header X-Frame-Options "SAMEORIGIN" always;
17 add_header X-Content-Type-Options "nosniff" always;
18 # En-tête obsolète : 0 désactive l'ancien filtre XSS, utilise plutôt une Content-Security-Policy
19 add_header X-XSS-Protection "0" always;
20
21 # Routage SPA
22 location / {
23 try_files $uri $uri/ /index.html;
24 }
25
26 # Mise en cache des assets statiques
27 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
28 expires 1y;
29 add_header Cache-Control "public, immutable";
30 }
31
32 # Ne pas mettre index.html en cache
33 location = /index.html {
34 add_header Cache-Control "no-cache, must-revalidate";
35 }
36}index.html ne doit pas aller dans un cache long, car il désigne les noms actuels des bundles. X-XSS-Protection est obsolète et MDN recommande à sa place Content-Security-Policy, d'où la valeur 0. Un piège : un add_header au niveau server n'est pas transmis aux blocs location qui ont leur propre add_header, donc les en-têtes de sécurité doivent y être répétés.
Surveillance et SEO
Après le lancement, tu surveilles les instruments : le profilage des performances dans les Vue DevTools montre les composants qui se rendent trop souvent, Lighthouse audite le chargement de la page, et les métriques Web Vitals (LCP, INP, CLS) reflètent l'expérience des vrais utilisateurs. Une SPA envoie aux robots des moteurs de recherche un index.html vide, et c'est JavaScript qui dessine le contenu. Google et Bing s'en sortent avec les applications simples, mais n'attendent pas les données chargées de façon asynchrone, donc seul le SSR ou le SSG fournit aux robots le HTML complet, le plus simplement avec Nuxt - tu le découvriras au Quantum Laboratory. Avant un lancement manuel, respecte l'ordre du pipeline : lint et vérification des types, tests, npm run build, prévisualisation avec npm run preview, et vercel --prod tout à la fin.
FÉLICITATIONS ! Tu as terminé ta formation sur la plateforme de lancement du NOVA LAB !
Ton code a passé tous les tests de pré-lancement et est prêt pour la mission Mars !
Désormais, ton application :
- Est entièrement testée (tous les systèmes vérifiés)
- Optimisée pour la performance (prête pour les conditions difficiles de l'espace)
- Prête pour le déploiement (procédure de lancement terminée)
- Dispose d'un pipeline CI/CD (systèmes de contrôle automatisés)
- Tourne dans un conteneur Docker (capsule hermétique)
MISSION DE LANCEMENT ACCOMPLIE !
Mon conseil : copie cet ensemble de fichiers dans ton propre dépôt et adapte-le à ton application - c'est un squelette pour chacun de tes futurs projets Vue. Ton code décolle vers Mars, et trois autres stations du NOVA LAB t'attendent encore ; dans la Holographic Chamber, tu donneras vie à l'interface avec des animations.
Retiens : une application n'est prête pour le vol que lorsque les tests, le build et le déploiement s'enchaînent automatiquement, sans intervention de l'opérateur.
Code de cette leçon : App.vue
1<script setup>
2import { ref, computed, onMounted } from 'vue'
3
4// NOVA LAB - Tableau de bord de monitoring
5const metrics = ref({
6 uptime: 99.9,
7 responseTime: 145,
8 errorRate: 0.02,
9 activeUsers: 1247,
10 requestsPerMinute: 3542,
11 cpuUsage: 45,
12 memoryUsage: 62,
13 diskUsage: 38
14})
15
16const alerts = ref([
17 { id: 1, level: 'warning', message: 'Utilisation mémoire élevée détectée', time: 'il y a 2 min' },
18 { id: 2, level: 'info', message: 'Déploiement terminé avec succès', time: 'il y a 15 min' }
19])
20
21const performanceData = ref([
22 { time: '00:00', value: 120 },
23 { time: '04:00', value: 95 },
24 { time: '08:00', value: 180 },
25 { time: '12:00', value: 220 },
26 { time: '16:00', value: 195 },
27 { time: '20:00', value: 150 }
28])
29
30const healthStatus = computed(() => {
31 const { uptime, errorRate, responseTime } = metrics.value
32
33 if (uptime >= 99.5 && errorRate < 0.1 && responseTime < 200) {
34 return { status: 'healthy', label: 'Bon état', color: '#00ff88' }
35 } else if (uptime >= 99 && errorRate < 0.5 && responseTime < 500) {
36 return { status: 'warning', label: 'Avertissement', color: '#ffa500' }
37 } else {
38 return { status: 'critical', label: 'Critique', color: '#ff0000' }
39 }
40})
41
42const performanceScore = computed(() => {
43 const avgResponseTime = metrics.value.responseTime
44 if (avgResponseTime < 100) return 'Excellent'
45 if (avgResponseTime < 200) return 'Bon'
46 if (avgResponseTime < 500) return 'Correct'
47 return 'Médiocre'
48})
49
50function refreshMetrics() {
51 // Simuler une mise à jour des métriques
52 metrics.value.responseTime = Math.floor(Math.random() * 100) + 100
53 metrics.value.activeUsers = Math.floor(Math.random() * 500) + 1000
54 metrics.value.requestsPerMinute = Math.floor(Math.random() * 1000) + 3000
55 metrics.value.cpuUsage = Math.floor(Math.random() * 30) + 30
56 metrics.value.memoryUsage = Math.floor(Math.random() * 20) + 50
57}
58
59onMounted(() => {
60 // Rafraîchissement automatique toutes les 5 secondes
61 setInterval(refreshMetrics, 5000)
62})
63</script>
64
65<template>
66 <div class="monitoring-dashboard">
67 <div class="header">
68 <h1>Tableau de bord du contrôle de mission</h1>
69 <p>Monitoring en temps réel NOVA LAB</p>
70 </div>
71
72 <div class="health-status" :style="{ borderColor: healthStatus.color }">
73 <div class="status-icon" :style="{ color: healthStatus.color }">
74 {{ healthStatus.status === 'healthy' ? 'OK' : healthStatus.status === 'warning' ? '!' : 'X' }}
75 </div>
76 <div class="status-info">
77 <h2>État du système : {{ healthStatus.label }}</h2>
78 <p>Disponibilité : {{ metrics.uptime }}% | Performance : {{ performanceScore }}</p>
79 </div>
80 </div>
81
82 <div class="metrics-grid">
83 <div class="metric-card">
84 <div class="metric-icon">◆</div>
85 <div class="metric-value">{{ metrics.activeUsers.toLocaleString() }}</div>
86 <div class="metric-label">Utilisateurs actifs</div>
87 </div>
88
89 <div class="metric-card">
90 <div class="metric-icon">↯</div>
91 <div class="metric-value">{{ metrics.responseTime }}ms</div>
92 <div class="metric-label">Temps de réponse moyen</div>
93 </div>
94
95 <div class="metric-card">
96 <div class="metric-icon">▲</div>
97 <div class="metric-value">{{ metrics.requestsPerMinute.toLocaleString() }}</div>
98 <div class="metric-label">Requêtes/min</div>
99 </div>
100
101 <div class="metric-card">
102 <div class="metric-icon">●</div>
103 <div class="metric-value">{{ metrics.errorRate }}%</div>
104 <div class="metric-label">Taux d'erreur</div>
105 </div>
106 </div>
107
108 <div class="resource-usage">
109 <h3>Utilisation des ressources</h3>
110 <div class="resource-bars">
111 <div class="resource-item">
112 <div class="resource-header">
113 <span>CPU</span>
114 <span>{{ metrics.cpuUsage }}%</span>
115 </div>
116 <div class="resource-bar">
117 <div
118 class="resource-fill"
119 :style="{
120 width: metrics.cpuUsage + '%',
121 background: metrics.cpuUsage > 80 ? '#ff0000' : metrics.cpuUsage > 60 ? '#ffa500' : '#00ff88'
122 }"
123 ></div>
124 </div>
125 </div>
126
127 <div class="resource-item">
128 <div class="resource-header">
129 <span>Mémoire</span>
130 <span>{{ metrics.memoryUsage }}%</span>
131 </div>
132 <div class="resource-bar">
133 <div
134 class="resource-fill"
135 :style="{
136 width: metrics.memoryUsage + '%',
137 background: metrics.memoryUsage > 80 ? '#ff0000' : metrics.memoryUsage > 60 ? '#ffa500' : '#00ff88'
138 }"
139 ></div>
140 </div>
141 </div>
142
143 <div class="resource-item">
144 <div class="resource-header">
145 <span>Disque</span>
146 <span>{{ metrics.diskUsage }}%</span>
147 </div>
148 <div class="resource-bar">
149 <div
150 class="resource-fill"
151 :style="{
152 width: metrics.diskUsage + '%',
153 background: metrics.diskUsage > 80 ? '#ff0000' : metrics.diskUsage > 60 ? '#ffa500' : '#00ff88'
154 }"
155 ></div>
156 </div>
157 </div>
158 </div>
159 </div>
160
161 <div class="alerts-panel">
162 <h3>Alertes récentes</h3>
163 <div class="alerts-list">
164 <div
165 v-for="alert in alerts"
166 :key="alert.id"
167 class="alert-item"
168 :class="alert.level"
169 >
170 <span class="alert-icon">
171 {{ alert.level === 'warning' ? '!' : alert.level === 'error' ? 'X' : 'ℹ' }}
172 </span>
173 <span class="alert-message">{{ alert.message }}</span>
174 <span class="alert-time">{{ alert.time }}</span>
175 </div>
176 </div>
177 </div>
178
179 <div class="actions">
180 <button @click="refreshMetrics" class="refresh-btn">
181 Rafraîchir les métriques
182 </button>
183 </div>
184 </div>
185</template>
186
187<style scoped>
188.monitoring-dashboard {
189 background: #0a0e27;
190 color: #00ff88;
191 padding: 2rem;
192 min-height: 100vh;
193 font-family: 'Courier New', monospace;
194}
195
196.header {
197 text-align: center;
198 margin-bottom: 2rem;
199 border-bottom: 2px solid #00b4d8;
200 padding-bottom: 1rem;
201}
202
203.header h1 {
204 color: #00ff88;
205 text-shadow: 0 0 10px #00ff88;
206 margin: 0;
207}
208
209.health-status {
210 display: flex;
211 align-items: center;
212 gap: 1.5rem;
213 background: rgba(0, 180, 216, 0.1);
214 border: 3px solid;
215 border-radius: 12px;
216 padding: 1.5rem;
217 margin-bottom: 2rem;
218}
219
220.status-icon {
221 font-size: 3rem;
222}
223
224.status-info h2 {
225 margin: 0 0 0.5rem 0;
226 color: #fff;
227}
228
229.status-info p {
230 margin: 0;
231 color: #00b4d8;
232}
233
234.metrics-grid {
235 display: grid;
236 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
237 gap: 1.5rem;
238 margin-bottom: 2rem;
239}
240
241.metric-card {
242 background: rgba(0, 180, 216, 0.1);
243 border: 2px solid #00b4d8;
244 border-radius: 8px;
245 padding: 1.5rem;
246 text-align: center;
247 transition: transform 0.3s;
248}
249
250.metric-card:hover {
251 transform: translateY(-5px);
252 box-shadow: 0 5px 20px rgba(0, 180, 216, 0.3);
253}
254
255.metric-icon {
256 font-size: 2.5rem;
257 margin-bottom: 0.5rem;
258}
259
260.metric-value {
261 font-size: 2rem;
262 color: #00ff88;
263 font-weight: bold;
264 margin-bottom: 0.5rem;
265}
266
267.metric-label {
268 color: #00b4d8;
269 font-size: 0.9rem;
270}
271
272.resource-usage, .alerts-panel {
273 background: rgba(0, 180, 216, 0.1);
274 border: 1px solid #00b4d8;
275 border-radius: 8px;
276 padding: 1.5rem;
277 margin-bottom: 1.5rem;
278}
279
280.resource-usage h3, .alerts-panel h3 {
281 color: #00b4d8;
282 margin-top: 0;
283}
284
285.resource-bars {
286 display: flex;
287 flex-direction: column;
288 gap: 1rem;
289}
290
291.resource-item {
292 display: flex;
293 flex-direction: column;
294 gap: 0.5rem;
295}
296
297.resource-header {
298 display: flex;
299 justify-content: space-between;
300 color: #fff;
301 font-size: 0.9rem;
302}
303
304.resource-bar {
305 height: 20px;
306 background: rgba(0, 255, 136, 0.1);
307 border-radius: 10px;
308 overflow: hidden;
309 border: 1px solid #00b4d8;
310}
311
312.resource-fill {
313 height: 100%;
314 transition: width 0.5s, background 0.5s;
315 border-radius: 10px;
316}
317
318.alerts-list {
319 display: flex;
320 flex-direction: column;
321 gap: 0.75rem;
322}
323
324.alert-item {
325 display: flex;
326 align-items: center;
327 gap: 1rem;
328 padding: 1rem;
329 border-radius: 8px;
330 border-left: 4px solid;
331}
332
333.alert-item.warning {
334 background: rgba(255, 165, 0, 0.1);
335 border-color: #ffa500;
336}
337
338.alert-item.error {
339 background: rgba(255, 0, 0, 0.1);
340 border-color: #ff0000;
341}
342
343.alert-item.info {
344 background: rgba(0, 180, 216, 0.1);
345 border-color: #00b4d8;
346}
347
348.alert-icon {
349 font-size: 1.5rem;
350}
351
352.alert-message {
353 flex: 1;
354 color: #fff;
355}
356
357.alert-time {
358 color: #00b4d8;
359 font-size: 0.85rem;
360}
361
362.actions {
363 text-align: center;
364}
365
366.refresh-btn {
367 padding: 1rem 2rem;
368 background: rgba(0, 180, 216, 0.2);
369 border: 2px solid #00b4d8;
370 color: #00b4d8;
371 font-size: 1rem;
372 font-family: 'Courier New', monospace;
373 border-radius: 8px;
374 cursor: pointer;
375 transition: all 0.3s;
376}
377
378.refresh-btn:hover {
379 background: #00b4d8;
380 color: #0a0e27;
381 box-shadow: 0 0 20px #00b4d8;
382}
383</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. Comment implémenter le chargement paresseux (lazy loading) d'un composant dans Vue 3 ?
2. Quels outils aident à surveiller les performances d'une application Vue ?
Voici 2 des 4 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Tri horizontal
Ordonne la syntaxe pour le chargement paresseux d'un composant :
- Éditeur de code
Configure un vite.config.js complet avec : le plugin vue, l'optimiseur d'images, le visualizer, et les options de build avec découpage en bundles (dans Vite 8 via codeSplitting au lieu de manualChunks).
- Tri vertical
Ordonne les étapes de préparation à la production, de la première à la dernière :
- Clique dans l'ordre
Ordonne la commande de déploiement en production pour Vercel :
- Tri vertical
Ordonne dans l'ordre les étapes du workflow CI/CD de GitHub Actions :
- Clique dans l'ordre
Ordonne la commande de déploiement en production pour Netlify :
- Tri horizontal
Ordonne la directive nginx pour le routage SPA :
- Clique dans l'ordre
Ordonne la commande d'exécution d'un conteneur Docker avec mappage de port :