Kurs Vue.js · Moduł 12: Testy i wdrożenie

Budowanie wersji produkcyjnej

17 min czytania
W tej lekcji14

Przez trzy poprzednie lekcje sprawdzałeś systemy stacji: komponenty, composables, store, wreszcie całe ścieżki operatora w testach E2E. Wszystkie kontrolki świecą na zielono - świecą jednak w laboratorium NOVA LAB, na Twojej maszynie, przy uruchomionym serwerze deweloperskim. Panel telemetrii ma tymczasem trafić na lądownik: do przeglądarki technika, który otwiera go z drugiego końca stacji, przez łącze o opóźnieniu liczonym w minutach. Serwer deweloperski tam z Tobą nie poleci.

Polecenie npm run dev, którego używasz od pierwszej lokacji, podaje przeglądarce Twoje pliki źródłowe niemal w oryginale. Każdy plik .vue transformuje dopiero wtedy, gdy przeglądarka o niego poprosi, a resztę pracy zostawia natywnemu systemowi modułów przeglądarki. Dzięki temu start jest natychmiastowy, ale ceną są setki osobnych żądań, brak minifikacji i katalog node_modules czekający w tle. Do wysyłki potrzebujesz czegoś dokładnie odwrotnego: jednego, policzonego i spakowanego ładunku, który przeglądarka na drugim końcu łącza pobierze w kilku plikach.

Jedno polecenie, które pakuje ładunek

Za spakowanie odpowiada osobny tryb Vite, uruchamiany skryptem build. Vite przełącza się wtedy z serwowania plików na budowanie: wczytuje index.html jako punkt wejścia, przechodzi po wszystkich importach, kompiluje każdy komponent, a następnie oddaje całość Rolldownowi, czyli napisanemu w Ruście bundlerowi, którego Vite używa od wersji 8. Rolldown skleja moduły w pakiety, a minifikator Oxc skraca wynik. To ta sama komenda w każdym projekcie Vue postawionym na Vite, niezależnie od tego, czy budujesz panel telemetrii, czy grę dla załogi.

1npm run build

Po kilkunastu sekundach w katalogu projektu pojawia się folder dist. Zwróć uwagę, co się przy tym nie zmieniło: ani jeden Twój plik źródłowy. Build wyłącznie czyta katalog src, a zapisuje do dist - dlatego możesz go uruchamiać dowolnie często i dlatego dist trafia do .gitignore, a nie do repozytorium.

Zapamiętaj nazwę tego skryptu dokładnie bo tu najłatwiej o pomyłkę. npm run nie zna żadnych magicznych komend - uruchamia wyłącznie skrypty wpisane do sekcji scripts w pliku package.json, a szablon Vue z Vite zakłada dokładnie trzy: dev, build i preview. Poleceń npm run prod ani npm run compile nie znajdziesz w żadnym standardowym szablonie i skończą się komunikatem o brakującym skrypcie. npm run deploy bywa dopisywany ręcznie, ale to skrypt wysyłki, a nie budowania - jeśli już go w projekcie zobaczysz, prawie na pewno sam w środku wywołuje npm run build, bo najpierw trzeba mieć co wysłać.

Podgląd ładunku przed startem

Zbudowana aplikacja bywa pierwszym miejscem, w którym wychodzą usterki niewidoczne w laboratorium: zła ścieżka bazowa, obrazek doklejany dynamicznie, biblioteka działająca tylko dzięki temu, że serwer deweloperski coś za nią załatwiał. Dlatego Vite ma trzeci skrypt, preview. Podnosi on prosty serwer plików statycznych i serwuje z niego zawartość dist - dokładnie te pliki, które za chwilę wyślesz. Adres wypisze w konsoli; jest inny niż adres serwera deweloperskiego, więc oba mogą działać jednocześnie. Obie komendy warto uruchamiać jedna po drugiej, spięte podwójnym ampersandem.

1npm run build && npm run preview

