Интеграция с Vite

Современные JavaScript-приложения требуют продуманных инструментов сборки. Nette Assets предлагает первоклассную интеграцию с Vite, сборщиком фронтенда нового поколения. Получите молниеносную разработку с горячей заменой модулей (HMR) и оптимизированные сборки для продакшна без возни с настройкой.

  • Никакой настройки – автоматический мост между Vite и PHP-шаблонами
  • Полное управление зависимостями – один тег занимается всеми ресурсами
  • Горячая замена модулей – мгновенные обновления JavaScript и CSS
  • Оптимизированные продакшн-сборки – разделение кода и tree shaking

Nette Assets бесшовно интегрируется с Vite, так что вы получаете все эти преимущества, а шаблоны пишете как обычно.

Настройка Vite

Настроим Vite шаг за шагом. Не переживайте, если вы новичок в инструментах сборки, мы всё объясним!

Шаг 1: установка Vite

Сначала установите в свой проект Vite и плагин для Nette:

npm install -D vite @nette/vite-plugin

Так устанавливается Vite и особый плагин, который помогает Vite прекрасно работать с Nette.

Шаг 2: структура проекта

Стандартный подход – разместить исходные файлы ресурсов в папке assets/ в корне проекта, а скомпилированные версии в www/assets/:

web-project/
├── assets/                   ← исходные файлы (SCSS, TypeScript, исходные изображения)
│   ├── public/               ← статические файлы (копируются как есть)
│   │   └── favicon.ico
│   ├── images/
│   │   └── logo.png
│   ├── app.js                ← главная точка входа
│   └── style.css             ← ваши стили
└── www/                      ← публичный каталог (document root)
	├── assets/               ← сюда попадут скомпилированные файлы
	└── index.php

Папка assets/ содержит ваши исходные файлы – код, который вы пишете. Vite обработает эти файлы и положит скомпилированные версии в www/assets/.

Шаг 3: настройка Vite

Создайте в корне проекта файл vite.config.ts. Этот файл говорит Vite, где искать исходные файлы и куда класть скомпилированные.

У плагина Vite для Nette есть умные значения по умолчанию, которые упрощают настройку. Он предполагает, что исходные файлы фронтенда лежат в каталоге assets/ (параметр root), а скомпилированные попадают в www/assets/ (параметр outDir). Вам нужно указать только точку входа:

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

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

Под капотом, кроме root и outDir, плагин задаёт ещё несколько параметров Vite, чтобы всё сходилось: base в '' (ресурсы отдаются прямо из document root), build.manifest в true (чтобы Nette Assets мог сопоставлять имена файлов с хешами) и build.assetsDir в '' (скомпилированные файлы попадают прямо в outDir, без подпапки static/). Любой из них можно переопределить.

outDir по умолчанию (www/assets) требует, чтобы каталог www/ уже существовал. Если его нет, плагин останавливается с ошибкой “The output directory … does not exist”.

Если вы хотите собирать ресурсы в каталог с другим именем, вам потребуется изменить несколько параметров:

export default defineConfig({
	root: 'assets', // корневой каталог исходных ресурсов

	build: {
		outDir: '../www/assets',  // куда попадают скомпилированные файлы
	},

	// ... остальная конфигурация ...
});

Путь outDir считается относительно root, поэтому в начале стоит ../.

Шаг 4: настройка Nette

Расскажите Nette Assets о Vite в своём common.neon:

assets:
	mapping:
		default:
			type: vite      # говорит Nette использовать ViteMapper
			path: assets

Шаг 5: добавление скриптов

Добавьте в свой package.json эти скрипты:

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

Теперь вы можете:

  • npm run dev – запустить сервер разработки с горячей перезагрузкой
  • npm run build – создать оптимизированные файлы для продакшна

Точки входа

Точка входа – главный файл, с которого начинается ваше приложение. Из этого файла вы импортируете другие файлы (CSS, модули JavaScript, изображения), создавая дерево зависимостей. Vite идёт по этим импортам и собирает всё вместе.

Пример точки входа assets/app.js:

// Импортируем стили
import './style.css'

// Импортируем модули JavaScript
import netteForms from 'nette-forms';
import naja from 'naja';

