2. Руководство разработчика › 2.4 Шаблонизатор › Сборка статики

Витрина обычно тянет десятки мелких .js и .css: библиотеки оформления, стили, скрипты отдельных модулей. Каждый файл — отдельный запрос к серверу. Бандл склеивает их в один файл, и происходит это прямо при сохранении в среде разработки — без внешних сборщиков, конфигов и отдельной команды сборки.

Строка бандлов

Откройте .js или .css в среде разработки. Над редактором две строки: синяя — описание файла, серая — параметры. В параметрах указывается, в какие бандлы попадёт этот файл:

base.js: 4

Слева от двоеточия — имя бандла, справа — приоритет. Один файл может входить в несколько бандлов, они перечисляются через запятую:

base.js: 4, print.css: 2

Приоритет необязателен: без него файл получает 0. Имя бандла становится именем итогового файла, поэтому расширение указывается прямо в нём — base.js, melbis.css, melbis.auth.js. В имени допустимы латинские буквы, цифры, точка и подчёркивание; остальные символы вырезаются — в частности дефис, так что melbis-auth.js превратится в melbisauth.js.

Что можно собирать

В бандл попадает любой .js или .css внутри группы шаблонов — и из общего каталога statics/, и из каталогов отдельных модулей:

templates/default/
    statics/
        base/bootstrap.js               base.js: 2
        base/bootbox.js                 base.js: 4
        melbis/main.js                  melbis.js: 1
    units/
        melbis_cataloge/scripts.js      melbis.js: 10
        melbis_base_page/scripts.js     melbis.js: 15

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

Результат сборки

Собранный файл кладётся в statics/ той же группы шаблонов, с префиксом bundle.:

templates/default/statics/bundle.melbis.js

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

В начало файла дописывается отчёт о составе:

/*       Melbis Shop auto bundle report       */
/*         Create: 2026-07-17 15:39:56        */

/*   #1    main.js       32 ln    1 kb    /templates/default/statics/melbis/main.js            */
/*   #10   scripts.js    57 ln    1 kb    /templates/default/units/melbis_cataloge/scripts.js  */
/*   #15   scripts.js    77 ln    3 kb    /templates/default/units/melbis_base_page/scripts.js */

По нему видно, что и в каком порядке вошло, — это первое, куда стоит заглянуть, когда непонятно, чей код отработал раньше.

Сборка — это только склейка. Минификации, транспиляции и переписывания путей внутри CSS не происходит: файлы соединяются в порядке приоритета как есть. Если нужен минифицированный вендорный код, кладите в бандл уже минифицированную версию файла.

Порядок файлов

Файлы сортируются по возрастанию приоритета. Сами числа условные — значение имеют только их отношения, поэтому удобно оставлять зазоры, чтобы потом вставлять файлы между уже существующими.

Приоритетами удобно разделять логические группы. Например, в демонстрационном магазине до 10 идут базовые файлы из statics/, после 10 — скрипты модулей: так библиотеки заведомо загрузятся раньше кода, который на них опирается. Это не правило платформы, а просто способ навести порядок.

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

Подключение в шаблоне

<link rel="stylesheet" type="text/css" href="{PATH}/statics/bundle.base.css">
<link rel="stylesheet" type="text/css" href="{PATH}/statics/bundle.melbis.css?{BUILD}">

<script defer src="{PATH}/statics/bundle.base.js"></script>
<script defer src="{PATH}/statics/bundle.melbis.js?{BUILD}"></script>

{BUILD} — номер сборки проекта, системный тег, доступный в любом шаблоне. Он работает меткой версии: пока номер не изменился, браузер берёт файл из своего кеша, а как только номер вырос — скачивает заново.

Меняется он двумя способами: вручную — параметром MELBIS_BUILD в диалоге «Проектирование → Инсталляция» (см. «Конфигурация»), либо автоматически — кнопкой в среде разработки, которая включает инкремент MELBIS_BUILD при каждом сохранении файла.

Ставить ?{BUILD} можно у любого бандла. Вендорные библиотеки меняются редко, поэтому их часто подключают без метки, — но это дело вкуса, а не требование.

Удаление из бандла

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

Если из бандла ушёл последний файл, сам бандл остаётся на диске. Собирать его больше не из чего, поэтому движок его просто не трогает — прежний statics/bundle.имя продолжает лежать и отдаваться витрине. То же самое при переименовании бандла: появится файл с новым именем, а старый останется. Такие файлы удаляются вручную.

