04.08.2026 10 мин чтения

Как правильно подключать статику в Bitrix: версионирование, Asset API и ES-модули

Кирилл Новожилов

Кирилл Новожилов

Автор

Как правильно подключать статику в Bitrix: версионирование, Asset API и ES-модули
Введение

Vite собрал main.css и main.js без хеша в имени. В header.php кто‑то написал filemtime / md5_file, приклеил ?ver= и вывел тег через addString. Сайт работает. Пока не переезжаете на другой хостинг, не включаете долгое кеширование статики и не пытаетесь подключить ES module «как обычный» addJs.

Ниже — как в main 26.x устроен Bitrix\Main\Page\Asset, зачем нужен getFullAssetPath, почему type="module" ядро само не нарисует, и когда вместо «голых» файлов правильнее Extension::load.

Сюжет: работает, но это обходной путь

Типичный кусок после фронтенд‑сборки со стабильными именами файлов:

        // local/templates/main/header.php — антипаттерн
$css = '/local/assets/css/main.css';
$ver = file_exists($_SERVER['DOCUMENT_ROOT'] . $css)
    ? (string) filemtime($_SERVER['DOCUMENT_ROOT'] . $css)
    : (string) time();

$asset = \Bitrix\Main\Page\Asset::getInstance();
$asset->addString('<link rel="stylesheet" href="' . $css . '?ver=' . $ver . '">');

    
Зачем так делают
Сборщик не кладёт хеш содержимого в имя файла, а «чтобы сбросить кеш браузера» руками дописывают параметр в URL. Иногда ещё md5_file. Иногда обходят addCss, потому что «на BeGet файлы пропадали из head».

Проблема не в том, что это «некрасиво». Проблема в том, что вы дублируете механизм, который ядро уже вызывает при выводе ассетов — и при этом теряете единый контракт с ShowHead, оптимизацией CSS/JS и минифицированными .min.* файлами.

Что ядро делает вместо вашего ?ver=

Цепочка короткая.

  1. Вы вызываете Asset::getInstance()->addCss('/local/assets/css/main.css') (или старый CMain::SetAdditionalCSS).
  2. При ShowHead / ShowCSS ядро готовит список и для локальных файлов зовёт внутренний getAssetPaths().
  3. Тот смотрит файл на диске, опционально предпочитает .min.css / .min.js, берёт filemtime и отдаёт URL через CUtil::GetAdditionalFileURL.

Упрощённо из ядра:

        // main/lib/page/Asset.php — getAssetPaths()
$result = [
    'PATH' => $path,
    'FILE_PATH' => $filePath,
    'FULL_PATH' => \CUtil::GetAdditionalFileURL($path, true),
];

// main/classes/general/util.php
return $file . '?' . filemtime($filePath) . filesize($filePath);

    
⚠️ Важно
В параметр URL попадает не «магический хеш», а склейка filemtime и filesize. Для сброса кеша этого достаточно: изменили файл — изменился mtime и/или размер — браузер запросит заново. Отдельный md5_file на каждый запрос не нужен.

Отсюда правило: для обычного <link> / классического <script src> ручной ?ver= лишний. Версионирование — у Bitrix.

Матрица: чем подключать что

Что подключаете API Версия Когда так
CSS шаблона сайта Asset::addCss($path) ядро допишет ?mtime+size в ShowHead почти всегда
Обычный JS (не module) Asset::addJs($path) то же скрипты без type="module"
ES module / кастомный тег getFullAssetPath + addString та же формула, но URL в руки нужен type="module", defer, crossorigin, …
Расширение ядра / модуля (main.core, свой /local/js/...) Extension::load('vendor.module.name') через config.php + CJSCore админка, компоненты на BX.*, переиспользуемые бандлы
Ассеты шаблона компонента $this->addExternalCss / addExternalJs тот же Asset локальные стили/скрипты шаблона

Старые обёртки SetAdditionalCSS / AddHeadScript / AddHeadString живы, но внутри они зовут тот же Asset. В новом коде лучше сразу D7‑API.

Канон для шаблона сайта

        use Bitrix\Main\Page\Asset;

$asset = Asset::getInstance();

$asset->addCss(SITE_TEMPLATE_PATH . '/assets/css/main.css');
$asset->addJs(SITE_TEMPLATE_PATH . '/assets/js/legacy-widget.js');

    

