Cours Vue.js · Module 12: Tests et déploiement
Le build de production
Dans cette leçon14
Pendant les trois leçons précédentes, tu as contrôlé les systèmes de la station : les composants, les composables, le store, puis les parcours complets de l'opérateur dans les tests E2E. Tous les voyants sont au vert - mais ils sont au vert dans le laboratoire NOVA LAB, sur ta machine, avec un serveur de développement qui tourne. Or le panneau de télémétrie doit partir sur l'atterrisseur : dans le navigateur d'un technicien qui l'ouvre depuis l'autre bout de la station, à travers une liaison dont la latence se compte en minutes. Le serveur de développement, lui, ne fera pas le voyage.
La commande npm run dev, que tu utilises depuis la toute première leçon, sert au navigateur tes fichiers sources presque tels quels. Elle ne transforme un fichier .vue qu'au moment où le navigateur le réclame, et laisse le reste du travail au système de modules natif du navigateur. Le démarrage est donc instantané, mais le prix à payer, ce sont des centaines de requêtes séparées, aucune minification et un dossier node_modules qui attend en coulisses. Pour l'expédition, il te faut exactement l'inverse : une charge utile unique, calculée et emballée, que le navigateur à l'autre bout de la liaison téléchargera en quelques fichiers.
Une seule commande qui emballe la charge utile
L'emballage est confié à un mode distinct de Vite, déclenché par le script build. Vite bascule alors du service de fichiers vers la construction : il lit index.html comme point d'entrée, parcourt tous les imports, compile chaque composant, puis confie l'ensemble à Rolldown, le bundler écrit en Rust que Vite utilise depuis la version 8. Rolldown assemble les modules en bundles, et le minificateur Oxc raccourcit le résultat. C'est la même commande dans tous les projets Vue montés sur Vite, que tu construises un panneau de télémétrie ou un jeu pour l'équipage.
1npm run buildAu bout d'une quinzaine de secondes, un dossier dist apparaît à la racine du projet. Remarque bien ce qui n'a pas changé : pas un seul de tes fichiers sources. Le build se contente de lire le dossier src et d'écrire dans dist - c'est pour cela que tu peux le relancer aussi souvent que tu le veux, et c'est pour cela que dist va dans le .gitignore et non dans le dépôt.
Retiens le nom de ce script à la lettre car c'est là que les erreurs se glissent le plus facilement. npm run ne connaît aucune commande magique : il n'exécute que les scripts déclarés dans la section scripts du fichier package.json, et le modèle de projet Vue avec Vite en prévoit exactement trois - dev, build et preview. Ni npm run prod ni npm run compile n'existent dans un modèle standard, et tous deux se terminent par un message annonçant un script introuvable. npm run deploy est parfois ajouté à la main, mais c'est un script d'expédition et non de construction : si tu en croises un dans un projet, il appelle presque toujours npm run build en interne, parce qu'il faut bien avoir quelque chose à expédier.
Inspecter la charge utile avant le décollage
L'application construite est souvent le premier endroit où sortent des pannes invisibles au laboratoire : un chemin de base erroné, une image assemblée dynamiquement, une bibliothèque qui ne fonctionnait que parce que le serveur de développement lui rendait un service au passage. C'est la raison d'être du troisième script, preview. Il démarre un simple serveur de fichiers statiques et sert le contenu de dist - exactement les fichiers que tu vas expédier. Il affiche son adresse dans la console ; elle diffère de celle du serveur de développement, si bien que les deux peuvent tourner en même temps. Le mieux est d'enchaîner les deux commandes, reliées par une double esperluette.
1npm run build && npm run previewLe signe && est un simple opérateur du shell : la seconde commande ne démarre que si la première se termine avec succès. Si le build s'interrompt sur une erreur d'import, l'aperçu ne se lèvera pas du tout et tu ne risqueras pas d'examiner par mégarde le contenu périmé de dist. Et une mise en garde, pour éviter tout malentendu : preview est un instrument de contrôle, pas un serveur de production. Pas de compression, pas d'en-têtes de cache, pas de certificat - il sert à regarder la charge utile, pas à la mettre à la disposition de l'équipage.
Ce que contient vraiment le dossier dist
Puisque dist est tout ce qui partira sur l'atterrisseur, autant savoir ce qui s'y trouve exactement. La structure est la même à chaque fois : un fichier index.html à la racine et un dossier assets avec tout le reste. Voici le contenu typique après la construction d'un panneau comportant quelques vues.
1dist/
2 index.html
3 assets/
4 index-4f3a9b1c.js
5 index-9c2e7d84.css
6 vendor-1b7e0a52.js
7 TelemetryView-6d41f0ab.jsLa suite de caractères dans le nom du fichier est une empreinte calculée à partir de son contenu. Change une ligne de code : l'empreinte change, donc le nom du fichier aussi, et le navigateur du technicien n'a plus aucun moyen de resservir l'ancienne version depuis son cache. Tu n'as rien à corriger à la main pour autant, car l'index.html présent dans dist référence déjà les nouveaux noms. C'est pour cette raison que le build génère son propre index.html au lieu de recopier le tien tel quel.
Dans dist, tu ne trouveras que des fichiers JS, CSS et HTML minifiés, prêts pour le déploiement. Pas un seul fichier .vue : les composants ont été compilés en fonctions de rendu et fondus dans les bundles. Pas de node_modules non plus, car les morceaux de bibliothèques que tu utilises réellement se trouvent à l'intérieur des bundles et le reste a été écarté. Pas de vite.config.js ni de fichiers de configuration du laboratoire, puisque ce sont des entrées du processus de construction et non son résultat.
Il y a un dossier facile à confondre avec le résultat : public. Lui aussi est une entrée - tu y déposes les fichiers qui doivent arriver dans dist sans aucun traitement, par exemple robots.txt ou l'icône de la station. Les noms build et out circulent également dans les documentations, mais ils appartiennent à d'autres outils : build est la convention des projets issus de Create React App et de webpack, out est le dossier d'export statique de Next.js. Dans Vite, le dossier de sortie par défaut est dist - c'est là que tu chercheras le résultat après chaque build.
La section build dans vite.config.js
Les réglages par défaut sont raisonnables, mais tôt ou tard la mission imposera des changements : l'hébergeur attend un autre nom de dossier, quelqu'un réclame des cartes de sources, quelqu'un d'autre une minification plus poussée. Tout cela se décrit dans le fichier vite.config.js, à l'intérieur de l'objet build. Les quatre options par lesquelles tu commenceras se lisent ainsi : outDir est le nom du dossier de sortie, assetsDir le sous-dossier des bundles et des styles à l'intérieur de celui-ci, sourcemap décide si des cartes permettant de déboguer le code minifié en termes de tes fichiers sources seront produites à côté des bundles, et minify désigne l'outil chargé de raccourcir ce code.
1// vite.config.js
2import { defineConfig } from 'vite'
3import vue from '@vitejs/plugin-vue'
4
5export default defineConfig({
6 plugins: [vue()],
7
8 build: {
9 outDir: 'dist',
10 assetsDir: 'assets',
11 sourcemap: false,
12 minify: 'oxc'
13 }
14})Ce fichier ne change rien - et c'est ce qu'il a de plus intéressant. Tu y as inscrit exactement les valeurs que Vite adopte de lui-même, donc le build se comportera comme il vient de le faire. Considère-le comme une liste de contrôle : tu sais désormais où aller le jour où l'hébergeur exigera un dossier portant un autre nom. La fonction defineConfig ne configure rien du tout, elle se contente de fournir les types à l'éditeur, si bien qu'une faute de frappe dans le nom d'une option s'allume immédiatement. Les cartes de sources, je te recommande de les laisser désactivées dans une application exposée publiquement, car elles publient avec elles le code source lisible du panneau ; si un système de rapport d'erreurs en a besoin, mets sourcemap: 'hidden' - les cartes seront produites, mais le bundle n'y renverra pas.
Laisse pour l'instant trois autres options de cette section tranquilles, mais connais-les au moins de nom. assetsInlineLimit fixe le seuil, en octets, en dessous duquel un petit fichier, une icône SVG par exemple, sera collé directement dans le code sous forme de data URI au lieu d'atterrir à part, soit une requête de moins. cssCodeSplit active le découpage des styles en morceaux correspondant aux bundles. chunkSizeWarningLimit est le seuil au-delà duquel Vite affiche un avertissement sur un bundle trop gros ; il compte la taille après minification mais avant la compression du serveur, donc le transfert réel sera plus léger que le nombre affiché dans la console.
Faire taire la console avec le minificateur terser
Il reste sûrement quelques appels à console.log dans le code du panneau - inestimables au laboratoire, inutiles sur l'atterrisseur. C'est le minificateur qui se charge de supprimer ces appels - il suffit de le lui demander explicitement. Le minificateur par défaut de Vite 8 est Oxc (minify: 'oxc') : il est très rapide et c'est lui qui assure l'essentiel du raccourcissement du code, tandis que l'ancienne valeur 'esbuild' est dépréciée et disparaîtra dans les prochaines versions. L'alternative s'appelle terser, plus lente, mais dotée d'un jeu de commutateurs minutieux - entre autres drop_console et drop_debugger, qui suppriment du bundle respectivement les appels à la console et les instructions debugger. Les réglages de terser se donnent dans l'objet terserOptions, section compress.
1// vite.config.js
2import { defineConfig } from 'vite'
3import vue from '@vitejs/plugin-vue'
4
5export default defineConfig({
6 plugins: [vue()],
7
8 build: {
9 minify: 'terser',
10 terserOptions: {
11 compress: {
12 drop_console: true,
13 drop_debugger: true
14 }
15 }
16 }
17})Il y a ici un piège dont les configurations qui circulent sur le web ne disent en général pas un mot : Vite n'installe pas terser avec lui. Si tu écris minify: 'terser' sans ajouter le paquet terser aux dépendances de développement, le build s'interrompt sur un message annonçant que le minificateur est indisponible, et il suffit alors de lancer npm install -D terser. Remarque aussi ce que cette configuration ne fait pas : elle ne touche ni à tes fichiers sources, ni au mode développement, ni à la console du navigateur elle-même. Le console.log reste dans ton code et continue d'écrire dans la console avec npm run dev ; il disparaît uniquement du contenu de dist.
Ma recommandation : reste sur Oxc par défaut - le build est nettement plus rapide, et tu peux supprimer les logs sans terser en réglant dropConsole: true dans build.rolldownOptions.output.minify.compress. Prends terser en connaissance de cause, quand tu as besoin de l'un de ses commutateurs minutieux. L'option esbuild.drop, que tu trouveras dans d'anciens tutoriels, est dépréciée dans Vite 8, tout comme l'option esbuild entière.
Découper la charge utile en bundles
Par défaut, Vite assemble ton code en un bundle unique, ce qui a un effet secondaire désagréable au moment des mises à jour. Tu corriges une étiquette du panneau, le nom du fichier change à cause de la nouvelle empreinte, et le technicien retélécharge la totalité - Vue, le router et Pinia compris, alors qu'ils n'ont pas bougé depuis des mois. Le remède consiste à séparer le code en bundles au rythme de changement différent. C'est le rôle de l'option codeSplitting, que tu donnes dans build.rolldownOptions.output, c'est-à-dire dans les réglages transmis directement à Rolldown. rolldownOptions prend place dans le même objet build que outDir, minify et sourcemap, si bien que toute la configuration du build tient dans un seul endroit. L'option prend une liste de groupes groups : chaque groupe a un nom name, qui deviendra le nom du futur 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 anciens tutoriels montrent ici manualChunks dans rollupOptions : Vite 8 ne prend plus en charge sa forme objet et interrompt le build avec une erreur, et il a déprécié la forme fonction.
1// vite.config.js
2import { defineConfig } from 'vite'
3import vue from '@vitejs/plugin-vue'
4
5export default defineConfig({
6 plugins: [vue()],
7
8 build: {
9 rolldownOptions: {
10 output: {
11 codeSplitting: {
12 groups: [
13 { name: 'vendor', test: /node_modules[\\/](vue|@vue|vue-router|pinia)[\\/]/ }
14 ]
15 }
16 }
17 }
18 }
19})Le groupe attrape aussi les paquets @vue, car c'est d'eux qu'est fait l'intérieur de Vue lui-même. Après un tel build, tu verras dans le dossier assets un fichier distinct commençant par le mot vendor. Les bibliothèques y ont atterri ensemble et, tant que tu ne changes pas leurs versions, son nom restera identique : le navigateur le télécharge une fois et le garde en cache malgré les livraisons successives du panneau. Et voici ce qui n'a pas changé : tu n'as touché à aucun import de ton code. Tu écris toujours import { ref } from 'vue' exactement là où tu l'écrivais, et la décision de répartir les fichiers appartient au seul build.
Une règle plutôt qu'une énumération
La liste manuelle fait l'affaire avec trois bibliothèques. Quand le package.json gonfle, il devient plus commode de donner une règle que d'énumérer les noms un par un. Le champ name d'un groupe accepte en effet aussi une fonction : Rolldown l'appelle pour chaque module en lui passant son chemin dans l'argument moduleId, et traite la chaîne renvoyée comme le nom du bundle. Quand la fonction renvoie null, le groupe ignore le module, qui atterrit là où il serait allé par défaut.
1// vite.config.js
2import { defineConfig } from 'vite'
3import vue from '@vitejs/plugin-vue'
4
5export default defineConfig({
6 plugins: [vue()],
7
8 build: {
9 rolldownOptions: {
10 output: {
11 codeSplitting: {
12 groups: [
13 {
14 name(moduleId) {
15 if (moduleId.includes('node_modules')) {
16 return 'vendor'
17 }
18 return null
19 }
20 }
21 ]
22 }
23 }
24 }
25 }
26})Cette version dit brièvement ceci : tout ce qui provient de node_modules atterrit dans le bundle vendor, et le reste ne bouge pas. En ajoutant une condition supplémentaire, tu peux isoler une seule bibliothèque lourde, par exemple le moteur de graphiques de télémétrie, utile sur un seul écran. Puisque tu as le choix, je te dis franchement ce que je recommande : commence par un groupe avec une expression régulière, c'est lisible et tu vois exactement ce qui va où. Ne passe à la fonction que lorsque la liste commence à s'éloigner de la réalité - une règle trop agressive peut éclater les bundles au point que le navigateur télécharge au démarrage plus de fichiers qu'il ne lui en faut.
Les paramètres de mission, autrement dit les variables d'environnement
Le panneau de télémétrie demande ses données au serveur de mission. Au laboratoire, ce serveur tourne sur ta machine ; sur l'atterrisseur, il se trouve à une tout autre adresse. Écrire cette adresse en dur dans le code signifie qu'avant chaque build quelqu'un doit penser à la remplacer ; tôt ou tard il oubliera, et le panneau de production ira frapper à la porte de localhost. Vite règle cela avec des fichiers .env, lus au moment de la construction. Le fichier .env s'applique toujours, .env.production s'y ajoute lors du build de production et .env.development lors du serveur de développement, sachant que les valeurs du fichier le plus spécifique l'emportent.
1# .env
2VITE_API_URL=http://localhost:3000
3
4# .env.production
5VITE_API_URL=https://telemetry.nova-lab.mars/apiLe préfixe VITE_ n'est pas un ornement, c'est un sas de sécurité. Dans le code qui part vers le navigateur, Vite ne laisse entrer que les variables portant ce préfixe ; toutes les autres restent du côté du processus de construction. La conclusion est plus tranchante qu'il n'y paraît : puisqu'une variable préfixée atterrit dans le bundle, quiconque ouvre dist en lira la valeur. Des adresses et des drapeaux de fonctionnalité, oui ; des clés et des mots de passe, jamais.
C'est ici aussi que se cache une confusion classique. Le préfixe VUE_APP_ et la lecture par process.env sont la convention de Vue CLI, c'est-à-dire du prédécesseur bâti sur webpack. Dans un projet Vite, l'objet process n'existe tout simplement pas dans le navigateur et une telle lecture se termine par une erreur. Un fichier config.js maison avec des noms de variables arbitraires n'aide pas davantage : c'est un module ordinaire, qui entrera dans le bundle comme le reste du code, mais sans le moindre mécanisme de bascule des valeurs entre environnements. Et pour lever tout doute : affirmer que Vite ne prend pas du tout en charge les variables d'environnement est faux. Il les prend en charge, mais à ses propres conditions.
Lire une variable dans le code
Du côté du code, tu lis les variables dans l'objet import.meta.env. import.meta est un élément standard de JavaScript, un objet contenant les métadonnées du module courant, et Vite y ajoute le champ env. L'écriture complète se compose donc de trois parties, dans cet ordre précis : import.meta, puis .env, puis le nom de la variable avec son préfixe, ce qui donne import.meta.env.VITE_API_URL.
1// api/telemetry.js
2const apiUrl = import.meta.env.VITE_API_URL
3
4export async function fetchModuleTelemetry(moduleId) {
5 const response = await fetch(`${apiUrl}/modules/${moduleId}`)
6
7 if (!response.ok) {
8 throw new Error('Echec de la requete de telemetrie')
9 }
10
11 return response.json()
12}Il est important que tu comprennes ce qui se passe réellement ici. Ce n'est pas une lecture dans un dictionnaire pendant l'exécution de l'application : Vite remplace l'expression entière import.meta.env.VITE_API_URL par un littéral de texte dès la construction. Dans le fichier déposé dans dist, il ne reste aucune trace d'import.meta, seulement une adresse toute prête. Cela a une conséquence pratique : le nom de la variable doit être écrit littéralement. Une clé assemblée à la volée, à partir du préfixe et d'un nom gardé dans une variable, ne sera pas remplacée et donnera undefined. La même écriture fonctionne à l'identique dans le <script setup> d'un composant, car c'est le même remplacement, au même moment.
Le mode dans lequel tourne l'application
À côté de tes variables, Vite en dépose quelques-unes des siennes dans import.meta.env. La première est MODE, le nom du mode sous forme de texte : avec un simple npm run dev ce sera development, et après npm run build ce sera production. Les deux suivantes, DEV et PROD, portent la même information sous forme de valeurs booléennes, ce qui les rend commodes dans les conditions.
1if (import.meta.env.DEV) {
2 console.log('Mode du centre de controle:', import.meta.env.MODE)
3}Cette condition ne coûte pas un octet dans la version de production. Puisque import.meta.env.DEV est remplacé par un littéral, le bundle de production contient une condition toujours fausse, et le minificateur jette la branche morte avec tout son contenu. C'est un motif très commode pour les panneaux de diagnostic destinés à l'équipage du laboratoire : tu les écris normalement, à côté du reste du code, et le technicien de l'atterrisseur ne les télécharge même pas.
Les modes ne s'arrêtent pas à ces deux noms. La commande npm run build -- --mode staging construit l'application dans un mode nommé staging et charge le fichier .env.staging - c'est la façon standard de gérer un environnement de test, quelque part entre le laboratoire et l'atterrisseur. Le double tiret devant --mode est indispensable, car c'est lui qui ordonne au gestionnaire de paquets de transmettre l'argument plus loin, jusqu'à Vite. MODE vaudra alors staging, tandis que PROD restera vrai par défaut, car ce drapeau dépend du type de commande et non du nom du mode.
Ce que Vite fait sans qu'on le lui demande
Avant de commencer à serrer quoi que ce soit, il vaut la peine de savoir combien de travail le build accomplit à ta place et, surtout, quelle technique fait quoi, car ces quatre noms sont très souvent confondus.
Tree shaking, c'est la suppression du code inutilisé. Rolldown lit les imports et les exports des modules ES, en construit un graphe de dépendances et retire du bundle tout export auquel personne ne fait référence. Tu importes une seule fonction d'une grosse bibliothèque : c'est cette seule fonction, avec ce dont elle a elle-même besoin, qui arrive dans dist. Voilà pourquoi les imports statiques en haut du fichier sont aujourd'hui la norme, car eux seuls donnent à l'outil la certitude de ce qui sert vraiment.
Minification, c'est le raccourcissement du code qui reste. Les espaces, les indentations et les commentaires disparaissent, et les noms des variables locales se réduisent à une lettre. Note la différence : le minificateur ne se demande pas si un module est utile à quelqu'un, il ne fait que le comprimer. La seule suppression des commentaires n'est qu'un fragment mineur de ce travail, et certainement pas le mécanisme qui élimine le code inutilisé.
Code splitting, c'est la division du résultat en plusieurs fichiers, exactement ce que tu viens de faire avec codeSplitting. Ici rien ne se perd, le code est seulement déplacé dans un bundle distinct.
Lazy loading, en revanche, est une stratégie qui s'applique pendant l'exécution : le bundle né du découpage n'est téléchargé qu'au moment où l'utilisateur en a réellement besoin. C'est une conséquence du code splitting, et non une technique de nettoyage du code à part entière.
Il reste deux malentendus à écarter. Le build n'emballe rien dans une archive ZIP, car le navigateur ne saurait pas exécuter un tel fichier. La compression existe bien sûr, elle s'appelle gzip ou brotli, elle se produit au niveau du transport HTTP, elle relève du serveur et n'a rien à voir avec la suppression de code. Il est également faux de dire que Vite n'optimise rien et qu'il faudrait un outil externe : vite build, c'est Rolldown avec la panoplie complète d'optimisations activée par défaut.
Les routes paresseuses en pratique
Le code splitting n'a pas besoin d'être manuel. Le découpage le plus efficace naît de l'import dynamique, c'est-à-dire de l'appel import() à l'intérieur d'une fonction plutôt que de l'import statique en haut du fichier. Rolldown traite un tel endroit comme une frontière : tout ce qui se trouve derrière part dans un bundle séparé. Dans le router, cela se traduit ainsi : au lieu du composant tout prêt, tu fournis une fonction qui ira le chercher.
1// router/index.js
2import { createRouter, createWebHistory } from 'vue-router'
3import MissionControl from '../views/MissionControl.vue'
4
5const routes = [
6 { path: '/', component: MissionControl },
7 { path: '/telemetry', component: () => import('../views/TelemetryView.vue') },
8 { path: '/crew', component: () => import('../views/CrewView.vue') }
9]
10
11export default createRouter({
12 history: createWebHistory(),
13 routes
14})L'écran d'accueil entre dans le bundle principal, puisque le technicien le verra de toute façon. Les deux autres vues reçoivent leur propre fichier, téléchargé seulement à la première entrée sur la route correspondante - c'est précisément un nom de ce genre que tu as vu dans l'exemple de contenu de dist. Ce qui n'a pas changé : les composants eux-mêmes. Le fichier TelemetryView.vue est identique à ce qu'il était, seule la façon dont le router le désigne a changé. La même mécanique s'applique à un composant isolé, avec defineAsyncComponent enroulé autour du même import(), et tu t'en serviras dans la leçon de projet.
Mesure avant d'optimiser
Optimiser au jugé revient d'ordinaire à serrer des boulons qui ne pèsent rien. Donc, avant de toucher à autre chose qu'aux réglages par défaut, mesure. Pour cela, il existe le plugin rollup-plugin-visualizer, ajouté aux dépendances de développement par npm install -D rollup-plugin-visualizer, puis inscrit dans la liste plugins. L'option open ouvre le rapport dans le navigateur dès la fin du build, tandis que gzipSize et brotliSize y ajoutent les tailles après compression.
1// vite.config.js
2import { defineConfig } from 'vite'
3import vue from '@vitejs/plugin-vue'
4import { visualizer } from 'rollup-plugin-visualizer'
5
6export default defineConfig({
7 plugins: [
8 vue(),
9 visualizer({
10 open: true,
11 gzipSize: true,
12 brotliSize: true
13 })
14 ]
15})Le rapport est une carte de rectangles : plus la surface est grande, plus le module est lourd. Un seul regard suffit d'habitude pour voir que la moitié du bundle est dévorée par une bibliothèque tirée pour une seule fonction. Regarde avant tout les tailles compressées, car ce sont elles qui correspondent à ce qui passera réellement par la liaison jusqu'à l'atterrisseur ; la taille brute est parfois plusieurs fois supérieure et effraie inutilement. Le plugin lui-même ne modifie rien dans le bundle, il ne fait que mesurer, alors garde-le à la fin de la liste plugins.
Les images pèsent plus lourd que le code
Dernière étape, car dans un panneau typique ce n'est pas le code qui pèse le plus. Une seule photo brute de la surface de Mars peut peser plus lourd que toute l'application, Vue compris. Vite ne compresse pas les images de lui-même : il les déplace dans dist et ajoute une empreinte à leur nom, mais ne touche pas à leur contenu. Pour la compression, il y a le plugin vite-plugin-image-optimizer, installé par npm install -D vite-plugin-image-optimizer. Il a encore besoin des paquets sharp et svgo, qu'il utilise en dessous. Pour chaque format, tu indiques quality, c'est-à-dire la qualité sur une échelle allant jusqu'à cent.
1// vite.config.js
2import { defineConfig } from 'vite'
3import vue from '@vitejs/plugin-vue'
4import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
5
6export default defineConfig({
7 plugins: [
8 vue(),
9 ViteImageOptimizer({
10 jpg: { quality: 80 },
11 png: { quality: 80 },
12 webp: { quality: 80 }
13 })
14 ]
15})La valeur 80 correspond à une compression avec perte dont la différence ne se voit en général pas à l'œil nu, alors que le fichier maigrit de plusieurs dizaines de pour cent. Le plus important reste toutefois ce que tu n'as pas à faire : tu ne modifies pas une seule balise <img> dans les templates et tu ne remplaces aucun fichier du dossier src. Le plugin travaille sur la copie qui part vers dist, donc les originaux du dépôt restent intacts et tu peux à tout moment changer les réglages et tout reconstruire.
Une charge utile construite, inspectée et mesurée, c'est tout ce qu'il faut avant le décollage ; reste la question de la plateforme vers laquelle l'expédier, et ce sera le sujet de la prochaine leçon. D'ici là, prends ce réflexe : d'abord npm run build, ensuite npm run preview, puis un regard sur le rapport des tailles, et seulement à la toute fin quoi que ce soit d'autre.
Retiens bien : npm run build n'est pas une manière de plus de lancer l'application, c'est l'emballage du laboratoire tout entier en une charge utile qui partira sans toi.
Code de cette leçon : App.vue
1<script setup>
2import { ref, computed } from 'vue'
3
4// NOVA LAB - Comparaison des plateformes de déploiement
5const platforms = ref([
6 {
7 id: 1,
8 name: 'Vercel',
9 logo: '▲',
10 features: ['Zéro config', 'Réseau Edge', 'Fonctions serverless', 'SSL automatique'],
11 buildTime: 45,
12 pricing: 'Offre gratuite disponible',
13 bestFor: 'Apps Next.js, React',
14 color: '#000'
15 },
16 {
17 id: 2,
18 name: 'Netlify',
19 logo: '◆',
20 features: ['Déploiement continu', 'Gestion des formulaires', 'Split testing', 'Fonctions Edge'],
21 buildTime: 60,
22 pricing: 'Offre gratuite disponible',
23 bestFor: 'Sites statiques, JAMstack',
24 color: '#00C7B7'
25 },
26 {
27 id: 3,
28 name: 'Render',
29 logo: '●',
30 features: ['Apps full stack', 'Bases de données', 'Tâches cron', 'Workers en arrière-plan'],
31 buildTime: 90,
32 pricing: 'Offre gratuite disponible',
33 bestFor: 'Apps full-stack',
34 color: '#46E3B7'
35 },
36 {
37 id: 4,
38 name: 'Railway',
39 logo: '',
40 features: ['Déploiement en un clic', 'Bases de données', 'Volumes', 'Réseau privé'],
41 buildTime: 75,
42 pricing: "À l'usage",
43 bestFor: 'Services backend',
44 color: '#A020F0'
45 }
46])
47
48const selectedPlatform = ref(null)
49const deployStatus = ref('idle') // idle, deploying, deployed, error (inactif, déploiement, déployé, erreur)
50
51const deployProgress = ref(0)
52let deployInterval = null
53
54function selectPlatform(platform) {
55 selectedPlatform.value = platform
56 deployStatus.value = 'idle'
57 deployProgress.value = 0
58}
59
60function startDeployment() {
61 if (!selectedPlatform.value) return
62
63 deployStatus.value = 'deploying'
64 deployProgress.value = 0
65
66 const buildTime = selectedPlatform.value.buildTime
67 const increment = 100 / (buildTime / 10)
68
69 deployInterval = setInterval(() => {
70 deployProgress.value += increment
71
72 if (deployProgress.value >= 100) {
73 clearInterval(deployInterval)
74 deployProgress.value = 100
75 deployStatus.value = 'deployed'
76 }
77 }, 100)
78}
79
80function resetDeployment() {
81 if (deployInterval) {
82 clearInterval(deployInterval)
83 }
84 deployStatus.value = 'idle'
85 deployProgress.value = 0
86 selectedPlatform.value = null
87}
88
89const deploymentUrl = computed(() => {
90 if (!selectedPlatform.value || deployStatus.value !== 'deployed') return ''
91 const platformName = selectedPlatform.value.name.toLowerCase()
92 return `https://nova-lab-mars.${platformName}.app`
93})
94</script>
95
96<template>
97 <div class="deployment-center">
98 <div class="header">
99 <h1>Sélecteur de plateforme de déploiement</h1>
100 <p>NOVA LAB - Choisis ta plateforme de lancement</p>
101 </div>
102
103 <div class="platforms-grid">
104 <div
105 v-for="platform in platforms"
106 :key="platform.id"
107 class="platform-card"
108 :class="{ selected: selectedPlatform?.id === platform.id }"
109 @click="selectPlatform(platform)"
110 >
111 <div class="platform-logo">{{ platform.logo }}</div>
112 <h3>{{ platform.name }}</h3>
113 <div class="platform-info">
114 <div class="build-time">
115 ⏱build ~{{ platform.buildTime }}s
116 </div>
117 <div class="pricing">{{ platform.pricing }}</div>
118 <div class="best-for">{{ platform.bestFor }}</div>
119 </div>
120 <div class="features">
121 <div v-for="feature in platform.features" :key="feature" class="feature">
122 {{ feature }}
123 </div>
124 </div>
125 </div>
126 </div>
127
128 <div v-if="selectedPlatform" class="deploy-panel">
129 <h2>Déployer sur {{ selectedPlatform.name }}</h2>
130
131 <div class="deploy-status" :class="deployStatus">
132 <template v-if="deployStatus === 'idle'">
133 Prêt à déployer l'app Mars Mission
134 </template>
135 <template v-else-if="deployStatus === 'deploying'">
136 Déploiement... {{ Math.round(deployProgress) }}%
137 </template>
138 <template v-else-if="deployStatus === 'deployed'">
139 Déployé avec succès !
140 </template>
141 </div>
142
143 <div v-if="deployStatus === 'deploying'" class="progress-bar">
144 <div class="progress-fill" :style="{ width: deployProgress + '%' }"></div>
145 </div>
146
147 <div v-if="deployStatus === 'deployed'" class="deployment-info">
148 <p class="url"><a :href="deploymentUrl" target="_blank">{{ deploymentUrl }}</a></p>
149 <p class="success-msg">Ta mission Mars est maintenant en ligne sur {{ selectedPlatform.name }} !</p>
150 </div>
151
152 <div class="actions">
153 <button
154 v-if="deployStatus === 'idle'"
155 @click="startDeployment"
156 class="deploy-btn"
157 >
158 Déployer maintenant
159 </button>
160 <button
161 v-if="deployStatus === 'deployed'"
162 @click="resetDeployment"
163 class="reset-btn"
164 >
165 Nouveau déploiement
166 </button>
167 </div>
168 </div>
169 </div>
170</template>
171
172<style scoped>
173.deployment-center {
174 background: #0a0e27;
175 color: #00ff88;
176 padding: 2rem;
177 min-height: 100vh;
178 font-family: 'Courier New', monospace;
179}
180
181.header {
182 text-align: center;
183 margin-bottom: 2rem;
184 border-bottom: 2px solid #00b4d8;
185 padding-bottom: 1rem;
186}
187
188.header h1 {
189 color: #00ff88;
190 text-shadow: 0 0 10px #00ff88;
191 margin: 0;
192}
193
194.platforms-grid {
195 display: grid;
196 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
197 gap: 1.5rem;
198 margin-bottom: 2rem;
199}
200
201.platform-card {
202 background: rgba(0, 180, 216, 0.1);
203 border: 2px solid #00b4d8;
204 border-radius: 8px;
205 padding: 1.5rem;
206 cursor: pointer;
207 transition: all 0.3s;
208}
209
210.platform-card:hover {
211 border-color: #00ff88;
212 transform: translateY(-5px);
213 box-shadow: 0 5px 20px rgba(0, 255, 136, 0.2);
214}
215
216.platform-card.selected {
217 border-color: #00ff88;
218 background: rgba(0, 255, 136, 0.1);
219 box-shadow: 0 0 20px rgba(0, 255, 136, 0.3);
220}
221
222.platform-logo {
223 font-size: 3rem;
224 text-align: center;
225 margin-bottom: 1rem;
226}
227
228.platform-card h3 {
229 color: #00ff88;
230 text-align: center;
231 margin: 0 0 1rem 0;
232}
233
234.platform-info {
235 font-size: 0.85rem;
236 color: #00b4d8;
237 margin-bottom: 1rem;
238}
239
240.platform-info > div {
241 margin-bottom: 0.5rem;
242}
243
244.features {
245 display: flex;
246 flex-direction: column;
247 gap: 0.5rem;
248}
249
250.feature {
251 font-size: 0.8rem;
252 color: #fff;
253 padding: 0.25rem 0.5rem;
254 background: rgba(0, 255, 136, 0.05);
255 border-radius: 4px;
256}
257
258.deploy-panel {
259 background: rgba(0, 180, 216, 0.1);
260 border: 2px solid #00b4d8;
261 border-radius: 8px;
262 padding: 2rem;
263}
264
265.deploy-panel h2 {
266 color: #00ff88;
267 margin-top: 0;
268 text-align: center;
269}
270
271.deploy-status {
272 text-align: center;
273 padding: 1rem;
274 border-radius: 8px;
275 margin-bottom: 1rem;
276 font-size: 1.1rem;
277}
278
279.deploy-status.idle {
280 background: rgba(0, 180, 216, 0.2);
281 color: #00b4d8;
282}
283
284.deploy-status.deploying {
285 background: rgba(255, 165, 0, 0.2);
286 color: #ffa500;
287 animation: pulse 2s infinite;
288}
289
290.deploy-status.deployed {
291 background: rgba(0, 255, 136, 0.2);
292 color: #00ff88;
293}
294
295@keyframes pulse {
296 0%, 100% { opacity: 1; }
297 50% { opacity: 0.7; }
298}
299
300.progress-bar {
301 height: 20px;
302 background: rgba(0, 255, 136, 0.1);
303 border-radius: 10px;
304 overflow: hidden;
305 margin-bottom: 1rem;
306}
307
308.progress-fill {
309 height: 100%;
310 background: linear-gradient(90deg, #00b4d8, #00ff88);
311 transition: width 0.3s;
312}
313
314.deployment-info {
315 text-align: center;
316 margin: 1.5rem 0;
317}
318
319.url {
320 color: #00b4d8;
321 font-size: 1.1rem;
322}
323
324.url a {
325 color: #00ff88;
326 text-decoration: none;
327}
328
329.success-msg {
330 color: #fff;
331 margin-top: 0.5rem;
332}
333
334.actions {
335 text-align: center;
336}
337
338.deploy-btn, .reset-btn {
339 padding: 1rem 2rem;
340 font-size: 1rem;
341 font-family: 'Courier New', monospace;
342 border-radius: 8px;
343 cursor: pointer;
344 border: 2px solid;
345 transition: all 0.3s;
346}
347
348.deploy-btn {
349 background: #00ff88;
350 color: #0a0e27;
351 border-color: #00ff88;
352}
353
354.deploy-btn:hover {
355 box-shadow: 0 0 20px #00ff88;
356 transform: scale(1.05);
357}
358
359.reset-btn {
360 background: rgba(0, 180, 216, 0.2);
361 color: #00b4d8;
362 border-color: #00b4d8;
363}
364
365.reset-btn:hover {
366 background: #00b4d8;
367 color: #0a0e27;
368}
369</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. Quelle commande construit une application Vue + Vite pour la production ?
2. Dans quel dossier Vite crée-t-il le build de production par défaut ?
Voici 2 des 6 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Complète vite.config.js avec la configuration du build : outDir, minify, sourcemap et un bundle vendor séparé (dans Vite 8 via codeSplitting au lieu de manualChunks).
- Clique dans l'ordre
Ordonne les commandes dans l'ordre : construire puis prévisualiser le build de production :
- Tri horizontal
Ordonne la syntaxe pour lire une variable d'environnement dans Vite :
- Éditeur de code
Configure les fichiers .env et .env.production avec des variables VITE_API_URL et utilise-les dans un composant.