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

Déploiement

8 min de lecture
Dans cette leçon7

Le déploiement de l'application sur un serveur - c'est le lancement de la fusée ! Ton code part en production comme une fusée vers Mars !

Le dossier dist de la leçon précédente se trouve sur ton disque. Il faut l'emmener en orbite : sur un serveur qui fournit les fichiers à chaque technicien, à une adresse fixe et en HTTPS - et sans copie manuelle à chaque correction. La réponse s'appelle CI/CD, c'est-à-dire Continuous Integration et Continuous Deployment - l'automatisation des tests, des builds et des déploiements, qui exécute seule toute la procédure de lancement après chaque envoi de code.

Déploiement sur Vercel

Le plus simple est de connecter un dépôt Git dans le tableau de bord de Vercel. Vercel détecte tout seul le projet Vite et règle la commande npm run build ainsi que le dossier dist, puis déploie la production après chaque push sur la branche principale et prépare une adresse de prévisualisation pour chaque pull request. Si tu préfères le terminal, utilise le CLI Vercel :

1# Installer le CLI Vercel
2npm install -g vercel
3
4# Déployer
5vercel
6
7# Déploiement en production
8vercel --prod

Un simple vercel est le raccourci de vercel deploy, car deploy est la commande par défaut du CLI. Sans option, il crée un déploiement de prévisualisation à une adresse unique, et --prod le publie sur le domaine de production ; seul le tout premier déploiement d'un nouveau projet part toujours en production. Tu peux garder les réglages dans le dépôt, dans un fichier vercel.json :

1// vercel.json
2{
3  "buildCommand": "npm run build",
4  "outputDirectory": "dist",
5  "framework": "vite",
6  "rewrites": [
7    { "source": "/(.*)", "destination": "/index.html" }
8  ]
9}

Les champs buildCommand, outputDirectory et framework répètent ce que Vercel détecte tout seul, mais ils ne dépendent pas des clics dans le tableau de bord. La partie la plus importante est la section rewrites.

Pourquoi une SPA a besoin de réécrire les adresses

Une application avec Vue Router est une SPA : le serveur n'a que index.html, et les routes comme /gallery sont gérées par JavaScript dans le navigateur. Quand un technicien rafraîchit la page sur /gallery, le navigateur demande au serveur un fichier /gallery qui n'existe pas et reçoit une erreur 404. La règle de réécriture demande au serveur de renvoyer index.html pour chaque chemin, et le router affiche la bonne vue.

Déploiement sur Netlify

Netlify fonctionne de façon similaire : tu connectes le dépôt ou tu utilises le CLI. La commande netlify deploy publie une version brouillon à une adresse de prévisualisation, et avec l'option --prod la version de production :

1# Installer le CLI Netlify
2npm install -g netlify-cli
3
4# Déployer
5netlify deploy
6
7# Production
8netlify deploy --prod

La configuration va dans netlify.toml. La section [build] indique la commande de build et le dossier à publier, et [[redirects]] fait pour la SPA ce que rewrites fait sur Vercel :

1# netlify.toml
2[build]
3  command = "npm run build"
4  publish = "dist"
5
6[[redirects]]
7  from = "/*"
8  to = "/index.html"
9  status = 200

Le statut 200 signifie une réécriture, donc l'adresse reste inchangée. Avec un code 301, le navigateur sauterait vers la page d'accueil et le technicien perdrait sa route.

Déploiement sur GitHub Pages

GitHub Pages héberge un site directement depuis le dépôt. Le paquet gh-pages envoie le contenu de dist sur une branche séparée :

1npm install -D gh-pages

Un site de projet vit à une adresse contenant le nom du dépôt, par exemple https://user.github.io/repository-name/. C'est pourquoi tu définis base dans vite.config.js, le chemin à partir duquel Vite construit les références aux fichiers :

1// vite.config.js
2export default defineConfig({
3  base: '/repository-name/', // Nom du dépôt
4  plugins: [vue()]
5})

Sans base, le navigateur chercherait les scripts à la racine du domaine et afficherait une page blanche ; pour un site utilisateur, garde base: '/'. Il reste le script de déploiement :