Znak && to zwykły operator powłoki: druga komenda wystartuje tylko wtedy, gdy pierwsza zakończy się powodzeniem. Jeśli build przerwie się na błędzie importu, podgląd w ogóle się nie podniesie i nie obejrzysz przypadkiem poprzedniej, nieaktualnej zawartości dist. I jedno zastrzeżenie, żeby nie było nieporozumień: preview jest narzędziem kontrolnym, a nie serwerem produkcyjnym. Nie ma kompresji, nagłówków pamięci podręcznej ani certyfikatu - służy do obejrzenia ładunku, nie do jego udostępniania załodze.

Co naprawdę leży w folderze dist

Skoro dist to wszystko, co poleci na lądownik, warto wiedzieć, co dokładnie w nim wyląduje. Struktura jest za każdym razem taka sama: jeden plik index.html w korzeniu i katalog assets z całą resztą. Poniżej typowa zawartość po zbudowaniu panelu z kilkoma widokami.

1dist/
2  index.html
3  assets/
4    index-4f3a9b1c.js
5    index-9c2e7d84.css
6    vendor-1b7e0a52.js
7    TelemetryView-6d41f0ab.js

Ciąg znaków w nazwie pliku to skrót policzony z jego zawartości. Zmienisz linijkę w kodzie - zmieni się skrót, a więc i nazwa pliku, więc przeglądarka technika nie ma jak podać starej wersji z pamięci podręcznej. Nie musisz przy tym niczego poprawiać ręcznie: index.html w dist ma już przepisane odwołania do nowych nazw. To dlatego build generuje własny index.html, zamiast kopiować Twój bez zmian.

W dist znajdziesz wyłącznie zminifikowane pliki JS, CSS i HTML gotowe do wdrożenia. Nie ma tam ani jednego pliku .vue - komponenty zostały skompilowane do funkcji renderujących i wtopione w pakiety. Nie ma node_modules: te fragmenty bibliotek, z których faktycznie korzystasz, siedzą wewnątrz pakietów, a reszta została odrzucona. Nie ma też vite.config.js ani plików konfiguracyjnych laboratorium - to wejście dla procesu budowania, a nie jego wynik.

Jest jeden folder, który łatwo pomylić z wynikiem: public. On również jest wejściem - wrzucasz do niego pliki, które mają trafić do dist bez żadnej obróbki, na przykład robots.txt czy ikonę stacji. Nazwy build i out też krążą po dokumentacjach, ale należą do innych narzędzi: build to konwencja projektów spod Create React App i webpacka, out to katalog eksportu statycznego w Next.js. W Vite domyślnym katalogiem wyjściowym jest dist - i tam szukaj wyniku po każdym buildzie.

Sekcja build w pliku vite.config.js

Domyślne ustawienia są rozsądne, ale prędzej czy później misja wymusi zmiany: hosting oczekuje innej nazwy katalogu, ktoś prosi o mapy źródeł, ktoś inny o mocniejszą minifikację. Wszystko to opisujesz w pliku vite.config.js, w obiekcie build. Cztery opcje, od których zaczniesz, brzmią tak: outDir to nazwa katalogu wyjściowego, assetsDir to podkatalog na pakiety i style wewnątrz niego, sourcemap decyduje, czy obok pakietów powstaną mapy pozwalające debugować zminifikowany kod w kategoriach Twoich plików źródłowych, a minify wskazuje narzędzie, które ten kod skraca.

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})

Ten plik nie zmienia niczego - i to jest w nim najciekawsze. Wpisałeś dokładnie te wartości, które Vite przyjmuje sam, więc build zachowa się identycznie jak przed chwilą. Traktuj go jak listę kontrolną: teraz wiesz, gdzie sięgnąć, gdy hosting zażąda katalogu o innej nazwie. Funkcja defineConfig niczego nie konfiguruje, tylko podpowiada edytorowi typy, dzięki czemu literówka w nazwie opcji podświetli się od razu. Mapy źródeł polecam trzymać wyłączone w aplikacji wystawionej publicznie, bo razem z nimi publikujesz czytelny kod źródłowy panelu; jeśli potrzebujesz ich do systemu raportowania błędów, ustaw sourcemap: 'hidden' - mapy powstaną, ale pakiet nie będzie do nich odsyłał.