// Инициализируем приложение
netteForms.initOnLoad();
naja.initialize();

В шаблоне точку входа можно вставить так:

{asset 'app.js'}

Nette Assets автоматически породит все необходимые HTML-теги: JavaScript, CSS и любые другие зависимости.

Несколько точек входа

Более крупным приложениям часто нужны отдельные точки входа:

export default defineConfig({
	plugins: [
		nette({
			entry: [
				'app.js',      // публичные страницы
				'admin.js',    // административная панель
			],
		}),
	],
});

Используйте их в разных шаблонах:

{* На публичных страницах *}
{asset 'app.js'}

{* В административной панели *}
{asset 'admin.js'}

Важно: исходные и скомпилированные файлы

Принципиально важно понимать, что в продакшне вы можете загружать только файлы, которые Vite делает доступными: либо через свой манифест, либо копируя их без изменений из публичной папки:

  1. Точки входа, определённые в entry (включая модули, которые они динамически импортируют), и ресурсы, на которые ссылаются из JavaScript или CSS (изображения, шрифты, …) – всё это записывается в манифест
  2. Файлы из каталога assets/public/ – их в манифесте нет; они копируются как есть, и {asset} находит их запасным путём через файловую систему

Вы не можете загружать через {asset} произвольные файлы из assets/: если на файл нигде нет ссылки, он не будет скомпилирован. Если вы хотите, чтобы Vite знал о других ресурсах, перенесите их в публичную папку.

Учтите, что по умолчанию Vite встраивает все ресурсы меньше 4 КБ, так что сослаться на эти файлы напрямую вы не сможете. (См. документацию Vite).

{* ✓ Это работает - это точка входа *}
{asset 'app.js'}

{* ✓ Это работает - файл в assets/public/ *}
{asset 'favicon.ico'}

{* ✗ Это не сработает - произвольный файл в assets/ *}
{asset 'components/button.js'}

Режим разработки

Режим разработки совершенно необязателен, но, если его включить, он даёт существенные преимущества. Главное из них – горячая замена модулей (HMR): изменения видны мгновенно и без потери состояния приложения, что делает разработку намного плавнее и быстрее.

Vite – современный инструмент сборки, который делает разработку невероятно быстрой. В отличие от традиционных сборщиков, Vite при разработке отдаёт ваш код прямо в браузер, а значит сервер запускается мгновенно независимо от размера проекта, и обновления происходят молниеносно.

Запуск сервера разработки

Запустите сервер разработки:

npm run dev

Вы увидите:

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

Держите этот терминал открытым во время разработки.

Пока dev-сервер работает, плагин записывает небольшой сигнальный файл www/assets/.vite/nette.json с его URL. Nette Assets на стороне PHP читает этот файл и переключается на загрузку с dev-сервера, когда выполняется и то и другое:

  1. dev-сервер Vite запущен (сигнальный файл существует) и
  2. ваше приложение на Nette находится в режиме отладки.

Результат:

{asset 'app.js'}
{* При разработке: <script src="http://localhost:5173/@vite/client" type="module"></script>
                   <script src="http://localhost:5173/app.js" type="module"></script> *}
{* В продакшне: <script src="/assets/app-4f3a2b1c.js" type="module" crossorigin></script> *}

Никакой настройки не нужно, оно просто работает! Чтобы отключить определение или задать URL dev-сервера вручную, смотрите параметр devServer.

Сигнальный файл живёт в www/assets/.vite/nette.json (прямо рядом с продакшн-файлом manifest.json). Переименовать его можно параметром плагина infoFile, который по умолчанию равен .vite/nette.json. Если Nette не подхватывает запущенный dev-сервер, проверьте, что этот файл существует и указывает на верный URL.

Работа на разных доменах

Если ваш сервер разработки работает не на localhost (например, на myapp.local), вы можете столкнуться с проблемами CORS (Cross-Origin Resource Sharing). CORS – механизм безопасности веб-браузеров, который по умолчанию блокирует запросы между разными доменами. Когда ваше PHP-приложение работает на myapp.local, а Vite на localhost:5173, браузер видит их как разные домены и блокирует запросы.

Решить это можно двумя способами:

Вариант 1: настроить CORS

Проще всего разрешить запросы с другого источника из вашего PHP-приложения:

export default defineConfig({
	// ... остальная конфигурация ...

	server: {
		cors: {
			origin: 'http://myapp.local',  // URL вашего PHP-приложения
		},
	},
});

Вариант 2: запустить Vite на своём домене

Другое решение – сделать так, чтобы Vite работал на том же домене, что и ваше PHP-приложение.

export default defineConfig({
	// ... остальная конфигурация ...

	server: {
		host: 'myapp.local',  // так же, как у вашего PHP-приложения
	},
});

На самом деле даже в этом случае CORS настроить нужно, потому что dev-сервер работает на том же имени хоста, но на другом порту. Однако в этом случае CORS настраивается автоматически плагином Vite для Nette.

Разработка по HTTPS

Если вы разрабатываете по HTTPS, вашему серверу разработки Vite нужны сертификаты. Проще всего воспользоваться плагином, который порождает сертификаты автоматически:

npm install -D vite-plugin-mkcert

Вот как настроить его в vite.config.ts:

import mkcert from 'vite-plugin-mkcert';

export default defineConfig({
	// ... остальная конфигурация ...

	plugins: [
		mkcert(),  // порождает сертификаты автоматически и включает https
		nette(),
	],
});

Обратите внимание, что если вы используете настройку CORS (вариант 1 выше), вам нужно поправить URL origin, чтобы он использовал https:// вместо http://.

Разработка в Docker

Когда вы запускаете Vite внутри контейнера Docker, нужно позаботиться о двух вещах: браузер на вашей машине должен добираться до dev-сервера, а Vite должен замечать изменения файлов через границу контейнера.

Сначала опубликуйте порт Vite из контейнера и привяжите dev-сервер ко всем интерфейсам, чтобы он был доступен снаружи контейнера:

export default defineConfig({
	// ... остальная конфигурация ...

	plugins: [
		nette(),
	],
	server: {
		host: '0.0.0.0',      // слушаем на всех интерфейсах (в контейнере обязательно)
		port: 5173,           // должен совпадать с опубликованным портом
		strictPort: true,     // лучше упасть, чем выбрать другой порт
		watch: {
			usePolling: true, // включите, если изменения файлов на смонтированных томах не замечаются
		},
	},
});

Плагин записывает URL dev-сервера в nette.json для стороны PHP. Поскольку host: '0.0.0.0' браузеру не годится (он перенаправляет на localhost для каждого ресурса), плагин автоматически переписывает его в этом URL на localhost, чтобы ресурсы загружались правильно.

Если вы открываете приложение на собственном домене, а не на localhost, задайте параметр плагина host равным этому домену:

	plugins: [
		nette({ host: 'myapp.local' }),  // тот же домен, что и у вашего PHP-приложения
	],

Плагин тогда использует этот хост для URL dev-сервера, добавляет его в источники CORS и вносит в белый список allowedHosts в Vite, так что всё работает без ручной настройки CORS.

Для более сложных схем, например когда Vite работает за обратным прокси, у которого публичный хост, порт и протокол отличаются от внутреннего адреса, задайте в Vite server.origin равным полному публичному URL. Плагин его учитывает и записывает в nette.json как есть, вместо того чтобы выводить URL из локального сокета:

	server: {
		origin: 'https://myapp.local:8443',  // публичный URL, по которому браузер добирается до Vite
	},

Сборки для продакшна

Создайте оптимизированные файлы для продакшна:

npm run build

Vite:

  • минифицирует весь JavaScript и CSS
  • разделит код на оптимальные куски
  • породит имена файлов с хешами для сброса кеша
  • создаст файл манифеста для Nette Assets

Пример вывода:

www/assets/
├── app-4f3a2b1c.js       # Ваш основной JavaScript (минифицированный)
├── app-7d8e9f2a.css      # Извлечённый CSS (минифицированный)
├── vendor-8c4b5e6d.js    # Общие зависимости
└── .vite/
	└── manifest.json     # Сопоставление для Nette Assets

Имена файлов с хешами обеспечивают, что браузеры всегда загружают свежую версию.

Публичная папка

