Nette Assets

Устали вручную управлять статическими файлами в своих веб-приложениях? Забудьте о жёстко прописанных путях, возне со сбросом кеша и заботах о версиях файлов. Nette Assets меняет то, как вы работаете с изображениями, таблицами стилей, скриптами и другими статическими ресурсами.

  • Умное версионирование обеспечивает, что браузеры всегда загружают свежие файлы
  • Автоматическое определение типов файлов и размеров
  • Бесшовная интеграция с Latte через интуитивные теги
  • Гибкая архитектура с поддержкой файловых систем, CDN и Vite
  • Ленивая загрузка ради максимальной производительности

Зачем нужен Nette Assets?

Работа со статическими файлами часто означает повторяющийся код, в котором легко ошибиться. Вы вручную составляете URL, добавляете параметры версии для сброса кеша и по-разному обрабатываете разные типы файлов. Это приводит к коду вроде такого:

<img src="/images/logo.png?v=1699123456" width="200" height="100" alt="Logo">
<link rel="stylesheet" href="/css/style.css?v=2">

С Nette Assets вся эта сложность исчезает:

{* Всё автоматизировано - URL, версионирование, размеры *}
<img n:asset="images/logo.png">
<link n:asset="css/style.css">

{* Или просто *}
{asset 'css/style.css'}

Вот и всё! Библиотека автоматически:

  • добавляет параметры версии по времени изменения файла
  • определяет размеры изображений и включает их в HTML
  • порождает правильный HTML-элемент для каждого типа файла
  • справляется и со средой разработки, и с продакшном

Установка

Установите Nette Assets с помощью Composer:

composer require nette/assets

Он требует PHP 8.1 или новее и прекрасно работает с Nette Framework, но может использоваться и самостоятельно.

Первые шаги

Nette Assets работает сразу, без всякой настройки. Поместите свои статические файлы в каталог www/assets/ и начинайте их использовать:

{* Показываем изображение с автоматическими размерами *}
{asset 'logo.png'}

{* Подключаем таблицу стилей с версионированием *}
{asset 'style.css'}

{* Загружаем скрипт *}
{asset 'app.js'}

Для большего контроля над порождаемым HTML используйте атрибут n:asset или функцию asset().

Как это работает

Nette Assets построен вокруг трёх основных понятий, которые делают его мощным и при этом простым в использовании:

Ресурсы: ваши файлы становятся умнее

Ресурс (asset) представляет любой статический файл в вашем приложении. Каждый файл становится объектом с полезными свойствами только для чтения:

$image = $assets->getAsset('photo.jpg');
echo $image->url;      // '/assets/photo.jpg?v=1699123456'
echo $image->file;     // '/var/www/assets/photo.jpg' (локальный путь либо null)
echo $image->width;    // 1920
echo $image->height;   // 1080
echo $image->mimeType; // 'image/jpeg'

Разные типы файлов дают разные свойства:

  • Изображения: ширина, высота, альтернативный текст, ленивая загрузка
  • Скрипты: тип модуля, хеши целостности, crossorigin
  • Таблицы стилей: медиазапросы, целостность
  • Аудио и видео: длительность, размеры (только видео)
  • Шрифты: правильная предзагрузка с CORS

Библиотека автоматически определяет типы файлов и создаёт подходящий класс ресурса.

Мапперы: откуда берутся файлы

Маппер знает, как найти файлы и создать для них URL. У вас может быть несколько мапперов для разных задач: локальные файлы, CDN, облачное хранилище или инструменты сборки (у каждого из них есть имя). Встроенный FilesystemMapper занимается локальными файлами, а ViteMapper интегрируется с современными инструментами сборки.

Мапперы определяются в конфигурации.

Реестр: ваш основной интерфейс

Реестр управляет всеми мапперами и предоставляет основной API:

// Внедряем реестр в свой сервис
public function __construct(
	private Nette\Assets\Registry $assets
) {}
// Получаем ресурсы из разных мапперов
$logo = $this->assets->getAsset('images:logo.png'); // маппер 'images'
$app = $this->assets->getAsset('app:main.js'); // маппер 'app'
$style = $this->assets->getAsset('style.css'); // использует маппер по умолчанию

Реестр автоматически выбирает нужный маппер и кеширует результаты ради производительности.