Удалять и переименовывать саму статику лучше в дереве среды разработки: тогда метаданные о бандлах переносятся или снимаются вместе с файлом. Если удалить файл мимо IDE — например, по FTP, — метаданные о нём останутся, и очередная сборка сообщит об ошибке Can't include file to bundle.


Переменные стилей: файлы .psv

CSS не умеет считать. Цвет наведения на кнопку — это основной цвет, затемнённый на 7.5%; фон уведомления — он же, разбавленный белым на 80%. Готовые фреймворки считают такое при своей сборке и приезжают к нам с уже посчитанными значениями: в bootstrap.css один основной цвет разложен в полтора десятка литералов и встречается в 77 местах. Перекрасить магазин означает найти их все.

Чтобы этого не делать, в бандл кладётся файл .psvphp style vars. Это обычный PHP-файл, который возвращает массив ключей; остальные файлы бандла подставляют эти ключи по имени.

base/theme.psv:

<?php
$primary = '#007bff';

return [
    'PRIMARY'       => $primary,
    'PRIMARY_HOVER' => '#0069d9'
    ];
?>

base/buttons.css:

.btn-primary { background-color: {PRIMARY}; border-color: {PRIMARY}; }
.btn-primary:hover { background-color: {PRIMARY_HOVER}; }

Файл .psv создаётся так же, как .css и .js — в дереве среды разработки, а .psv выбирается в списке расширений. Редактор подсвечивает его как PHP, и строка бандлов у него такая же:

base.css: 0

Правила