Файлы из каталога assets/public/ копируются в вывод без обработки:

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

Ссылайтесь на них обычным образом:

{* Эти файлы копируются как есть *}
<link rel="icon" href={asset 'favicon.ico'}>
<meta property="og:image" content={asset 'images/og-image.jpg'}>

Для публичных файлов можно использовать возможности FilesystemMapper. Параметр extension относится к ссылкам без расширения (например, {asset 'images/og-image'} сначала найдёт og-image.webp):

assets:
	mapping:
		default:
			type: vite
			path: assets
			extension: [webp, jpg, png]  # для ссылок без расширения
			versioning: true             # Добавляет сброс кеша

В конфигурации vite.config.ts публичную папку можно сменить параметром publicDir.

Динамические импорты

Vite автоматически разделяет код ради оптимальной загрузки. Динамические импорты позволяют загружать код только тогда, когда он действительно нужен, уменьшая размер начального пакета:

// Загружаем тяжёлые компоненты по требованию
button.addEventListener('click', async () => {
	let { Chart } = await import('./components/chart.js')
	new Chart(data)
})

Динамические импорты создают отдельные куски, которые загружаются только при надобности. Это называется “разделением кода” и является одной из самых мощных возможностей Vite. Когда вы используете динамические импорты, Vite автоматически создаёт отдельные файлы JavaScript для каждого динамически импортируемого модуля.

Тег {asset 'app.js'} не предзагружает эти динамические куски автоматически. Это намеренное поведение: мы не хотим скачивать код, который, возможно, никогда не понадобится. Куски скачиваются, только когда выполняется динамический импорт.

Однако если вы знаете, что определённые динамические импорты критичны и скоро понадобятся, вы можете их предзагрузить:

{* Главная точка входа *}
{asset 'app.js'}

{* Предзагружаем критичные динамические импорты *}
{preload 'components/chart.js'}

Это говорит браузеру скачать компонент диаграммы в фоне, чтобы он был готов сразу, как только понадобится.

Поддержка TypeScript

TypeScript работает сразу:

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

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

Ссылайтесь на файлы TypeScript обычным образом (как и с любым файлом, main.ts должен быть точкой входа):

{asset 'main.ts'}

Для полной поддержки TypeScript установите его:

npm install -D typescript

Дополнительная настройка Vite

Вот несколько полезных параметров конфигурации Vite с подробными пояснениями:

export default defineConfig({
	// Корневой каталог с исходными ресурсами
	root: 'assets',

	// Папка, содержимое которой копируется в каталог вывода как есть
	// По умолчанию: 'public' (относительно 'root')
	publicDir: 'public',

	build: {
		// Куда класть скомпилированные файлы (относительно 'root')
		outDir: '../www/assets',

		// Очищать каталог вывода перед сборкой?
		// Полезно, чтобы убрать старые файлы прошлых сборок
		emptyOutDir: true,

		// Подкаталог внутри outDir для порождённых кусков и ресурсов
		// Помогает упорядочить структуру вывода
		assetsDir: 'static',

		rollupOptions: {
			// Точка входа или несколько - может быть одним файлом или массивом файлов
			// Каждая точка входа становится отдельным пакетом
			input: [
				'app.js',      // основное приложение
				'admin.js',    // административная панель
			],
		},
	},

	server: {
		// Хост, к которому привязывается dev-сервер
		// Используйте '0.0.0.0', чтобы открыть его в сеть
		host: 'localhost',

		// Порт dev-сервера
		port: 5173,

		// Настройка CORS для запросов с другого источника
		cors: {
			origin: 'http://myapp.local',
		},
	},

	css: {
		// Включить карты исходников CSS при разработке
		devSourcemap: true,
	},

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

Осторожно с путями точек входа в rollupOptions.input выше: Rollup разрешает относительные пути относительно текущего рабочего каталога (корня вашего проекта), а не относительно root: 'assets'. Так что голого 'app.js' во время сборки не окажется. Либо используйте параметр плагина entry (который разрешает пути точек входа относительно root), либо пишите пути относительно корня проекта, например 'assets/app.js'.

Вот и всё! Теперь у вас есть современная система сборки, интегрированная с Nette Assets.

версия: 1.x