Работа с ресурсами в PHP

Реестр предоставляет два метода получения ресурсов:

// Выбрасывает Nette\Assets\AssetNotFoundException, если файла нет
$logo = $assets->getAsset('logo.png');

// Возвращает null, если файла нет
$banner = $assets->tryGetAsset('banner.jpg');
if ($banner) {
	echo $banner->url;
}

Указание мапперов

Вы можете явно выбрать, какой маппер использовать:

// Использовать маппер по умолчанию
$file = $assets->getAsset('document.pdf');

// Использовать конкретный маппер через приставку
$image = $assets->getAsset('images:photo.jpg');

// Использовать конкретный маппер через запись массивом
$script = $assets->getAsset(['scripts', 'app.js']);

Свойства и типы ресурсов

Каждый тип ресурса даёт подходящие свойства только для чтения:

// Свойства изображения
$image = $assets->getAsset('photo.jpg');
echo $image->width;     // 1920
echo $image->height;    // 1080
echo $image->mimeType;  // 'image/jpeg'

// Свойства скрипта
$script = $assets->getAsset('app.js');
echo $script->type;     // null ('module' для точек входа Vite)

// Свойства аудио
$audio = $assets->getAsset('song.mp3');
echo $audio->duration;  // длительность в секундах

// Все ресурсы можно привести к строке (вернётся URL)
$url = (string) $assets->getAsset('document.pdf');

Свойства вроде размеров или длительности загружаются лениво, только при обращении, благодаря чему библиотека остаётся быстрой.

Для точного статического анализа установите расширение nette/phpstan-rules. PHPStan тогда знает конкретный тип каждого ресурса, так что getAsset('photo.jpg') понимается как ImageAsset, а обращение к ->width не вызывает ошибки.

Использование ресурсов в шаблонах Latte

Nette Assets предлагает интуитивную интеграцию с Latte через теги и функции.

{asset}

Тег {asset} отрисовывает целые HTML-элементы:

{* Отрисует: <img src="/assets/hero.jpg?v=123" width="1920" height="1080"> *}
{asset 'hero.jpg'}

{* Отрисует: <script src="/assets/app.js?v=456"></script> *}
{asset 'app.js'}

{* Отрисует: <link rel="stylesheet" href="/assets/style.css?v=789"> *}
{asset 'style.css'}

Тег автоматически:

  • определяет тип ресурса и порождает подходящий HTML
  • добавляет версионирование для сброса кеша
  • добавляет размеры для изображений
  • задаёт правильные атрибуты (type, media и т. д.)

Внутри HTML-атрибутов и внутри элементов <style> и <script> он выводит только URL:

<div style="background-image: url({asset 'bg.jpg'})">
<img srcset="{asset 'logo@2x.png'} 2x">

n:asset

Для полного контроля над HTML-атрибутами:

{* Атрибут n:asset заполняет src, размеры и т. д. *}
<img n:asset="product.jpg" alt="Product" class="rounded">

{* Работает с любым подходящим элементом *}
<script n:asset="analytics.js" defer></script>
<link n:asset="print.css" media="print">
<audio n:asset="podcast.mp3" controls></audio>

Используйте переменные и мапперы:

{* Переменные работают естественно *}
<img n:asset="$product->image">

{* Указываем маппер фигурными скобками *}
<img n:asset="images:{$product->image}">

{* Указываем маппер записью массивом *}
<img n:asset="[images, $product->image]">

n:asset работает и на <a>, где заполняет href, и на <link>, где создаёт подсказку предзагрузки:

<a n:asset="hero.jpg">Скачать изображение</a>

Обратите внимание, что варианты <a> и <link> работают только для отрисовываемых ресурсов (изображение, скрипт и так далее), но никогда для GenericAsset, например PDF.

Для изображений достаточно задать только width (или только height), и второй размер вычислится автоматически с сохранением соотношения сторон:

{* height дополняется из соотношения сторон *}
<img n:asset="product.jpg" width="200">

asset()

Для максимальной гибкости используйте функцию asset():

{var $logo = asset('logo.png')}
<img src={$logo} width={$logo->width} height={$logo->height}>

{* Или напрямую *}
<img src={asset('logo.png')} alt="Logo">

Необязательные ресурсы