1// package.json
2{
3  "scripts": {
4    "deploy": "npm run build && gh-pages -d dist"
5  }
6}

npm run deploy construit l'application et publie dist. GitHub Pages ne prend pas en charge les règles de réécriture, donc rafraîchir une route profonde de la SPA renvoie une erreur 404. La documentation de Vite décrit aussi le déploiement via GitHub Actions à la place du paquet gh-pages.

Déploiement avec Docker

Ton propre serveur nécessite une image Docker. Le Dockerfile utilise un build multi-étapes (multi-stage build) : la première étape, sur une image Node, construit l'application, et la seconde, sur un nginx léger, se contente de la servir :

1# Dockerfile
2FROM node:24-alpine AS builder
3
4WORKDIR /app
5
6COPY package*.json ./
7RUN npm ci
8
9COPY . .
10RUN npm run build
11
12FROM nginx:alpine
13
14COPY --from=builder /app/dist /usr/share/nginx/html
15COPY nginx.conf /etc/nginx/conf.d/default.conf
16
17EXPOSE 80
18
19CMD ["nginx", "-g", "daemon off;"]

COPY --from=builder ne reprend de la première étape que dist, donc le code source, node_modules et Node lui-même restent hors de l'image finale. L'image de base est Node 24, la version LTS actuelle - Vite 8 exige au moins Node 20.19 ou 22.12, donc l'ancien node:18 ferait échouer le build. nginx a encore besoin d'une règle pour la SPA :

1# nginx.conf
2server {
3  listen 80;
4  server_name localhost;
5
6  root /usr/share/nginx/html;
7  index index.html;
8
9  location / {
10    try_files $uri $uri/ /index.html;
11  }
12
13  # Mise en cache des assets statiques
14  location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
15    expires 1y;
16    add_header Cache-Control "public, immutable";
17  }
18}

try_files $uri $uri/ /index.html vérifie le fichier, puis le dossier, et en dernier recours renvoie index.html. Un cache d'un an pour les assets est sans risque, car une modification du code change le hash dans le nom du fichier. Il ne reste qu'à construire l'image et lancer le conteneur :

1# Construire et lancer
2docker build -t gallery-app .
3docker run -p 8080:80 gallery-app

-t gallery-app donne un nom à l'image, et le point désigne le dossier courant comme contexte de build. -p 8080:80 relie le port 8080 de ton ordinateur au port 80 du conteneur, donc l'application tourne sur http://localhost:8080.

CI/CD avec GitHub Actions

Un workflow GitHub Actions est un fichier YAML dans le dossier .github/workflows, lancé lors des événements listés dans la section on. Celui-ci exécute la procédure de lancement après chaque push sur main :

1# .github/workflows/deploy.yml
2name: Deploy
3
4on:
5  push:
6    branches: [main]
7
8jobs:
9  deploy:
10    runs-on: ubuntu-latest
11
12    steps:
13      - uses: actions/checkout@v7
14
15      - name: Setup Node
16        uses: actions/setup-node@v7
17        with:
18          node-version: 24
19
20      - name: Install dependencies
21        run: npm ci
22
23      - name: Run tests
24        run: npm test
25
26      - name: Build
27        run: npm run build
28
29      - name: Deploy to Vercel
30        uses: amondnet/vercel-action@v42
31        with:
32          vercel-token: ${{ secrets.VERCEL_TOKEN }}
33          vercel-org-id: ${{ secrets.ORG_ID }}
34          vercel-project-id: ${{ secrets.PROJECT_ID }}
35          vercel-args: '--prod'

Les étapes s'exécutent dans l'ordre : actions/checkout récupère le code, actions/setup-node installe Node, npm ci installe les dépendances exactement selon package-lock.json, puis viennent les tests, le build et le déploiement. Si une étape échoue, les suivantes ne s'exécutent pas. Dans un pipeline complet, le linting (npm run lint) passe aussi avant les tests - tu le verras dans le projet. Les secrets sont des clés secrètes enregistrées dans les réglages du dépôt, et amondnet/vercel-action est une action de la communauté - un projet relié à Vercel via Git n'a pas besoin de cette étape.

