Kurs Vue.js · Moduł 12: Testy i wdrożenie
Budowanie wersji produkcyjnej
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 buildPo 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 previewZnak && 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.jsCią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/apiPrefiks 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. Jaka komenda buduje aplikację Vue + Vite do produkcji?
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.