Kurs Vue.js · Moduł 12: Testy i wdrożenie
Projekt: Mars Mission Launch - Complete Production App
W tej lekcji7
Finalna aplikacja gotowa do startu! Twój kod leci na Marsa!
Przez pięć lekcji składałeś procedurę startową z osobnych elementów: testy komponentów, testy logiki, E2E, build i wdrożenie. Każdy działał w swojej lekcji, ale prawdziwy projekt potrzebuje ich wszystkich naraz, w jednym repozytorium, spiętych tak, żeby żaden krok nie zależał od pamięci operatora. Ten projekt to kompletna konfiguracja produkcyjna galerii - przejdziemy przez nią plik po pliku.
Konfiguracja Vite gotowa do startu
Plik vite.config.js zaczyna się od trzech wtyczek: vue() kompiluje komponenty, ViteImageOptimizer kompresuje obrazy, a visualizer rysuje raport rozmiarów pakietów i od razu otwiera go w przeglądarce:
1// vite.config.js - Production ready
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 ],Wtyczki nie ruszają plików w src - pracują na tym, co wędruje do dist. Druga część pliku ustawia build: minifikator terser, podział na pakiety i port serwera deweloperskiego:
1 // vite.config.js - dalszy ciąg 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})Opcje drop_console i drop_debugger w terserOptions usuwają z kodu produkcyjnego wszystkie wywołania console.log i instrukcje debugger; pliki źródłowe zostają nietknięte. Terser trzeba doinstalować (npm install -D terser), a domyślny minifikator Vite 8, Oxc, zrobi to samo opcją dropConsole w build.rolldownOptions.output.minify.compress. Podział opisuje codeSplitting: każda grupa ma nazwę pakietu i test, czyli wyrażenie regularne dopasowywane do ścieżki modułu, w którym [\\/] łapie ukośnik i ukośnik wsteczny ścieżek Windows. Starsze konfiguracje z rollupOptions.output.manualChunks w formie obiektu przestały działać - Vite 8 przerywa na nich build.
Skrypty w package.json
Plik package.json zbiera polecenia procedury startowej w jednym miejscu - każdy etap pipeline wywoła później jeden z tych skryptów:
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 przegląda kod ESLintem, format porządkuje go Prettierem, a type-check uruchamia vue-tsc, czyli sprawdzanie typów TypeScript także w plikach .vue. deploy wysyła build poleceniem vercel --prod, równoważnym z vercel deploy --prod.
Lazy loading komponentów
Ciężki komponent, na przykład wykres telemetrii otwierany na żądanie, nie musi siedzieć w głównym pakiecie. Funkcja defineAsyncComponent z pakietu vue przyjmuje funkcję z dynamicznym import() i zwraca komponent pobierany przy pierwszym wyrenderowaniu:
1import { defineAsyncComponent } from 'vue'
2
3const TelemetryChart = defineAsyncComponent(() =>
4 import('./components/TelemetryChart.vue')
5)Build wydzieli TelemetryChart.vue do osobnego pakietu, a w szablonie używasz go jak zwykłego komponentu. Trasom routera wystarczy samo () => import().
Pipeline CI/CD
Workflow dzieli pracę na trzy zadania (jobs). Pierwsze, test, rusza przy każdym pushu i pull requeście i przechodzi pełną kontrolę jakości:
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 }}Kolejność jest celowa: najtańsze kontrole idą pierwsze. cache: 'npm' przechowuje paczki między uruchomieniami, a akcja Codecov wysyła raport pokrycia z tokenem z sekretów repozytorium. Zadanie build czeka na zielony test dzięki polu needs i zapisuje dist jako artefakt:
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: distArtefakt to plik, który GitHub przechowuje między zadaniami. Wersje v3 akcji artefaktów przestały działać 30 stycznia 2025 roku, dlatego stare workflow kończą się dziś błędem. Ostatnie zadanie wdraża aplikację wyłącznie z gałęzi main, co sprawdza warunek 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'Pull requesty przechodzą więc testy i build bez wdrożenia. Pobrany artefakt przyda się przy hostingu statycznym, bo Vercel domyślnie buduje projekt u siebie, z przesłanego kodu.
Kapsuła Docker
Wersja kontenerowa to Dockerfile z poprzedniej lekcji z komentarzami i kontrolą HEALTHCHECK, która co 30 sekund sprawdza, czy nginx odpowiada:
1# Dockerfile - Multi-stage build
2FROM node:24-alpine AS builder
3
4WORKDIR /app
5
6# Copy package files
7COPY package*.json ./
8
9# Install dependencies (the build needs devDependencies too)
10RUN npm ci
11
12# Copy source
13COPY . .
14
15# Build
16RUN npm run build
17
18# Production stage
19FROM nginx:alpine
20
21# Copy build
22COPY /app/dist /usr/share/nginx/html
23
24# Copy nginx config
25COPY nginx.conf /etc/nginx/conf.d/default.conf
26
27# Health check
28HEALTHCHECK \
29 CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1
30
31EXPOSE 80
32
33CMD ["nginx", "-g", "daemon off;"]Uważaj na częsty błąd: npm ci --only=production w etapie budowania pomija devDependencies, w których siedzi Vite, więc build by się nie udał. Do końcowego obrazu i tak trafia tylko dist.
Zoptymalizowany nginx
Konfiguracja serwera dokłada kompresję gzip, nagłówki bezpieczeństwa i osobne reguły cache:
1# nginx.conf - Optimized
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 # Security headers
16 add_header X-Frame-Options "SAMEORIGIN" always;
17 add_header X-Content-Type-Options "nosniff" always;
18 # Deprecated header: 0 disables the legacy XSS filter, use a Content-Security-Policy instead
19 add_header X-XSS-Protection "0" always;
20
21 # SPA routing
22 location / {
23 try_files $uri $uri/ /index.html;
24 }
25
26 # Cache static assets
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 # Don't cache index.html
33 location = /index.html {
34 add_header Cache-Control "no-cache, must-revalidate";
35 }
36}index.html nie może trafić do długiego cache, bo wskazuje aktualne nazwy pakietów. X-XSS-Protection jest przestarzały i MDN zaleca zamiast niego Content-Security-Policy, stąd wartość 0. Pułapka: add_header z poziomu server nie przechodzi do bloków location z własnym add_header, więc tam nagłówki bezpieczeństwa trzeba powtórzyć.
Monitorowanie i SEO
Po starcie patrzysz na przyrządy: profilowanie wydajności w Vue DevTools pokaże zbyt częste renderowanie, Lighthouse audyt ładowania, a metryki Web Vitals (LCP, INP, CLS) doświadczenia prawdziwych użytkowników. SPA wysyła botom wyszukiwarek pusty index.html, a treść dorysowuje JavaScript. Google i Bing radzą sobie z prostymi aplikacjami, ale nie czekają na dane pobierane asynchronicznie, więc pełny HTML da botom dopiero SSR albo SSG, najprościej z Nuxt - poznasz go w Laboratorium Kwantowym. Przed ręcznym startem zachowaj kolejność pipeline: lint i sprawdzenie typów, testy, npm run build, podgląd npm run preview, a na końcu vercel --prod.
GRATULACJE! Ukończyłeś szkolenie na Platformie Startowej NOVA LAB!
Twój kod przeszedł wszystkie testy przedstartowe i jest gotowy na misję marsjańską!
Teraz Twoja aplikacja:
- Jest w pełni przetestowana (wszystkie systemy sprawdzone)
- Zoptymalizowana pod kątem wydajności (gotowa na trudne warunki kosmiczne)
- Gotowa do wdrożenia (procedura startowa zakończona)
- Ma CI/CD pipeline (automatyczne systemy kontroli)
- Działa w kontenerze Docker (hermetyczna kapsuła)
MISJA STARTOWA ZAKOŃCZONA SUKCESEM!
Moja rada: skopiuj ten zestaw plików do własnego repozytorium i dopasuj go do swojej aplikacji - to szkielet na każdy kolejny projekt Vue. Twój kod startuje na Marsa, a przed Tobą jeszcze trzy lokacje NOVA LAB; w Sali Holograficznej ożywisz interfejs animacjami.
Zapamiętaj: aplikacja jest gotowa do lotu dopiero wtedy, gdy testy, build i wdrożenie przechodzą automatycznie, bez udziału operatora.
Kod do tej lekcji: App.vue
1<script setup>
2import { ref, computed, onMounted } from 'vue'
3
4// NOVA LAB - panel monitoringu
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: 'Wykryto wysokie zuzycie pamieci', time: '2 min temu' },
18 { id: 2, level: 'info', message: 'Wdrozenie zakonczone pomyslnie', time: '15 min temu' }
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: 'Sprawny', color: '#00ff88' }
35 } else if (uptime >= 99 && errorRate < 0.5 && responseTime < 500) {
36 return { status: 'warning', label: 'Ostrzezenie', color: '#ffa500' }
37 } else {
38 return { status: 'critical', label: 'Krytyczny', color: '#ff0000' }
39 }
40})
41
42const performanceScore = computed(() => {
43 const avgResponseTime = metrics.value.responseTime
44 if (avgResponseTime < 100) return 'Doskonala'
45 if (avgResponseTime < 200) return 'Dobra'
46 if (avgResponseTime < 500) return 'Przecietna'
47 return 'Slaba'
48})
49
50function refreshMetrics() {
51 // Symulacja aktualizacji metryk
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 // Automatyczne odswiezanie co 5 sekund
61 setInterval(refreshMetrics, 5000)
62})
63</script>
64
65<template>
66 <div class="monitoring-dashboard">
67 <div class="header">
68 <h1>Panel kontroli misji</h1>
69 <p>Monitoring NOVA LAB w czasie rzeczywistym</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>Stan systemu: {{ healthStatus.label }}</h2>
78 <p>Dostepnosc: {{ metrics.uptime }}% | Wydajnosc: {{ 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">Aktywni uzytkownicy</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">Sredni czas odpowiedzi</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">Zapytania/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">Odsetek bledow</div>
105 </div>
106 </div>
107
108 <div class="resource-usage">
109 <h3>Zuzycie zasobow</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>Pamiec</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>Dysk</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>Ostatnie alerty</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 Odswiez metryki
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>Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jak zaimplementować lazy loading komponentu w Vue 3?
2. Jakie narzędzia pomagają monitorować performance aplikacji Vue?
To 2 z 4 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Układanie w poziomie
Ułóż składnię lazy loading komponentu:
- Edytor kodu
Skonfiguruj pełną vite.config.js z: vue plugin, image optimizer, visualizer, build options z podziałem na pakiety (w Vite 8 przez codeSplitting zamiast manualChunks).
- Układanie w pionie
Uporządkuj kroki przygotowania do produkcji od pierwszego do ostatniego:
- Klikanie w kolejności
Ułóż komendę wdrożenia produkcyjnego na Vercel:
- Układanie w pionie
Uporządkuj kroki GitHub Actions CI/CD workflow:
- Klikanie w kolejności
Ułóż komendę wdrożenia produkcyjnego na Netlify:
- Układanie w poziomie
Ułóż dyrektywę nginx dla SPA routing:
- Klikanie w kolejności
Ułóż komendę uruchomienia kontenera Docker z mapowaniem portów: