Bitrix D7 Page\Asset: CSS, JS, порядок подключения и типичные ошибки

В шаблонах и компонентах Битрикс до сих пор часто пишут $APPLICATION->SetAdditionalCSS / AddHeadScript «где придётся»: в header.php, в result_modifier, в ajax-ответе. Потом стили дублируются, скрипт уезжает в середину HTML, а на AJAX-перерисовке компонент теряет CSS. Нормальный контракт ядра — Bitrix\Main\Page\Asset.

Рядом по теме: проверка уже подключённого CSS, AJAX в компонентах, Application / Context, Loader / ModuleManager. Ниже — addCss/addJs/addString, порядок вывода, уникальность путей и типичные ошибки на проде.

Зачем Asset, а не SetAdditionalCSS напрямую

$APPLICATION по-прежнему работает и внутри вызывает тот же менеджер ресурсов. Но прямой доступ к Asset::getInstance() удобнее в модулях, сервисах и коде без глобального $APPLICATION:

  • единая точка регистрации CSS/JS/строк в <head> и перед </body>;
  • дедупликация по пути файла — повторный addCss того же URL не плодит второй <link>;
  • явный режим вывода (шапка / тело / отложенно), без «вставил echo в template.php и получил FOUC»;
  • проще тестировать и вызывать из D7-сервисов после ServiceLocator.

Правило простое: новый код модуля и local/ — через Bitrix\Main\Page\Asset. Старые вызовы $APPLICATION->SetAdditionalCSS оставляйте, пока не трогаете файл; при правке — переводите на Asset.

Базовое API: addCss, addJs, addString

<?php
use Bitrix\Main\Page\Asset;

$asset = Asset::getInstance();

// CSS относительно корня сайта
$asset->addCss('/local/css/catalog.filters.css');

// JS
$asset->addJs('/local/js/catalog.filters.js');

// Произвольная строка в head (meta, inline-критика, preconnect)
$asset->addString(
    '<link rel="preconnect" href="https://cdn.example.com" crossorigin>',
    true // unique: не дублировать одинаковую строку
);

Пути лучше начинать с / от document root. Относительные «как в шаблоне» ломаются на ЧПУ и во вложенных разделах. Для файлов модуля удобно собирать URL через Loader / путь модуля, а не хардкодить /bitrix/modules/... в публичку.

Где вызывать: компонент, init.php, epilog

Типичные места регистрации:

  1. В компоненте (component.php или class.php) — если ресурс нужен только этому компоненту. Регистрируйте до вывода шаблона, не внутри цикла по элементам.
  2. В template.php — допустимо для уникальных стилей шаблона; избегайте вызова на каждый элемент списка.
  3. В local/php_interface/init.php или обработчике OnEpilog/OnProlog — для глобальных библиотек сайта.
  4. В ajax-ветке — если ответ HTML-фрагмент, CSS/JS всё равно нужно зарегистрировать так, чтобы ядро/BX подтянули их в ответ (или отдать критичные стили инлайном осознанно).
<?php
// local/components/vendor/widget/class.php
use Bitrix\Main\Loader;
use Bitrix\Main\Page\Asset;

class WidgetComponent extends CBitrixComponent
{
    public function executeComponent()
    {
        if ($this->startResultCache()) {
            // ... данные ...
            $this->includeComponentTemplate();
        }

        // После кеша шаблона ресурсы всё равно нужны на каждом хите
        $this->registerAssets();
    }

    protected function registerAssets(): void
    {
        $asset = Asset::getInstance();
        $asset->addCss($this->getPath() . '/templates/.default/style.css');
        $asset->addJs($this->getPath() . '/templates/.default/script.js');
    }
}

Важно: если регистрацию спрятать только внутрь startResultCache() без повторного вызова снаружи, при отдаче из кеша компонента CSS/JS могут не попасть на страницу. Либо регистрируйте ресурсы вне кеша, либо используйте $this->SetResultCacheKeys / стандартные style.css/script.js шаблона компонента (ядро подключает их само).

Дедупликация и проверка «уже подключено»

addCss/addJs сами не плодят второй тег для того же пути. Отдельная ручная проверка нужна реже, чем кажется. Если всё же надо узнать список (отладка, условная логика, миграция со старого кода) — смотрите массив через $APPLICATION->GetCssList() / исторический GetCSSArray() и разбор в статье «Проверить, подключён ли стиль».

<?php
use Bitrix\Main\Page\Asset;
use Bitrix\Main\Page\AssetLocation;

$asset = Asset::getInstance();
$path = '/local/css/shared.ui.css';

// Достаточно одного вызова из разных компонентов:
$asset->addCss($path);
$asset->addCss($path); // второй раз — без второго <link>

// Строки дедуплицируйте флагом unique
$asset->addString('<meta name="viewport" content="width=device-width, initial-scale=1">', true);

Грабля: один и тот же файл под разными URL (/local/css/a.css и /local/css/a.css?v=2, или с доменом и без) считается разным ресурсом. Нормализуйте путь и версию централизованно (query ?v= из конфига релиза).

Порядок и место вывода

