Integracja z Vite

Nowoczesne aplikacje JavaScriptowe wymagają zaawansowanych narzędzi budujących. Nette Assets daje pierwszorzędną integrację z Vite, narzędziem budującym frontend nowej generacji. Uzyskaj błyskawiczne tworzenie z Hot Module Replacement (HMR) i zoptymalizowane buildy produkcyjne bez cierpienia z konfiguracją.

  • Zero konfiguracji – automatyczny most między Vite a szablonami PHP
  • Kompletne zarządzanie zależnościami – jeden tag obsługuje wszystkie zasoby
  • Hot Module Replacement – natychmiastowe aktualizacje JavaScriptu i CSS
  • Zoptymalizowane buildy produkcyjne – code splitting i tree shaking

Nette Assets integruje się z Vite płynnie, więc dostajesz wszystkie te korzyści, pisząc szablony jak zwykle.

Ustawienie Vite

Ustawmy Vite krok po kroku. Nie martw się, jeśli narzędzia budujące są dla Ciebie nowe, wszystko wyjaśnimy!

Krok 1: instalacja Vite

Najpierw zainstaluj w swoim projekcie Vite i plugin Nette:

npm install -D vite @nette/vite-plugin

Instaluje to Vite i specjalny plugin, który pomaga Vite doskonale współpracować z Nette.

Krok 2: struktura projektu

Standardowe podejście to umieszczenie źródłowych plików zasobów w folderze assets/ w katalogu głównym projektu, a wersji skompilowanych w www/assets/:

web-project/
├── assets/                   ← pliki źródłowe (SCSS, TypeScript, obrazki źródłowe)
│   ├── public/               ← pliki statyczne (kopiowane bez zmian)
│   │   └── favicon.ico
│   ├── images/
│   │   └── logo.png
│   ├── app.js                ← główny punkt wejścia
│   └── style.css             ← Twoje style
└── www/                      ← katalog publiczny (document root)
	├── assets/               ← tutaj trafią pliki skompilowane
	└── index.php

Folder assets/ zawiera Twoje pliki źródłowe, czyli kod, który piszesz. Vite przetworzy te pliki i umieści wersje skompilowane w www/assets/.

Krok 3: konfiguracja Vite

Utwórz w katalogu głównym projektu plik vite.config.ts. Plik ten mówi Vite, gdzie znaleźć Twoje pliki źródłowe i gdzie umieścić skompilowane.

Plugin Vite dla Nette przychodzi ze sprytnymi wartościami domyślnymi, które upraszczają konfigurację. Zakłada, że Twoje źródłowe pliki front-endowe są w katalogu assets/ (opcja root), a pliki skompilowane trafiają do www/assets/ (opcja outDir). Musisz podać tylko punkt wejścia:

import { defineConfig } from 'vite';
import nette from '@nette/vite-plugin';

export default defineConfig({
	plugins: [
		nette({
			entry: 'app.js',
		}),
	],
});

Pod maską, oprócz root i outDir, plugin ustawia jeszcze kilka opcji Vite, żeby wszystko do siebie pasowało: base na '' (zasoby serwowane są prosto z document rootu), build.manifest na true (żeby Nette Assets mogło zmapować zahashowane nazwy plików) i build.assetsDir na '' (pliki skompilowane lądują bezpośrednio w outDir, bez podfolderu static/). Każdą z nich możesz nadpisać.

Domyślny outDir (www/assets) wymaga, żeby katalog www/ już istniał. Jeśli nie istnieje, plugin zatrzymuje się z błędem “The output directory … does not exist”.

Jeśli chcesz podać inną nazwę katalogu do budowania swoich zasobów, musisz zmienić kilka opcji:

export default defineConfig({
	root: 'assets', // katalog główny zasobów źródłowych

	build: {
		outDir: '../www/assets',  // gdzie trafiają pliki skompilowane
	},

	// ... pozostała konfiguracja ...
});

Ścieżka outDir traktowana jest jako względna wobec root, dlatego na początku jest ../.

