Cours Vue.js · Module 12: Tests et déploiement
Déploiement
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 --prodUn 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 --prodLa 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 = 200Le 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-pagesUn 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 /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. Comment déployer une application Vue sur la plateforme Vercel ?
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).