Изящно справляйтесь с отсутствующими ресурсами через {asset?}, n:asset? и tryAsset():

{* Необязательный тег: если ресурса нет, не отрисуется ничего *}
{asset? 'optional-banner.jpg'}

{* Необязательный атрибут: пропускается, если ресурса нет *}
<img n:asset?="user-avatar.jpg" alt="Avatar" class="avatar">

{* С запасным вариантом *}
{var $avatar = tryAsset('user-avatar.jpg') ?? asset('default-avatar.jpg')}
<img n:asset=$avatar alt="Avatar">

{preload}

Улучшайте скорость загрузки страницы:

{* В секции <head> *}
{preload 'critical.css'}
{preload 'important-font.woff2'}
{preload 'hero-image.jpg'}

Порождает подходящие ссылки предзагрузки:

<link rel="preload" href="/assets/critical.css?v=123" as="style">
<link rel="preload" href="/assets/important-font.woff2?v=456" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/hero-image.jpg?v=789" as="image" type="image/jpeg">

Когда ваш ответ задаёт заголовок Content-Security-Policy с nonce, Nette автоматически добавляет соответствующий атрибут nonce каждому порождённому элементу <script>, <link> и <style>, чтобы политика безопасности браузера их не заблокировала.

Продвинутые возможности

Автоопределение расширения

Автоматически справляйтесь с несколькими форматами:

assets:
	mapping:
		images:
			path: img
			extension: [webp, jpg, png]  # Пробовать по порядку

Теперь можно запрашивать без расширения:

{* Автоматически найдёт logo.webp, logo.jpg или logo.png *}
{asset 'images:logo'}

Отлично подходит для постепенного улучшения с современными форматами.

Умное версионирование

Файлы автоматически версионируются по времени изменения:

{asset 'style.css'}
{* Вывод: <link rel="stylesheet" href="/assets/style.css?v=1699123456"> *}

Когда вы обновляете файл, метка времени меняется и вынуждает браузер обновить кеш.

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

// Отключаем версионирование для конкретного ресурса
$asset = $assets->getAsset('style.css', ['version' => false]);
{* В Latte *}
{asset 'style.css', version: false}

Тот же синтаксис ссылка, ключ: значение передаёт параметры и в n:asset, и в {preload}:

<img n:asset="photo.jpg, version: false">
{preload 'style.css', version: false}

Ресурсы-шрифты

Шрифты получают особое обращение с правильным CORS:

{* Правильная предзагрузка с crossorigin *}
{preload 'fonts:OpenSans-Regular.woff2'}

{* Использование в CSS *}
<style>
@font-face {
	font-family: 'Open Sans';
	src: url('{asset 'fonts:OpenSans-Regular.woff2'}') format('woff2');
	font-display: swap;
}
</style>

Собственные мапперы

Создавайте собственные мапперы для особых нужд вроде облачного хранилища или динамического порождения:

use Nette\Assets\Mapper;
use Nette\Assets\Asset;
use Nette\Assets\Helpers;

class CloudStorageMapper implements Mapper
{
	public function __construct(
		private CloudClient $client,
		private string $bucket,
	) {}

	public function getAsset(string $reference, array $options = []): Asset
	{
		if (!$this->client->exists($this->bucket, $reference)) {
			throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found");
		}

		$url = $this->client->getPublicUrl($this->bucket, $reference);
		return Helpers::createAssetFromUrl($url);
	}
}

Регистрируем в конфигурации:

assets:
	mapping:
		cloud: CloudStorageMapper(@cloudClient, 'my-bucket')

Используем как любой другой маппер:

{asset 'cloud:user-uploads/photo.jpg'}

Метод Helpers::createAssetFromUrl() автоматически создаёт правильный тип ресурса по расширению файла.

Типы ресурсов, реализующие интерфейс Nette\Assets\HtmlRenderable (изображения, скрипты, стили и так далее), может отрисовать {asset} как целый HTML-элемент. Остальные типы файлов становятся GenericAsset (например, PDF), который нельзя отрисовать как HTML-элемент, но он всё равно предоставляет URL (и другие метаданные). Попытка отрисовать такой ресурс как HTML-элемент выбрасывает Nette\InvalidArgumentException; его URL внутри атрибута использовать по-прежнему можно.

Дополнительные материалы

версия: 1.x