Krok 4: konfiguracja Nette

Powiedz Nette Assets o Vite w swoim common.neon:

assets:
	mapping:
		default:
			type: vite      # mówi Nette, żeby użyło ViteMappera
			path: assets

Krok 5: dodanie skryptów

Dodaj te skrypty do swojego package.json:

{
	"scripts": {
		"dev": "vite",
		"build": "vite build"
	}
}

Teraz możesz:

  • npm run dev – uruchomić serwer deweloperski z hot reloadingiem
  • npm run build – utworzyć zoptymalizowane pliki produkcyjne

Punkty wejścia

Punkt wejścia to główny plik, w którym zaczyna się Twoja aplikacja. Z tego pliku importujesz inne pliki (CSS, moduły JavaScriptu, obrazki), tworząc drzewo zależności. Vite podąża za tymi importami i pakuje wszystko razem.

Przykładowy punkt wejścia assets/app.js:

// Importujemy style
import './style.css'

// Importujemy moduły JavaScriptu
import netteForms from 'nette-forms';
import naja from 'naja';

// Inicjalizujemy swoją aplikację
netteForms.initOnLoad();
naja.initialize();

W szablonie punkt wejścia możesz wstawić tak:

{asset 'app.js'}

Nette Assets automatycznie generuje wszystkie potrzebne tagi HTML: JavaScript, CSS i wszelkie inne zależności.

Wiele punktów wejścia

Większe aplikacje często potrzebują osobnych punktów wejścia:

export default defineConfig({
	plugins: [
		nette({
			entry: [
				'app.js',      // strony publiczne
				'admin.js',    // panel administracyjny
			],
		}),
	],
});

Używaj ich w różnych szablonach:

{* Na stronach publicznych *}
{asset 'app.js'}

{* W panelu administracyjnym *}
{asset 'admin.js'}

Ważne: pliki źródłowe kontra skompilowane

Kluczowe jest zrozumienie, że w produkcji możesz wczytywać tylko te pliki, które Vite udostępnia: albo przez swój manifest, albo kopiując je bez zmian z folderu public:

  1. Punkty wejścia zdefiniowane w entry (wraz z modułami, które dynamicznie importują) i zasoby, do których odwołuje się JavaScript albo CSS (obrazki, fonty, …) – wszystko to zapisane jest w manifeście
  2. Pliki z katalogu assets/public/ – tych nie ma w manifeście; kopiowane są bez zmian, a {asset} znajduje je przez fallback do systemu plików

Nie możesz wczytywać przez {asset} dowolnych plików z assets/: jeśli plik nie jest nigdzie referencjonowany, nie zostanie skompilowany. Jeśli chcesz, żeby Vite wiedziało o innych zasobach, możesz przenieść je do folderu public.

Zwróć uwagę, że domyślnie Vite osadza inline wszystkie zasoby mniejsze niż 4 kB, więc do tych plików nie będziesz mógł odwołać się bezpośrednio. (Patrz dokumentacja Vite).

{* ✓ To działa - to punkt wejścia *}
{asset 'app.js'}

{* ✓ To działa - jest w assets/public/ *}
{asset 'favicon.ico'}

{* ✗ To nie zadziała - przypadkowy plik w assets/ *}
{asset 'components/button.js'}

Tryb deweloperski

Tryb deweloperski jest całkowicie opcjonalny, ale po włączeniu daje znaczne korzyści. Główną zaletą jest Hot Module Replacement (HMR): widzisz zmiany natychmiast, bez utraty stanu aplikacji, przez co tworzenie jest znacznie płynniejsze i szybsze.

Vite to nowoczesne narzędzie budujące, które czyni tworzenie niesamowicie szybkim. W przeciwieństwie do tradycyjnych bundlerów Vite serwuje Twój kod przeglądarce bezpośrednio podczas tworzenia, co oznacza natychmiastowy start serwera niezależnie od wielkości projektu i błyskawiczne aktualizacje.

Uruchomienie serwera deweloperskiego

Uruchom serwer deweloperski:

npm run dev