Trzy inne opcje z tej sekcji zostaw na razie w spokoju, warto jednak znać je z nazwy. assetsInlineLimit wyznacza granicę w bajtach, poniżej której drobny plik, na przykład mała ikona SVG, zostanie wklejony wprost do kodu jako data URI, zamiast lądować osobno - o jedno żądanie mniej. cssCodeSplit włącza dzielenie stylów na części odpowiadające pakietom. chunkSizeWarningLimit to próg, po przekroczeniu którego Vite wypisze ostrzeżenie o zbyt dużym pakiecie; liczy on rozmiar po minifikacji, ale przed kompresją serwera, więc realny transfer będzie mniejszy niż liczba, którą zobaczysz w konsoli.

Wyciszanie konsoli, czyli minifikator terser

W kodzie panelu na pewno zostało kilka wywołań console.log - w laboratorium nieocenionych, na lądowniku zbędnych. Wycinaniem takich wywołań zajmuje się minifikator - trzeba mu tylko o to wprost powiedzieć. Domyślnym minifikatorem Vite 8 jest Oxc (minify: 'oxc'): jest bardzo szybki i to on odpowiada za większość skrócenia kodu, a dawna wartość 'esbuild' jest przestarzała i zniknie w kolejnych wersjach. Alternatywą jest terser, wolniejszy, za to z drobiazgowym zestawem przełączników - między innymi drop_console i drop_debugger, które usuwają z pakietu odpowiednio wywołania konsoli oraz instrukcje debugger. Ustawienia terser podajesz w obiekcie terserOptions, w sekcji 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})

Jest tu pułapka, o której konfiguracje krążące po sieci zwykle milczą: Vite nie instaluje terser razem ze sobą. Jeśli wpiszesz minify: 'terser' bez dodania paczki terser do zależności deweloperskich, build przerwie się komunikatem, że minifikator jest niedostępny - wystarczy wtedy npm install -D terser. Zwróć też uwagę, czego ta konfiguracja nie robi: nie rusza Twoich plików źródłowych ani trybu deweloperskiego. console.log zostaje w kodzie i nadal wypisuje w konsoli przy npm run dev, znika wyłącznie z zawartości dist.

Moja rekomendacja: zostań przy domyślnym Oxc - build jest wtedy wyraźnie szybszy, a logi wytniesz i bez terser, ustawiając dropConsole: true w build.rolldownOptions.output.minify.compress. Po terser sięgaj świadomie, gdy potrzebujesz któregoś z jego drobiazgowych przełączników. Opcję esbuild.drop, którą znajdziesz w starszych poradnikach, Vite 8 uznaje za przestarzałą - tak jak całą opcję esbuild.

Dzielenie ładunku na pakiety

Domyślnie Vite skleja Twój kod w jeden pakiet, a to ma nieprzyjemny skutek uboczny przy aktualizacjach. Poprawiasz jedną etykietę w panelu, nazwa pliku zmienia się przez nowy skrót, a technik pobiera od nowa całość - razem z Vue, routerem i Pinią, które nie drgnęły od miesięcy. Lekarstwem jest rozdzielenie kodu na pakiety o różnym tempie zmian. Służy do tego opcja codeSplitting, którą podajesz w build.rolldownOptions.output, czyli w ustawieniach przekazywanych wprost Rolldownowi. Przyjmuje ona listę grup groups: każda grupa ma nazwę name, która stanie się nazwą przyszłego pakietu, oraz test - wyrażenie regularne dopasowywane do ścieżki modułu, w którym [\\/] łapie zarówno ukośnik, jak i ukośnik wsteczny ścieżek Windows. Starsze poradniki pokazują tu manualChunks w rollupOptions: wersji z obiektem Vite 8 już nie obsługuje i przerywa build błędem, a wersję z funkcją oznaczył jako przestarzałą.

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})