В шаблоне сайта должны быть ShowHead() (или раздельно CSS / strings / scripts) — иначе вы добавили файлы в очередь, а в HTML их никто не вывел.

Второй аргумент additional:

        $asset->addCss('/local/assets/css/critical.css');           // обычная очередь
$asset->addCss('/local/assets/css/late.css', true);         // additional — ближе к «хвосту» шаблона

    

Не передавайте в addCss/addJs уже готовый path?ver=123: Asset::getAssetPath() отрежет параметры URL до ?, а свою версию ядро соберёт позже само.

ES modules: дыра в addJs и зачем getFullAssetPath

В исходниках main нет генерации type="module". addJs всегда рисует обычный script. Если фронт отдаёт ES‑модуль, «правильный» путь такой:

        use Bitrix\Main\Page\Asset;
use Bitrix\Main\Page\AssetLocation;

$asset = Asset::getInstance();
$path = $asset->getFullAssetPath('/local/assets/js/main.js');

if ($path !== null)
{
    $asset->addString(
        str: '<script type="module" src="' . htmlspecialcharsbx($path) . '"></script>',
        unique: true, // не размножать тег
        location: AssetLocation::AFTER_JS,
    );
}

    

getFullAssetPath()не отдельная магия версионирования. Это публичная обёртка над тем же getAssetPaths()GetAdditionalFileURL, которым пользуются addCss/addJs при выводе.

Метод Что даёт
addCss / addJs сам вставит тег с ?mtime+size
getFullAssetPath($path) ту же строку URL (или null, если файла нет)
Asset::getAssetTime($url) вытащит параметр из уже готового URL ('…?12345''12345'); файл на диске не смотрит

Итого для modules: getFullAssetPath — нормальный способ получить ту же Bitrix‑версию, без самописного ?ver= и без md5_file.

Когда нужен Extension::load, а не addJs

Asset — про «положить файл в head страницы».
Extension — про именованный бандл с зависимостями, который знает ядро и другие расширения.

        /local/js/bxmax.site/hero/
├── src/
├── dist/
├── bundle.config.js
└── config.php

    
        // config.php
return [
    'js' => './dist/hero.bundle.js',
    'css' => './dist/hero.bundle.css',
    'rel' => ['main.core', 'main.loader'],
    'skip_core' => false,
];

    
        \Bitrix\Main\UI\Extension::load('bxmax.site.hero');

    

На JS:

        BX.Runtime.loadExtension('bxmax.site.hero').then(() => {
    // BX.Bxmax.Site.Hero доступен
});

    

Берите extensions, когда:

  • код живёт рядом с модулем и переиспользуется;
  • нужны rel / порядок относительно main.core;
  • это админка, слайдеры UI, общие виджеты на BX.*;
  • бандл собираете через @bitrix/chef (npx bitrix build).

Не тащите через Extension весь публичный Vite‑бандл лендинга «потому что модно». Для витрины сайта чаще достаточно addCss + (при необходимости) module через getFullAssetPath. Для куска, который говорит на языке ядра — Extension::load.

Прямой <script src="/local/js/..."> в обход Extension::load для зарегистрированных расширений — антипаттерн: сломаете rel и порядок инициализации.

Долгое кеширование и версия в URL — союзники, не враги

Версия в параметре URL решает «когда перезапросить». Заголовки кеша решают «как долго можно не спрашивать».

Имеет смысл на /local/assets/ (или вашем каталоге сборки) выставить длинный Cache-Control / Expires на веб‑сервере: пока ?mtime+size тот же — браузер и CDN спокойно кешируют; файл обновили — параметр сменился — это уже новый URL.

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

Выводы

Статика в Bitrix — это не «куда воткнуть <link>», а выбор API под тип файла. Обычные CSS/JS отдайте Asset. ES modules — через getFullAssetPath. Код экосистемы ядра — через Extension. Ручной сброс кеша оставляйте хостинговым костылям, а не шаблону по умолчанию.

Опубликовано 5 дней назад

Комментарии (0)

Пожалуйста, войдите в аккаунт, чтобы оставить комментарий

Оставить комментарий

Похожие статьи

Мы используем файлы cookie для улучшения работы сайта. Продолжая использовать сайт, вы соглашаетесь с нашей политикой конфиденциальности.
AI Домовой

AI Домовой История

на связи

пишет…
Нет истории чатов
AI Домовой

Нужна авторизация

Войдите, чтобы задавать вопросы AI Домовому.

Войти