Zobaczysz:

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose

Zostaw ten terminal otwarty podczas tworzenia.

Gdy serwer deweloperski działa, plugin zapisuje mały plik sygnalizacyjny www/assets/.vite/nette.json zawierający jego URL. Nette Assets po stronie PHP czyta ten plik i przełącza się na wczytywanie z serwera deweloperskiego, gdy jednocześnie:

  1. serwer deweloperski Vite działa (plik sygnalizacyjny istnieje) oraz
  2. Twoja aplikacja Nette jest w trybie debug.

Wynik:

{asset 'app.js'}
{* Przy tworzeniu: <script src="http://localhost:5173/@vite/client" type="module"></script>
                   <script src="http://localhost:5173/app.js" type="module"></script> *}
{* W produkcji: <script src="/assets/app-4f3a2b1c.js" type="module" crossorigin></script> *}

Żadna konfiguracja nie jest potrzebna, po prostu działa! Żeby wyłączyć wykrywanie albo ustawić URL serwera deweloperskiego ręcznie, patrz opcja devServer.

Plik sygnalizacyjny leży w www/assets/.vite/nette.json (zaraz obok produkcyjnego manifest.json). Możesz go przemianować opcją pluginu infoFile, która domyślnie ma wartość .vite/nette.json. Jeśli Nette nie wykrywa działającego serwera deweloperskiego, sprawdź, czy plik ten istnieje i wskazuje właściwy URL.

Praca na różnych domenach

Jeśli Twój serwer deweloperski działa na czymś innym niż localhost (jak myapp.local), możesz natrafić na problemy z CORS (Cross-Origin Resource Sharing). CORS to funkcja bezpieczeństwa przeglądarek, która domyślnie blokuje żądania między różnymi domenami. Gdy Twoja aplikacja PHP działa na myapp.local, a Vite na localhost:5173, przeglądarka widzi je jako różne domeny i blokuje żądania.

Masz dwie możliwości, żeby to rozwiązać:

Opcja 1: skonfiguruj CORS

Najprostszym rozwiązaniem jest dopuszczenie żądań cross-origin z Twojej aplikacji PHP:

export default defineConfig({
	// ... pozostała konfiguracja ...

	server: {
		cors: {
			origin: 'http://myapp.local',  // URL Twojej aplikacji PHP
		},
	},
});

Opcja 2: uruchom Vite na swojej domenie

Drugim rozwiązaniem jest sprawienie, żeby Vite działało na tej samej domenie co Twoja aplikacja PHP.

export default defineConfig({
	// ... pozostała konfiguracja ...

	server: {
		host: 'myapp.local',  // tak samo jak Twoja aplikacja PHP
	},
});

Właściwie nawet w tym przypadku musisz skonfigurować CORS, bo serwer deweloperski działa na tej samej nazwie hosta, ale na innym porcie. W tym przypadku CORS konfiguruje jednak automatycznie plugin Vite dla Nette.

Tworzenie na HTTPS

Jeśli tworzysz na HTTPS, potrzebujesz certyfikatów dla swojego serwera deweloperskiego Vite. Najprościej użyć pluginu generującego certyfikaty automatycznie:

npm install -D vite-plugin-mkcert

Oto jak skonfigurować go w vite.config.ts:

import mkcert from 'vite-plugin-mkcert';

export default defineConfig({
	// ... pozostała konfiguracja ...

	plugins: [
		mkcert(),  // generuje certyfikaty automatycznie i włącza https
		nette(),
	],
});

Zwróć uwagę, że jeśli używasz konfiguracji CORS (opcja 1 powyżej), musisz zaktualizować URL origin, żeby używał https:// zamiast http://.

Tworzenie w Dockerze

Gdy uruchamiasz Vite wewnątrz kontenera Dockera, dwie rzeczy wymagają uwagi: przeglądarka na Twojej maszynie musi móc dosięgnąć serwera deweloperskiego, a Vite musi wykrywać zmiany plików przez granicę kontenera.