По умолчанию CSS уходит в head, JS — в зависимости от настроек сайта/композита и режима Asset. Для строк можно указать локацию:

<?php
use Bitrix\Main\Page\Asset;
use Bitrix\Main\Page\AssetLocation;

$asset = Asset::getInstance();

// В <head>
$asset->addString(
    '<style>.hero{min-height:40vh}</style>',
    true,
    AssetLocation::BEFORE_CSS
);

// Перед закрытием body (если поддерживается режимом)
$asset->addString(
    '<script>window.__CATALOG_BOOTSTRAP__=1</script>',
    true,
    AssetLocation::AFTER_JS
);

Не вставляйте большие бандлы инлайном через addString — это убивает кеш браузера. Инлайн оставляйте для критичного CSS «above the fold» или крошечных конфигов (window.__CONFIG__). Тяжёлые библиотеки — отдельными файлами с долгим Cache-Control.

Связка с шаблоном компонента и Extension

Если в каталоге шаблона лежат style.css и script.js, ядро подключит их автоматически при показе компонента — отдельный addCss не обязателен. Asset нужен для:

  • общих библиотек из /local/css, CDN, npm-сборки;
  • условного подключения (только на детальной, только для авторизованных);
  • ресурсов, которые регистрирует сервис/модуль вне шаблона.

Для JS-расширений ядра (ui.vue, main.core, кастомных extension.php) предпочтителен Bitrix\Main\UI\Extension::load('vendor.widget'): зависимости, порядок и локализация подтянутся сами. Asset при этом остаётся правильным слоем для «простых» файлов сайта.

<?php
use Bitrix\Main\UI\Extension;
use Bitrix\Main\Page\Asset;

// Ядровое/модульное расширение со своими deps
Extension::load(['ui.buttons', 'vendor.widget']);

// Плоский файл витрины
Asset::getInstance()->addJs('/local/js/landing.hero.js');

AJAX и композит

При компонентном AJAX (AJAX_MODE=Y или ручной HTML-фрагмент) браузер уже загрузил страницу. Новые CSS/JS из фрагмента ядро старается догрузить через BX, если ресурсы зарегистрированы штатно. Если вы отдаёте «голый» JSON и сами рисуете DOM — CSS нужно либо подключить заранее на полной странице, либо грузить скриптом явно.

Практичный подход: все стили виджета регистрируйте на полной отрисовке листинга; в ajax-ответе меняйте только разметку. Подробнее про ajaxMode и JSON-ответы — в AJAX в компонентах и Web\Json.

С композитом не кешируйте HTML, в который через addString попали персональные данные или одноразовые токены. Статические CSS/JS с постоянным URL — безопасны; инлайн-конфиг пользователя — нет.

Типичные ошибки

  • Регистрация внутри кеша компонента без повтора снаружи. После хита из кеша страница «лысая».
  • Относительные пути (css/style.css без ведущего /) — ломаются на вложенных URL.
  • Echo <script> в template.php вместо Asset — порядок, CSP, дедуп и композит страдают.
  • Два URL на один файл (с query и без, http/https, с доменом) — двойная загрузка.
  • Подключение тяжёлого бандла на всех страницах из init.php — PageSpeed падает на корзине и 404.
  • addString с огромным CSS — нет кеша файла в браузере.
  • Забыли charset/минификацию на CDN и подключили «dev»-сборку на прод.
  • Смешение Extension::load и ручного addJs той же библиотеки — двойной Vue/ядро, конфликты глобалов.

Мини-чеклист внедрения

  1. В новом коде — только Asset::getInstance() (или авто-файлы шаблона / Extension).
  2. Пути абсолютные от корня сайта, версия — один query из релиза.
  3. Ресурсы компонента регистрируйте так, чтобы они попадали и при отдаче из кеша.
  4. Глобальные библиотеки — по условию раздела/шаблона, не «на весь сайт всегда».
  5. Для модульного JS с зависимостями — Extension::load, не копипаста в /local/js.
  6. В ajax-сценариях заранее грузите CSS на полной странице; JSON — через Web\Json.

Быстрая отладка

<?php
use Bitrix\Main\Page\Asset;

// Временно на стейдже: посмотреть, что накопил Asset к концу хита
AddMessage2Log([
    'css' => Asset::getInstance()->getCss(),
    'js'  => Asset::getInstance()->getJs(),
], 'local.asset');

// В браузере: DevTools → Network → фильтр CSS/JS,
// поиск дублей одного файла с разными query.

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

Вывод

Bitrix\Main\Page\Asset — правильный слой для CSS/JS в модулях и local/: дедуп путей, предсказуемое место в HTML, работа без глобального $APPLICATION. Старые SetAdditionalCSS/AddHeadScript не запрещены, но для новой разработки лучше сразу Asset + авто-файлы шаблона + Extension::load для ядровых пакетов. Проверку «уже есть в списке» оставляйте для отладки и легаси; для обычного подключения хватает повторного addCss с тем же путём. Дальше по стеку фронта компонента — AJAX без костылей, сериализация ответов — Json, HTTP-контекст — Application/Context.