Grupa łapie też pakiety @vue, bo z nich składa się wnętrze samego Vue. Po takim buildzie w katalogu assets zobaczysz osobny plik zaczynający się od słowa vendor. Biblioteki wylądowały w nim razem i dopóki nie zmienisz ich wersji, jego nazwa pozostanie ta sama - przeglądarka pobierze go raz i zatrzyma w pamięci podręcznej mimo kolejnych wydań panelu. Co się przy tym nie zmieniło: nie dotknąłeś ani jednego importu w kodzie. Nadal piszesz import { ref } from 'vue' dokładnie tam, gdzie pisałeś, a decyzję o rozdziale plików podejmuje wyłącznie build.

Jedna reguła zamiast wyliczanki

Ręczna lista sprawdza się przy trzech bibliotekach. Gdy package.json puchnie, wygodniej podać regułę niż wymieniać nazwy jedna po drugiej. Pole name grupy przyjmuje bowiem także funkcję: Rolldown wywoła ją dla każdego modułu, przekazując jego ścieżkę jako argument moduleId, a zwrócony napis potraktuje jako nazwę pakietu. Gdy funkcja zwróci null, grupa pominie moduł, a ten trafi tam, gdzie trafiłby domyślnie.

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})

Ta wersja mówi krótko: cokolwiek pochodzi z node_modules, ląduje w pakiecie vendor, a wszystko inne zostaje bez zmian. Dokładając kolejny warunek, możesz wydzielić pojedynczą, ciężką bibliotekę - na przykład silnik wykresów telemetrii, potrzebny tylko na jednym ekranie. Skoro masz wybór, powiem wprost, co polecam: zacznij od grupy z wyrażeniem regularnym, bo jest czytelna i dokładnie widzisz, co gdzie ląduje. Po funkcję sięgaj dopiero wtedy, gdy lista zaczyna rozjeżdżać się z rzeczywistością - zbyt agresywna reguła potrafi rozbić pakiety tak, że przeglądarka pobiera na starcie więcej plików, niż potrzebuje.

Parametry misji, czyli zmienne środowiskowe

Panel telemetrii pyta o dane serwer misji. W laboratorium ten serwer stoi na Twojej maszynie, na lądowniku - pod zupełnie innym adresem. Wpisanie adresu na sztywno w kod oznacza, że przed każdym buildem ktoś musi pamiętać o jego podmianie; prędzej czy później zapomni i produkcyjny panel zacznie pukać do localhosta. Vite rozwiązuje to plikami .env, czytanymi w chwili budowania. Plik .env obowiązuje zawsze, .env.production dokłada się do niego przy buildzie produkcyjnym, a .env.development przy serwerze deweloperskim, przy czym wartości z pliku bardziej szczegółowego wygrywają.

1# .env
2VITE_API_URL=http://localhost:3000
3
4# .env.production
5VITE_API_URL=https://telemetry.nova-lab.mars/api

Prefiks VITE_ nie jest ozdobnikiem, tylko bramką bezpieczeństwa. Do kodu, który trafia do przeglądarki, Vite wpuszcza wyłącznie zmienne z tym prefiksem; wszystkie pozostałe zostają po stronie procesu budowania. Wniosek jest ostrzejszy, niż się na pierwszy rzut oka wydaje: skoro zmienna z prefiksem ląduje w pakiecie, to każdy, kto otworzy dist, przeczyta jej wartość. Adresy i flagi funkcji - tak; klucze i hasła - nigdy.

Tu też siedzi klasyczna pomyłka. Prefiks VUE_APP_ i odczyt przez process.env to konwencja Vue CLI, czyli poprzednika opartego na webpacku. W projekcie na Vite obiekt process w przeglądarce po prostu nie istnieje i taki odczyt kończy się błędem. Nie pomoże również własny plik config.js z dowolnie nazwanymi zmiennymi - to zwykły moduł, który wjedzie do pakietu tak samo jak reszta kodu, tyle że bez jakiegokolwiek mechanizmu przełączania wartości między środowiskami. I na wszelki wypadek: twierdzenie, że Vite w ogóle nie obsługuje zmiennych środowiskowych, jest nieprawdziwe. Obsługuje, tylko na własnych zasadach.

Odczyt zmiennej w kodzie