Najpierw opublikuj port Vite z kontenera i przypnij serwer deweloperski do wszystkich interfejsów, żeby był osiągalny spoza kontenera:

export default defineConfig({
	// ... pozostała konfiguracja ...

	plugins: [
		nette(),
	],
	server: {
		host: '0.0.0.0',      // nasłuchuj na wszystkich interfejsach (wymagane w kontenerze)
		port: 5173,           // musi odpowiadać opublikowanemu portowi
		strictPort: true,     // raczej zawiedź, niż wybieraj inny port
		watch: {
			usePolling: true, // włącz, jeśli zmiany plików nie są wykrywane na zamontowanych wolumenach
		},
	},
});

Plugin zapisuje URL serwera deweloperskiego do nette.json dla strony PHP. Ponieważ host: '0.0.0.0' nie nadaje się do użycia przez przeglądarkę (dla każdego zasobu przekierowuje na localhost), plugin automatycznie przepisuje go w tym URL na localhost, żeby zasoby wczytywały się poprawnie.

Jeśli otwierasz aplikację na własnej domenie zamiast na localhost, ustaw opcję pluginu host na tę domenę:

	plugins: [
		nette({ host: 'myapp.local' }),  // ta sama domena co Twoja aplikacja PHP
	],

Plugin używa wtedy tego hosta w URL serwera deweloperskiego, dodaje go do origins CORS i wpisuje na białą listę allowedHosts Vite, więc działa bez żadnej ręcznej konfiguracji CORS.

Dla bardziej złożonych konfiguracji, na przykład gdy Vite działa za reverse proxy, gdzie publiczny host, port i protokół różnią się od adresu wewnętrznego, ustaw server.origin Vite na pełny publiczny URL. Plugin respektuje go i zapisuje do nette.json bez zmian, zamiast wyprowadzać URL z lokalnego gniazda:

	server: {
		origin: 'https://myapp.local:8443',  // publiczny URL, pod którym przeglądarka sięga po Vite
	},

Buildy produkcyjne

Utwórz zoptymalizowane pliki produkcyjne:

npm run build

Vite:

  • Zminifikuje cały JavaScript i CSS
  • Podzieli kod na optymalne chunki
  • Wygeneruje zahashowane nazwy plików na potrzeby cache bustingu
  • Utworzy plik manifestu dla Nette Assets

Przykładowe wyjście:

www/assets/
├── app-4f3a2b1c.js       # Twój główny JavaScript (zminifikowany)
├── app-7d8e9f2a.css      # Wyodrębniony CSS (zminifikowany)
├── vendor-8c4b5e6d.js    # Wspólne zależności
└── .vite/
	└── manifest.json     # Mapowanie dla Nette Assets

Zahashowane nazwy plików zapewniają, że przeglądarki zawsze wczytują najnowszą wersję.

Folder public

Pliki w katalogu assets/public/ kopiowane są na wyjście bez przetwarzania:

assets/
├── public/
│   ├── favicon.ico
│   ├── robots.txt
│   └── images/
│       └── og-image.jpg
├── app.js
└── style.css

Odwołuj się do nich normalnie:

{* Te pliki kopiowane są bez zmian *}
<link rel="icon" href={asset 'favicon.ico'}>
<meta property="og:image" content={asset 'images/og-image.jpg'}>

Dla plików publicznych możesz używać funkcji FilesystemMappera. Opcja extension dotyczy odwołań bez rozszerzenia (np. {asset 'images/og-image'} znajdzie najpierw og-image.webp):

assets:
	mapping:
		default:
			type: vite
			path: assets
			extension: [webp, jpg, png]  # dla odwołań bez rozszerzenia
			versioning: true             # Dodaje cache busting

W konfiguracji vite.config.ts możesz zmienić folder public opcją publicDir.

Importy dynamiczne

Vite automatycznie dzieli kod na potrzeby optymalnego wczytywania. Importy dynamiczne pozwalają wczytywać kod dopiero wtedy, gdy jest faktycznie potrzebny, zmniejszając początkowy rozmiar bundle'a:

// Wczytujemy ciężkie komponenty na żądanie
button.addEventListener('click', async () => {
	let { Chart } = await import('./components/chart.js')
	new Chart(data)
})

Importy dynamiczne tworzą osobne chunki wczytywane tylko wtedy, gdy są potrzebne. Nazywa się to “code splitting” i jest jedną z najpotężniejszych funkcji Vite. Gdy używasz importów dynamicznych, Vite automatycznie tworzy osobne pliki JavaScript dla każdego dynamicznie importowanego modułu.

Tag {asset 'app.js'} nie preładowuje tych dynamicznych chunków automatycznie. To zachowanie zamierzone: nie chcemy pobierać kodu, który może nigdy nie zostać użyty. Chunki pobierane są dopiero wtedy, gdy import dynamiczny zostanie wykonany.

Jeśli jednak wiesz, że pewne importy dynamiczne są krytyczne i będą wkrótce potrzebne, możesz je preładować:

{* Główny punkt wejścia *}
{asset 'app.js'}

{* Preładowanie krytycznych importów dynamicznych *}
{preload 'components/chart.js'}

Mówi to przeglądarce, żeby pobrała komponent wykresu w tle, dzięki czemu jest gotowy natychmiast, gdy będzie potrzebny.

Wsparcie dla TypeScriptu

TypeScript działa od razu:

// assets/main.ts
interface User {
	name: string
	email: string
}

export function greetUser(user: User): void {
	console.log(`Hello, ${user.name}!`)
}

Odwołuj się do plików TypeScript normalnie (jak przy każdym pliku, main.ts musi być punktem wejścia):

{asset 'main.ts'}

Dla pełnego wsparcia TypeScriptu zainstaluj go:

npm install -D typescript

Dodatkowa konfiguracja Vite

Oto kilka przydatnych opcji konfiguracyjnych Vite ze szczegółowymi wyjaśnieniami:

export default defineConfig({
	// Katalog główny zawierający zasoby źródłowe
	root: 'assets',

	// Folder, którego zawartość kopiowana jest do katalogu wyjściowego bez zmian
	// Domyślnie: 'public' (względem 'root')
	publicDir: 'public',

	build: {
		// Gdzie umieszczać pliki skompilowane (względem 'root')
		outDir: '../www/assets',

		// Opróżnić katalog wyjściowy przed budowaniem?
		// Przydatne do usunięcia starych plików z poprzednich buildów
		emptyOutDir: true,

		// Podkatalog w outDir na generowane chunki i zasoby
		// Pomaga to uporządkować strukturę wyjścia
		assetsDir: 'static',

		rollupOptions: {
			// Punkt(y) wejścia - może być pojedynczy plik albo tablica plików
			// Każdy punkt wejścia staje się osobnym bundlem
			input: [
				'app.js',      // główna aplikacja
				'admin.js',    // panel administracyjny
			],
		},
	},

	server: {
		// Host, do którego przypinany jest serwer deweloperski
		// Użyj '0.0.0.0', żeby udostępnić go w sieci
		host: 'localhost',

		// Port serwera deweloperskiego
		port: 5173,

		// Konfiguracja CORS dla żądań cross-origin
		cors: {
			origin: 'http://myapp.local',
		},
	},

	css: {
		// Włącza source mapy CSS przy tworzeniu
		devSourcemap: true,
	},

	plugins: [
		nette(),
	],
});

Uważaj na ścieżki punktów wejścia w powyższym rollupOptions.input: Rollup rozwiązuje ścieżki względne wobec bieżącego katalogu roboczego (katalogu głównego Twojego projektu), a nie wobec root: 'assets'. Samo 'app.js' nie będzie więc istnieć w czasie budowania. Albo użyj opcji pluginu entry (która rozwiązuje ścieżki punktów wejścia względem root), albo zapisz ścieżki względem katalogu głównego projektu, np. 'assets/app.js'.

I to wszystko! Masz teraz nowoczesny system budowania zintegrowany z Nette Assets.

wersja: 1.x