Kurs Vue.js · Moduł 12: Testy i wdrożenie
Deployment
W tej lekcji7
Wdrożenie aplikacji na serwer - start rakiety! Twój kod leci do produkcji jak rakieta na Marsa!
Folder dist z poprzedniej lekcji leży na Twoim dysku. Trzeba go wynieść na orbitę: na serwer, który pod stałym adresem i przez HTTPS poda pliki każdemu technikowi - i to bez ręcznego kopiowania przy każdej poprawce. Odpowiedzią jest CI/CD, czyli Continuous Integration i Continuous Deployment - automatyzacja testów, buildu i wdrożeń, która po każdym wypchnięciu kodu sama przechodzi procedurę startową.
Deployment na Vercel
Najprościej podłączyć repozytorium Git w panelu Vercela. Vercel sam wykryje projekt Vite i ustawi komendę npm run build oraz katalog dist, a potem wdroży produkcję po każdym pushu do głównej gałęzi i przygotuje adres podglądu dla każdego pull requesta. Gdy wolisz terminal, użyj Vercel CLI:
1# Install Vercel CLI
2npm install -g vercel
3
4# Deploy
5vercel
6
7# Production deployment
8vercel --prodSamo vercel to skrót od vercel deploy, bo deploy jest domyślnym poleceniem CLI. Bez flagi powstaje wdrożenie podglądowe pod unikalnym adresem, a --prod publikuje je pod domeną produkcyjną; tylko pierwsze wdrożenie nowego projektu zawsze trafia na produkcję. Ustawienia możesz trzymać w repozytorium, w pliku 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}Pola buildCommand, outputDirectory i framework powtarzają to, co Vercel wykrywa sam, ale nie zależą od kliknięć w panelu. Najważniejsza jest sekcja rewrites.
Dlaczego SPA potrzebuje przepisywania adresów
Aplikacja z Vue Router to SPA: serwer ma tylko index.html, a trasy w rodzaju /gallery obsługuje JavaScript w przeglądarce. Gdy technik odświeży stronę na /gallery, przeglądarka poprosi serwer o nieistniejący plik /gallery i dostanie 404. Reguła przepisywania każe dla każdej ścieżki oddać index.html, a router wyświetli właściwy widok.
Deployment na Netlify
Netlify działa podobnie: podłączasz repozytorium albo korzystasz z CLI. Polecenie netlify deploy publikuje wersję roboczą pod adresem podglądu, a z flagą --prod wersję produkcyjną:
1# Install Netlify CLI
2npm install -g netlify-cli
3
4# Deploy
5netlify deploy
6
7# Production
8netlify deploy --prodKonfiguracja trafia do netlify.toml. Sekcja [build] podaje komendę budowania i katalog do publikacji, a [[redirects]] robi dla SPA to samo, co rewrites na Vercelu:
1# netlify.toml
2[build]
3 command = "npm run build"
4 publish = "dist"
5
6[[redirects]]
7 from = "/*"
8 to = "/index.html"
9 status = 200Status 200 oznacza przepisanie, więc adres zostaje bez zmian. Z kodem 301 przeglądarka przeskoczyłaby na stronę główną i technik zgubiłby trasę.
Deployment na GitHub Pages
GitHub Pages hostuje stronę prosto z repozytorium. Paczka gh-pages wysyła zawartość dist na osobną gałąź:
1npm install -D gh-pagesStrona projektu działa pod adresem z nazwą repozytorium, na przykład https://user.github.io/repository-name/. Dlatego w vite.config.js ustawiasz base, czyli ścieżkę, od której Vite zbuduje odwołania do plików:
1// vite.config.js
2export default defineConfig({
3 base: '/repository-name/', // Nazwa repo
4 plugins: [vue()]
5})Bez base przeglądarka szukałaby skryptów w katalogu głównym domeny i pokazałaby pustą stronę; dla strony użytkownika zostaw base: '/'. Pozostaje skrypt wdrożenia:
1// package.json
2{
3 "scripts": {
4 "deploy": "npm run build && gh-pages -d dist"
5 }
6}npm run deploy buduje aplikację i publikuje dist. GitHub Pages nie obsługuje reguł przepisywania, więc odświeżenie głębokiej trasy SPA da 404. Dokumentacja Vite opisuje też wdrażanie przez GitHub Actions zamiast paczki gh-pages.
Docker Deployment
Własny serwer wymaga obrazu Docker. Dockerfile używa budowania wieloetapowego (multi-stage build): pierwszy etap na obrazie Node buduje aplikację, drugi na lekkim nginx tylko ją serwuje:
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 przenosi z pierwszego etapu wyłącznie dist, więc kod źródłowy, node_modules i sam Node nie trafiają do końcowego obrazu. Obraz bazowy to Node 24, aktualna wersja LTS - Vite 8 wymaga co najmniej Node 20.19 lub 22.12, więc dawny node:18 przerwałby build. nginx potrzebuje jeszcze reguły dla 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 # Cache static assets
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 sprawdza po kolei plik, katalog, a w ostateczności oddaje index.html. Roczne cache dla zasobów jest bezpieczne, bo zmiana kodu zmienia hash w nazwie pliku. Zostaje zbudować obraz i uruchomić kontener:
1# Build and run
2docker build -t gallery-app .
3docker run -p 8080:80 gallery-app-t gallery-app nadaje obrazowi nazwę, a kropka wskazuje bieżący katalog jako kontekst budowania. -p 8080:80 łączy port 8080 komputera z portem 80 kontenera, więc aplikacja działa pod http://localhost:8080.
CI/CD with GitHub Actions
Workflow GitHub Actions to plik YAML w katalogu .github/workflows, uruchamiany przy zdarzeniach z sekcji on. Ten przechodzi procedurę startową po każdym pushu do 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'Kroki idą po kolei: actions/checkout pobiera kod, actions/setup-node instaluje Node, npm ci instaluje zależności dokładnie według package-lock.json, potem testy, build i wdrożenie. Gdy któryś krok zawiedzie, następne się nie wykonają. W pełnym pipeline przed testami stoi jeszcze linting (npm run lint) - zobaczysz go w projekcie. secrets to tajne klucze z ustawień repozytorium, a amondnet/vercel-action to akcja społeczności - projekt podłączony do Vercela przez Git tego kroku nie potrzebuje.
Performance Checklist
- Lazy load routes - widoki ładowane dopiero przy wejściu na trasę
- Code splitting - biblioteki w osobnych pakietach trzymanych w cache
- Image optimization - obrazy ważą zwykle więcej niż kod
- Minification - Vite robi ją domyślnie
- Gzip/Brotli compression - kompresja po stronie serwera
- CDN dla statycznych zasobów - pliki bliżej użytkownika
- Cache headers - długie dla plików z hashem, krótkie dla
index.html - Preload critical resources - Vite sam dodaje
modulepreload - Remove console.logs - minifikator wytnie je po włączeniu odpowiedniej opcji
- Source maps tylko dla dev - publicznie tylko świadomie, np.
sourcemap: 'hidden'dla raportowania błędów
Moja rada: na start wybierz Vercel albo Netlify z integracją Git - HTTPS, CDN i podglądy pull requestów dostajesz bez konfiguracji. Dockera zostaw na czas, gdy aplikacja musi stanąć na infrastrukturze stacji. W projekcie końcowym spinasz testy, build i wdrożenie w jeden pipeline.
Zapamiętaj: CI/CD to automatyczna procedura przedstartowa, która nie pozwoli wystartować z uszkodzonym systemem.
Kod do tej lekcji: App.vue
1<script setup>
2import { ref, computed } from 'vue'
3
4// NOVA LAB - wizualizacja pipeline CI/CD
5const pipelineStages = ref([
6 { id: 1, name: 'Kod zrodlowy', status: 'completed', duration: 2, icon: '' },
7 { id: 2, name: 'Build', status: 'idle', duration: 45, icon: '' },
8 { id: 3, name: 'Testy', status: 'idle', duration: 30, icon: '' },
9 { id: 4, name: 'Skan bezpieczenstwa', status: 'idle', duration: 20, icon: '' },
10 { id: 5, name: 'Wdrozenie staging', status: 'idle', duration: 25, icon: '' },
11 { id: 6, name: 'Wdrozenie produkcyjne', 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 gotowy do startu...'
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 = ['Start pipeline CI/CD...']
41
42 // Reset wszystkich etapow poza pierwszym
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(`→ W toku: ${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}: zakonczono pomyslnie`)
72
73 setTimeout(() => {
74 runNextStage()
75 }, 500)
76 }
77 }, 100)
78}
79
80function completePipeline() {
81 pipelineRunning.value = false
82 logs.value.push('Pipeline zakonczony pomyslnie!')
83 logs.value.push('Misja marsjanska wdrozona na produkcje!')
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 zresetowany. Gotowy do startu...']
97}
98</script>
99
100<template>
101 <div class="cicd-dashboard">
102 <div class="header">
103 <h1>Monitor pipeline CI/CD</h1>
104 <p>Automatyczny system wdrozen NOVA LAB</p>
105 </div>
106
107 <div class="pipeline-status" :class="pipelineStatus">
108 <div class="status-indicator">
109 <template v-if="pipelineStatus === 'idle'">Bezczynny</template>
110 <template v-else-if="pipelineStatus === 'running'">W toku</template>
111 <template v-else-if="pipelineStatus === 'success'">Sukces</template>
112 <template v-else-if="pipelineStatus === 'failed'">Blad</template>
113 </div>
114 <div class="overall-progress">
115 Postep: {{ 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>Logi 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 ▸ Uruchom pipeline
158 </button>
159 <button
160 @click="resetPipeline"
161 class="reset-btn"
162 >
163 Reset
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>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 wdrożyć aplikację Vue na platformę Vercel?
2. Jakie ustawienia build są potrzebne przy deployment na Netlify?
To 2 z 4 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Uzupełnij plik vercel.json z konfiguracją buildCommand, outputDirectory, framework i rewrites dla SPA.
- Układanie w pionie
Uporządkuj etapy pipeline CI/CD od pierwszego do ostatniego:
- Układanie w pionie
Ułóż etapy pipeline CI/CD od kodu do produkcji:
- Klikanie w kolejności
Ułóż komendę budowania obrazu Docker:
- Układanie w poziomie
Ułóż konfigurację Vite dla GitHub Pages:
- Edytor kodu
Uzupełnij Dockerfile z multi-stage build: etap budowania (node) i etap produkcyjny (nginx).