Po stronie kodu zmienne czytasz z obiektu import.meta.env. Samo import.meta to standardowy element JavaScriptu - obiekt z metadanymi bieżącego modułu - a Vite dokłada do niego pole env. Pełny zapis składa się więc z trzech części w tej właśnie kolejności: import.meta, potem .env, potem nazwa zmiennej razem z prefiksem, czyli 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('Telemetry request failed')
9  }
10
11  return response.json()
12}

Ważne, żebyś rozumiał, co się tutaj naprawdę dzieje. To nie jest odczyt z jakiegoś słownika w czasie działania aplikacji - Vite podmienia całe wyrażenie import.meta.env.VITE_API_URL na literał tekstowy jeszcze przy budowaniu. W pliku w dist nie ma śladu po import.meta, jest gotowy adres. Ma to jedną praktyczną konsekwencję: nazwę zmiennej trzeba zapisać dosłownie. Sklejanie klucza w locie, z prefiksu i nazwy trzymanej w zmiennej, nie zostanie podmienione i da w wyniku undefined.

Tryb, w którym działa aplikacja

Obok Twoich zmiennych Vite wstawia do import.meta.env kilka własnych. Pierwsza z nich to MODE, czyli nazwa trybu zapisana tekstem: przy zwykłym npm run dev będzie to development, a po npm run build - production. Kolejne dwie, DEV i PROD, niosą tę samą informację w postaci wartości logicznych, przez co wygodnie używa się ich w warunkach.

1if (import.meta.env.DEV) {
2  console.log('Mission control mode:', import.meta.env.MODE)
3}

Ten warunek nie kosztuje ani bajta w wersji produkcyjnej. Skoro import.meta.env.DEV jest podmieniane na literał, w pakiecie produkcyjnym powstaje warunek zawsze fałszywy, a minifikator wyrzuca całą martwą gałąź razem z jej zawartością. To bardzo wygodny wzorzec na panele diagnostyczne dla załogi laboratorium: piszesz je normalnie, obok reszty kodu, a technik na lądowniku nawet ich nie pobiera.

Tryby nie kończą się na tych dwóch nazwach. Polecenie npm run build -- --mode staging zbuduje aplikację w trybie o nazwie staging i wczyta plik .env.staging - to standardowy sposób na środowisko testowe, stojące gdzieś pomiędzy laboratorium a lądownikiem. Podwójny dywiz przed --mode jest konieczny, bo to on każe menedżerowi pakietów przekazać argument dalej, do Vite. MODE będzie wtedy równe staging, natomiast PROD domyślnie pozostanie prawdziwe, bo o tej fladze decyduje rodzaj polecenia, a nie nazwa trybu.

Co Vite robi bez pytania

Zanim zaczniesz cokolwiek dokręcać, warto wiedzieć, ile pracy build wykonuje za Ciebie i - co ważniejsze - która technika za co odpowiada, bo te cztery nazwy bywają mylone.

Tree shaking to wycinanie nieużywanego kodu. Rolldown czyta importy i eksporty modułów ES, buduje z nich graf zależności i usuwa z pakietu każdy eksport, do którego nikt się nie odwołuje. Importujesz jedną funkcję z dużej biblioteki - do dist trafi ta jedna funkcja wraz z tym, czego sama potrzebuje. To dlatego statyczne importy na górze pliku są dziś standardem: dopiero one dają narzędziu pewność, co jest naprawdę używane.

Minifikacja to skracanie kodu, który zostaje. Znikają spacje, wcięcia i komentarze, a nazwy zmiennych lokalnych zamieniają się w pojedyncze litery. Zauważ różnicę: minifikator nie zastanawia się, czy dany moduł jest komukolwiek potrzebny, on go wyłącznie ściska. Samo usuwanie komentarzy jest zaledwie drobnym fragmentem tej pracy i z pewnością nie tym mechanizmem, który usuwa nieużywany kod.

Code splitting to podział wyniku na kilka plików - dokładnie to, co robiłeś przed chwilą przy codeSplitting. Tu nic nie ginie, kod zostaje jedynie przeniesiony do osobnego pakietu.

Lazy loading jest natomiast strategią w czasie działania aplikacji: pakiet powstały z podziału pobierany jest dopiero wtedy, gdy użytkownik faktycznie go potrzebuje. To skutek code splittingu, a nie osobna technika czyszczenia kodu.