Checklist de performance

  • Lazy load routes - les vues ne se chargent qu'à l'entrée sur leur route
  • Code splitting - les bibliothèques dans des bundles séparés gardés en cache
  • Image optimization - les images pèsent souvent plus que le code
  • Minification - Vite la fait par défaut
  • Gzip/Brotli compression - compression côté serveur
  • CDN pour les assets statiques - les fichiers au plus près de l'utilisateur
  • Cache headers - longs pour les fichiers avec hash, courts pour index.html
  • Preload critical resources - Vite ajoute lui-même modulepreload
  • Remove console.logs - le minifieur les supprime une fois la bonne option activée
  • Source maps uniquement en dev - publie-les seulement en connaissance de cause, par exemple sourcemap: 'hidden' pour le signalement d'erreurs

Mon conseil : pour commencer, choisis Vercel ou Netlify avec l'intégration Git - tu obtiens HTTPS, un CDN et des prévisualisations de pull requests sans aucune configuration. Garde Docker pour le moment où l'application devra tourner sur l'infrastructure de la station. Dans le projet final, tu relieras les tests, le build et le déploiement en un seul pipeline.

Retiens : le CI/CD est une procédure de pré-lancement automatique qui ne te laissera pas décoller avec un système endommagé.

Code de cette leçon : App.vue
1<script setup>
2import { ref, computed } from 'vue'
3
4// NOVA LAB - Visualisation du pipeline CI/CD
5const pipelineStages = ref([
6  { id: 1, name: 'Source', status: 'completed', duration: 2, icon: '' },
7  { id: 2, name: 'Build', status: 'idle', duration: 45, icon: '' },
8  { id: 3, name: 'Test', status: 'idle', duration: 30, icon: '' },
9  { id: 4, name: 'Scan de sécurité', status: 'idle', duration: 20, icon: '' },
10  { id: 5, name: 'Déploiement staging', status: 'idle', duration: 25, icon: '' },
11  { id: 6, name: 'Déploiement production', status: 'idle', duration: 30, icon: '' }
12])
13
14const pipelineRunning = ref(false)
15const currentStage = ref(0)
16const pipelineProgress = ref(0)
17
18const logs = ref([
19  '→ Pipeline prêt à démarrer...'
20])
21
22let stageInterval = null
23
24const overallProgress = computed(() => {
25  const completed = pipelineStages.value.filter(s => s.status === 'completed').length
26  return Math.round((completed / pipelineStages.value.length) * 100)
27})
28
29const pipelineStatus = computed(() => {
30  if (pipelineStages.value.some(s => s.status === 'failed')) return 'failed'
31  if (pipelineStages.value.every(s => s.status === 'completed')) return 'success'
32  if (pipelineRunning.value) return 'running'
33  return 'idle'
34})
35
36function startPipeline() {
37  pipelineRunning.value = true
38  currentStage.value = 0
39  pipelineProgress.value = 0
40  logs.value = ['Démarrage du pipeline CI/CD...']
41
42  // Réinitialiser toutes les étapes sauf la première
43  pipelineStages.value.forEach((stage, index) => {
44    stage.status = index === 0 ? 'completed' : 'idle'
45  })
46
47  runNextStage()
48}
49
50function runNextStage() {
51  if (currentStage.value >= pipelineStages.value.length - 1) {
52    completePipeline()
53    return
54  }
55
56  currentStage.value++
57  const stage = pipelineStages.value[currentStage.value]
58  stage.status = 'running'
59
60  logs.value.push(`⏳ En cours : ${stage.name}...`)
61
62  pipelineProgress.value = 0
63  const increment = 100 / (stage.duration / 10)
64
65  stageInterval = setInterval(() => {
66    pipelineProgress.value += increment
67
68    if (pipelineProgress.value >= 100) {
69      clearInterval(stageInterval)
70      stage.status = 'completed'
71      logs.value.push(`${stage.name} terminé avec succès`)
72
73      setTimeout(() => {
74        runNextStage()
75      }, 500)
76    }
77  }, 100)
78}
79
80function completePipeline() {
81  pipelineRunning.value = false
82  logs.value.push('Pipeline terminé avec succès !')
83  logs.value.push('Mission Mars déployée en production !')
84}
85
86function resetPipeline() {
87  if (stageInterval) clearInterval(stageInterval)
88  pipelineRunning.value = false
89  currentStage.value = 0
90  pipelineProgress.value = 0
91
92  pipelineStages.value.forEach((stage, index) => {
93    stage.status = index === 0 ? 'completed' : 'idle'
94  })
95
96  logs.value = ['→ Pipeline réinitialisé. Prêt à démarrer...']
97}
98</script>
99
100<template>
101  <div class="cicd-dashboard">
102    <div class="header">
103      <h1>Moniteur du pipeline CI/CD</h1>
104      <p>Système de déploiement automatisé NOVA LAB</p>
105    </div>
106
107    <div class="pipeline-status" :class="pipelineStatus">
108      <div class="status-indicator">
109        <template v-if="pipelineStatus === 'idle'">Inactif</template>
110        <template v-else-if="pipelineStatus === 'running'">En cours</template>
111        <template v-else-if="pipelineStatus === 'success'">Succès</template>
112        <template v-else-if="pipelineStatus === 'failed'">Échec</template>
113      </div>
114      <div class="overall-progress">
115        Progression : {{ overallProgress }}%
116      </div>
117    </div>
118
119    <div class="pipeline-visualization">
120      <div
121        v-for="stage in pipelineStages"
122        :key="stage.id"
123        class="pipeline-stage"
124        :class="stage.status"
125      >
126        <div class="stage-icon">{{ stage.icon }}</div>
127        <div class="stage-name">{{ stage.name }}</div>
128        <div class="stage-status">
129          <template v-if="stage.status === 'idle'"></template>
130          <template v-else-if="stage.status === 'running'"></template>
131          <template v-else-if="stage.status === 'completed'"></template>
132          <template v-else-if="stage.status === 'failed'"></template>
133        </div>
134        <div v-if="stage.status === 'running'" class="stage-progress">
135          <div class="progress-bar">
136            <div class="progress-fill" :style="{ width: pipelineProgress + '%' }"></div>
137          </div>
138        </div>
139      </div>
140    </div>
141
142    <div class="logs-panel">
143      <h3>Logs du pipeline</h3>
144      <div class="logs">
145        <div v-for="(log, index) in logs" :key="index" class="log-entry">
146          {{ log }}
147        </div>
148      </div>
149    </div>
150
151    <div class="controls">
152      <button
153        @click="startPipeline"
154        :disabled="pipelineRunning"
155        class="start-btn"
156      >
157        ▶Démarrer le pipeline
158      </button>
159      <button
160        @click="resetPipeline"
161        class="reset-btn"
162      >
163        Réinitialiser
164      </button>
165    </div>
166  </div>
167</template>
168
169<style scoped>
170.cicd-dashboard {
171  background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
172  color: #00ff88;
173  padding: 2rem;
174  min-height: 100vh;
175  font-family: 'Courier New', monospace;
176}
177
178.header {
179  text-align: center;
180  margin-bottom: 2rem;
181  border-bottom: 2px solid #00b4d8;
182  padding-bottom: 1rem;
183}
184
185.header h1 {
186  color: #00ff88;
187  text-shadow: 0 0 10px #00ff88;
188  margin: 0;
189}
190
191.pipeline-status {
192  display: flex;
193  justify-content: space-between;
194  align-items: center;
195  padding: 1.5rem;
196  border-radius: 8px;
197  margin-bottom: 2rem;
198  border: 2px solid;
199}
200
201.pipeline-status.idle {
202  background: rgba(128, 128, 128, 0.1);
203  border-color: #888;
204}
205
206.pipeline-status.running {
207  background: rgba(0, 180, 216, 0.2);
208  border-color: #00b4d8;
209  animation: pulse 2s infinite;
210}
211
212.pipeline-status.success {
213  background: rgba(0, 255, 136, 0.2);
214  border-color: #00ff88;
215}
216
217.pipeline-status.failed {
218  background: rgba(255, 0, 0, 0.2);
219  border-color: #ff0000;
220}
221
222.status-indicator {
223  font-size: 1.3rem;
224  font-weight: bold;
225}
226
227.pipeline-visualization {
228  display: flex;
229  flex-direction: column;
230  gap: 1rem;
231  margin-bottom: 2rem;
232}
233
234.pipeline-stage {
235  background: rgba(0, 180, 216, 0.1);
236  border: 2px solid #00b4d8;
237  border-radius: 8px;
238  padding: 1rem;
239  display: grid;
240  grid-template-columns: 50px 1fr 50px;
241  align-items: center;
242  gap: 1rem;
243  transition: all 0.3s;
244}
245
246.pipeline-stage.running {
247  border-color: #00b4d8;
248  background: rgba(0, 180, 216, 0.2);
249  box-shadow: 0 0 20px rgba(0, 180, 216, 0.3);
250}
251
252.pipeline-stage.completed {
253  border-color: #00ff88;
254  background: rgba(0, 255, 136, 0.1);
255}
256
257.stage-icon {
258  font-size: 2rem;
259  text-align: center;
260}
261
262.stage-name {
263  color: #fff;
264  font-size: 1.1rem;
265}
266
267.stage-status {
268  font-size: 1.5rem;
269  text-align: center;
270}
271
272.stage-progress {
273  grid-column: 1 / -1;
274  margin-top: 0.5rem;
275}
276
277.progress-bar {
278  height: 8px;
279  background: rgba(0, 255, 136, 0.1);
280  border-radius: 4px;
281  overflow: hidden;
282}
283
284.progress-fill {
285  height: 100%;
286  background: linear-gradient(90deg, #00b4d8, #00ff88);
287  transition: width 0.3s;
288}
289
290.logs-panel {
291  background: rgba(0, 180, 216, 0.1);
292  border: 1px solid #00b4d8;
293  border-radius: 8px;
294  padding: 1.5rem;
295  margin-bottom: 2rem;
296}
297
298.logs-panel h3 {
299  color: #00b4d8;
300  margin-top: 0;
301}
302
303.logs {
304  background: #000;
305  border-radius: 4px;
306  padding: 1rem;
307  max-height: 200px;
308  overflow-y: auto;
309  font-family: monospace;
310}
311
312.log-entry {
313  color: #00ff88;
314  margin-bottom: 0.5rem;
315  font-size: 0.9rem;
316}
317
318.controls {
319  display: flex;
320  gap: 1rem;
321  justify-content: center;
322}
323
324.start-btn, .reset-btn {
325  padding: 1rem 2rem;
326  font-size: 1rem;
327  font-family: 'Courier New', monospace;
328  border-radius: 8px;
329  cursor: pointer;
330  border: 2px solid;
331  transition: all 0.3s;
332}
333
334.start-btn {
335  background: #00ff88;
336  color: #0a0e27;
337  border-color: #00ff88;
338}
339
340.start-btn:disabled {
341  opacity: 0.5;
342  cursor: not-allowed;
343}
344
345.start-btn:hover:not(:disabled) {
346  box-shadow: 0 0 20px #00ff88;
347}
348
349.reset-btn {
350  background: rgba(0, 180, 216, 0.2);
351  color: #00b4d8;
352  border-color: #00b4d8;
353}
354
355.reset-btn:hover {
356  background: #00b4d8;
357  color: #0a0e27;
358}
359</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 déployer une application Vue sur la plateforme Vercel ?

  2. 2. Quels réglages de build sont nécessaires pour un déploiement sur Netlify ?

Voici 2 des 4 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Éditeur de code

    Complète le fichier vercel.json avec buildCommand, outputDirectory, framework et rewrites pour la configuration SPA.

  • Tri vertical

    Ordonne les étapes du pipeline CI/CD, de la première à la dernière :

  • Tri vertical

    Ordonne les étapes du pipeline CI/CD, du code à la production :

  • Clique dans l'ordre

    Ordonne la commande de build d'une image Docker :

  • Tri horizontal

    Ordonne la configuration Vite pour GitHub Pages :

  • Éditeur de code

    Complète un Dockerfile avec un build multi-étapes : l'étape de build (node) et l'étape de production (nginx).