Ключи пишутся заглавными. В шаблоне подставляется всё, что подходит под {ИМЯ}: заглавные латинские буквы, цифры и подчёркивание. Именно регистр и делает запись безопасной среди фигурных скобок CSS: ни {color:red}, ни @media (min-width: 576px) { под неё не попадают.

Ключи действуют на весь бандл, а не на файл. Один .psv с малым приоритетом задаёт палитру, все файлы после него ею пользуются. Поэтому вендорный bootstrap.css остаётся общим для всех магазинов, а свой у каждого — только theme.psv.

Приоритет решает, кто победит. Файлы .psv подключаются в порядке приоритета, и одинаковый ключ, встреченный дважды, берёт значение из последнего. На этом строится переопределение: базовая тема с приоритетом 0, магазинная с приоритетом 1, и во второй перечислено только то, что отличается.

Сам .psv в собранный файл не попадает. Он не CSS — он только приносит ключи. В отчёте о составе бандла он виден, в теле — нет.

Ключ, которого нет, остаётся как написан. Сборка не прерывается, а среда разработки после сохранения показывает предупреждение:

Warning! Unknown: PRIMARY_HOWER (buttons.css);

Обычно это опечатка в имени. Если в бандле нет ни одного .psv, подстановка не запускается вовсе — бандл из чистого JS не будет ругаться на свои {...}.

Подстановка живёт только в бандле. Файл, подключённый в шаблон напрямую, минуя бандл, уедет в браузер с сырыми {PRIMARY}.

Класс PhpStyleVar

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

<?php
use Melbis\MelbisShop\PhpStyleVar as PSV;

$primary = '#007bff';

return [
    'PRIMARY'       => $primary,
    'PRIMARY_HOVER' => PSV::darken($primary, 7.5),
    'PRIMARY_BG'    => PSV::mix('#fff', $primary, 80),
    'PRIMARY_RGB'   => PSV::rgb($primary)
    ];
?>

Цвет передаётся так, как он пишется в CSS — #007bff или #fff, — и возвращается так же.

Метод Что делает
darken($hex, $percent) темнее на столько-то процентов
lighten($hex, $percent) светлее
mix($front, $back, $percent) столько-то процентов первого цвета поверх второго
saturate($hex, $percent) насыщеннее
desaturate($hex, $percent) ближе к серому
hue($hex, $degrees) поворот тона по цветовому кругу
yiq($hex, $dark, $light) который из двух текстов читается на этом фоне
rgb($hex) 0, 123, 255 — числа для rgba()
rgba($hex, $alpha) целиком rgba(0, 123, 255, 0.25)
url($hex) %23007bff — цвет внутри картинки, нарисованной в самом CSS
rem($pixels) пиксели в rem
stack(['Inter', 'Segoe UI']) список шрифтов; кавычит только имена с пробелом
svg($markup) разметку картинки в готовый url("data:image/svg+xml,...")

Считает класс так же, как это делает SASS, — вплоть до округления половины, поэтому фреймворк, собранный где-то там, сохраняет здесь свои собственные оттенки.

Три метода нужны там, где цвет попадает в CSS не сам по себе:

.shadow { box-shadow: 0 0 0 0.2rem rgba({PRIMARY_RGB}, 0.25); }
.select { background-image: url("data:image/svg+xml,%3csvg%3e%3cpath fill='{DARK_URL}'/%3e%3c/svg%3e"); }

Ключи циклом

Массив собирается обычным PHP, поэтому однотипные оттенки удобнее не перечислять, а посчитать:

$base = [
    'PRIMARY' => '#007bff',
    'SUCCESS' => '#28a745',
    'DANGER'  => '#dc3545'
    ];

$vars = $base;
foreach ( $base as $name => $color )
{
    $vars[$name.'_HOVER'] = PSV::darken($color, 7.5);
    $vars[$name.'_BG'] = PSV::mix('#fff', $color, 80);
    $vars[$name.'_RGB'] = PSV::rgb($color);
}

return $vars;

Так список цветов вверху файла остаётся единственным местом, куда потом заглядывают, а число ключей роли не играет: лишние никому не мешают, а нужный всегда оказывается на месте.

Когда что-то не так

Файл обязан вернуть массив. Если он вернёт что-то другое, сборка остановится:

second.psv gives no array of keys back

Ошибка в самом PHP — опечатка, несуществующий метод — тоже останавливает сборку и называет настоящий файл и настоящую строку:

File: .../templates/default/statics/base/theme.psv : 105
Uncaught Error: Call to undefined method Melbis\MelbisShop\PhpStyleVar::url()

В обоих случаях прежний собранный бандл остаётся нетронутым: витрина продолжает отдавать последнюю удачную сборку, пока файл не поправят.


Преобразователь файлов

Бандл только склеивает. Если код нужно ещё и минифицировать, обфусцировать или как-то иначе преобразовать, для этого есть отдельный механизм — преобразователь файлов, Ctrl+B или кнопка на панели редактирования.

Работает он так: среда отправляет на сервер имя файла и текущее содержимое редактора — вместе с несохранёнными правками, — сервер вызывает по HTTP ваш скрипт-преобразователь, а то, что тот вернёт, целиком заменяет текст в редакторе. Сам результат никуда не сохраняется: посмотрите, что получилось, и сохраните файл обычным способом.

Настройка

Адрес скрипта задаётся в «Настройки редактора → Оптимизация → Преобразователь файлов»:

Это настройка среды разработки, а не проекта: она не лежит в config.json, и каждый разработчик задаёт её у себя.

Скрипт вызывает сервер, а не программа, — как и задачи планировщика. Поэтому в адресе стоит localhost: сервер обращается сам к себе, и преобразователь работает сразу, без настройки домена. Если вынести скрипт на отдельный хост, адрес должен быть доступен с веб-сервера магазина, а не с рабочей станции.

Что получает скрипт

Обычный POST с двумя полями:

Поле Что в нём
name имя файла — по расширению удобно решать, что с ним делать
content текущее содержимое редактора

Вернуть нужно готовый текст в теле ответа — без обёрток, заголовков и JSON. Ответ с кодом 400 и выше среда покажет как ошибку, а содержимое редактора не тронет.

Пример: minify.php

В демонстрационном магазине лежит готовый преобразователь — минификатор JS и CSS. Это обычный PHP-файл в корне проекта, движок в нём не участвует.

Начинается он с проверки, что запрос пришёл с самого сервера:

// Преобразователь вызывает движок, поэтому пускаем только локальные запросы
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
if ( $ip !== '127.0.0.1' && $ip !== '::1' )
{
    header('HTTP/1.1 403 Forbidden');
    die('Only local allowed');
}

Дальше — собственно преобразование:

switch ( pathinfo($_POST['name'], PATHINFO_EXTENSION) )
{
    case 'js':
        $data = array('input' => $_POST['content']);
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, 'https://www.toptal.com/developers/javascript-minifier/api/raw');
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 60);
        echo curl_exec($ch);
        curl_close($ch);
        break;

    case 'css':
        // То же самое, но обращение к минификатору CSS
        break;

    default:
        echo $_POST['content'];
}

Разбор по расширению — не требование платформы, а удобное соглашение: один адрес обслуживает все типы файлов. Ветка default возвращает содержимое как есть, поэтому Ctrl+B на файле неизвестного типа ничего не испортит; в собственном преобразователе это правило стоит сохранить.

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

Проверку на локальный вызов не выбрасывайте. Преобразователь лежит в корне сайта, принимает произвольный текст и выполняется на вашем сервере — без неё запустить его сможет кто угодно из интернета. Тот же приём используют cron-модули, там для этого есть готовый метод CronLocalOnly() (см. «Планировщик задач»).