Warto jeszcze rozprawić się z dwoma nieporozumieniami. Build niczego nie pakuje do archiwum ZIP - przeglądarka nie umiałaby takiego pliku uruchomić. Kompresja oczywiście istnieje, nazywa się gzip albo brotli, dzieje się na poziomie transportu HTTP, odpowiada za nią serwer i nie ma nic wspólnego z usuwaniem kodu. Nieprawdą jest również, że Vite niczego nie optymalizuje i trzeba do tego zewnętrznego narzędzia: vite build to Rolldown z kompletem optymalizacji włączonym domyślnie.

Leniwe trasy w praktyce

Code splitting nie musi być ręczny. Najskuteczniejszy podział bierze się z importu dynamicznego, czyli wywołania import() w środku funkcji zamiast statycznego import na górze pliku. Rolldown traktuje takie miejsce jak granicę: wszystko, co za nią, wędruje do osobnego pakietu. W routerze wygląda to tak, że zamiast gotowego komponentu podajesz funkcję, która dopiero go sprowadzi.

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})

Ekran startowy wchodzi do głównego pakietu, bo technik zobaczy go zawsze. Dwa pozostałe widoki dostają własne pliki, pobierane dopiero przy pierwszym wejściu na daną trasę - to właśnie taką nazwę widziałeś w przykładowej zawartości dist. Co się nie zmieniło: same komponenty. Plik TelemetryView.vue wygląda identycznie jak wcześniej, zmienił się wyłącznie sposób, w jaki router go wskazuje.

Zmierz, zanim zaczniesz optymalizować

Optymalizowanie na wyczucie zwykle kończy się dokręcaniem rzeczy, które ważą tyle co nic. Dlatego zanim ruszysz cokolwiek poza domyślne ustawienia, zmierz. Do pomiarów służy wtyczka rollup-plugin-visualizer, dokładana do zależności deweloperskich poleceniem npm install -D rollup-plugin-visualizer, a potem dopisywana do listy plugins. Opcja open otwiera raport w przeglądarce zaraz po zakończeniu builda, natomiast gzipSize i brotliSize dokładają do niego rozmiary po kompresji.

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})

Raport jest mapą prostokątów: im większe pole, tym cięższy moduł. Zwykle wystarcza jedno spojrzenie, żeby zobaczyć, że połowę pakietu zjada jedna biblioteka wciągnięta dla jednej funkcji. Patrz przede wszystkim na rozmiary skompresowane - to one odpowiadają temu, co naprawdę przejdzie przez łącze do lądownika, bo rozmiar surowy bywa kilkukrotnie większy i niepotrzebnie przeraża. Sama wtyczka niczego w pakiecie nie zmienia, wyłącznie mierzy, dlatego trzymaj ją na końcu listy plugins.

Obrazy ważą więcej niż kod

Ostatni przystanek, bo w typowym panelu to nie kod waży najwięcej. Jedno nieobrobione zdjęcie powierzchni Marsa potrafi przeważyć całą aplikację razem z Vue. Vite nie kompresuje obrazów samodzielnie - przenosi je do dist i dokłada skrót do nazwy, ale nie rusza zawartości. Od kompresji jest wtyczka vite-plugin-image-optimizer, instalowana przez npm install -D vite-plugin-image-optimizer. Potrzebuje ona jeszcze pakietów sharp i svgo, z których korzysta pod spodem. Dla każdego formatu podajesz quality, czyli jakość w skali do stu.

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})

Wartość 80 oznacza kompresję stratną, przy której różnicy zwykle nie widać gołym okiem, a plik chudnie o kilkadziesiąt procent. Najważniejsze jest jednak to, czego robić nie musisz: nie zmieniasz ani jednego <img> w szablonach i nie podmieniasz plików w katalogu src. Wtyczka pracuje na kopii wędrującej do dist, więc oryginały w repozytorium zostają nietknięte i w każdej chwili możesz zmienić ustawienia oraz zbudować wszystko od nowa.

