Cours Vue.js · Module 12: Tests et déploiement

Projet : Lancement de la mission Mars - application complète en production

9 min de lecture
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: dist

Un 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 --from=builder /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 --interval=30s --timeout=3s \
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. 1. Comment implémenter le chargement paresseux (lazy loading) d'un composant dans Vue 3 ?

  2. 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 :