Zbudowany, obejrzany i zmierzony ładunek to wszystko, czego potrzeba przed startem - zostaje pytanie, na jaką platformę go wysłać, i tym zajmiemy się w następnej lekcji. Do tego czasu wyrób sobie odruch: najpierw npm run build, potem npm run preview, potem spojrzenie na raport rozmiarów, a dopiero na końcu cokolwiek dalej.

Pamiętaj: npm run build nie jest kolejnym sposobem uruchomienia aplikacji - to spakowanie całego laboratorium w jeden ładunek, który poleci bez Ciebie.

Kod do tej lekcji: App.vue
1<script setup>
2import { ref, computed } from 'vue'
3
4// NOVA LAB - porownanie platform do wdrozen
5const platforms = ref([
6  {
7    id: 1,
8    name: 'Vercel',
9    logo: '▲',
10    features: ['Zero konfiguracji', 'Siec brzegowa (edge)', 'Funkcje serverless', 'Automatyczny SSL'],
11    buildTime: 45,
12    pricing: 'Dostepny darmowy plan',
13    bestFor: 'Aplikacje Next.js i React',
14    color: '#000'
15  },
16  {
17    id: 2,
18    name: 'Netlify',
19    logo: '◆',
20    features: ['Ciagle wdrazanie', 'Obsluga formularzy', 'Testy A/B', 'Funkcje edge'],
21    buildTime: 60,
22    pricing: 'Dostepny darmowy plan',
23    bestFor: 'Strony statyczne, JAMstack',
24    color: '#00C7B7'
25  },
26  {
27    id: 3,
28    name: 'Render',
29    logo: '●',
30    features: ['Aplikacje full stack', 'Bazy danych', 'Zadania cron', 'Procesy w tle'],
31    buildTime: 90,
32    pricing: 'Dostepny darmowy plan',
33    bestFor: 'Aplikacje full stack',
34    color: '#46E3B7'
35  },
36  {
37    id: 4,
38    name: 'Railway',
39    logo: '',
40    features: ['Wdrozenie jednym kliknieciem', 'Bazy danych', 'Wolumeny', 'Siec prywatna'],
41    buildTime: 75,
42    pricing: 'Oplaty za zuzycie',
43    bestFor: 'Uslugi backendowe',
44    color: '#A020F0'
45  }
46])
47
48const selectedPlatform = ref(null)
49const deployStatus = ref('idle') // idle, deploying, deployed, error
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>Wybor platformy wdrozeniowej</h1>
100      <p>NOVA LAB - wybierz platforme startowa</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            Czas buildu: ~{{ 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>Wdrozenie na {{ selectedPlatform.name }}</h2>
130
131      <div class="deploy-status" :class="deployStatus">
132        <template v-if="deployStatus === 'idle'">
133          Aplikacja misji marsjanskiej gotowa do wdrozenia
134        </template>
135        <template v-else-if="deployStatus === 'deploying'">
136          Wdrazanie... {{ Math.round(deployProgress) }}%
137        </template>
138        <template v-else-if="deployStatus === 'deployed'">
139          Wdrozenie zakonczone sukcesem!
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">Twoja misja marsjanska dziala juz na {{ 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          Wdroz teraz
159        </button>
160        <button
161          v-if="deployStatus === 'deployed'"
162          @click="resetDeployment"
163          class="reset-btn"
164        >
165          Nowe wdrozenie
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>

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Jaka komenda buduje aplikację Vue + Vite do produkcji?

  2. 2. W jakim folderze Vite domyślnie tworzy produkcyjny build?

To 2 z 6 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Uzupełnij vite.config.js z konfiguracją build: outDir, minify, sourcemap i wydzielenie pakietu vendor (w Vite 8 przez codeSplitting zamiast manualChunks).

  • Klikanie w kolejności

    Ułóż komendy w kolejności: budowanie i podgląd produkcyjnego buildu:

  • Układanie w poziomie

    Ułóż składnię odczytu zmiennej środowiskowej w Vite:

  • Edytor kodu

    Skonfiguruj pliki .env i .env.production ze zmiennymi VITE_API_URL i użyj ich w komponencie.

Przydatne artykuły