HTML компонента из контроллера вместе с CSS и JS
Проблема/контекст
Экшен отдаёт HTML компонента, а его стили и скрипты остаются на сервере: шаблон вызвал Extension::load() или AddHeadScript(), но в AJAX-ответ это не попало. Ручная сборка списков ассетов на фронте быстро расходится с реальными зависимостями шаблона. В ядре для этого есть Bitrix\Main\Engine\Response\Component, но контракт ответа в документации не описан.
Решение с кодом
Response\Component — final-класс, наследник HtmlContent. Его конструктор: __construct($componentName, $componentTemplate = '', array $componentParams = [], array $additionalResponseParams = [], $dataKeys = []).
<?php
declare(strict_types=1);
namespace Vendor\Catalog\Infrastructure\Controller;
use Bitrix\Main\Engine\Controller;
use Bitrix\Main\Engine\Response;
final class Slider extends Controller
{
public function viewAction(int $sectionId): Response\Component
{
return $this->renderComponentAjax(
'vendor:catalog.slider',
'.default',
['SECTION_ID' => $sectionId],
['sectionId' => $sectionId], // -> data.additionalParams
['ITEMS', 'NAV_STRING'], // срез arResult -> data.componentResult
);
}
}
Порядок работы задан в конструкторе HtmlContent: сначала ContentArea\Component::getHtml() выполняет $APPLICATION->IncludeComponent(...) внутри ob_start(), и только потом collectAssetsPathList() снимает списки с Asset::getInstance(). Поэтому в ответ попадает всё, что шаблон зарегистрировал во время рендера.
Итоговый JSON — обычная обёртка AjaxJson:
{
"status": "success",
"data": {
"html": "...",
"assets": {"css": [], "js": [], "string": []},
"additionalParams": {"sectionId": 12},
"componentResult": {"ITEMS": [], "NAV_STRING": "..."}
},
"errors": []
}
Ключ ассетов называется string, а не strings. componentResult есть в ответе всегда (ContentArea\Component реализует DataSectionInterface), при пустом $dataKeys в нём []. $dataKeys же управляет шестым аргументом IncludeComponent — returnResult. Массив режет arResult через array_intersect_key, callable получает весь результат компонента. В renderComponentAjax() параметр объявлен как array, поэтому для callable объект создают напрямую:
return new Response\Component(
componentName: 'vendor:catalog.slider',
componentTemplate: '.default',
componentParams: ['SECTION_ID' => $sectionId],
dataKeys: static fn(array $result): array => ['count' => count($result['ITEMS'])],
);
Признак для фронта — заголовок X-Process-Assets: assets, он ставится в конструкторе HtmlContent. Разбирает его buildAjaxPromiseToRestoreCsrf в js/main/core/core_ajax.js, то есть работает только через BX.ajax.runAction и BX.ajax.runComponentAction. Сценарий: строки вида <script type="extension/settings" сразу уходят в head, затем BX.load(css), BX.loadScript(js), затем остальные inline-строки, и только после этого промис резолвится.
BX.ajax.runAction('vendor:catalog.api.slider.view', { data: { sectionId: 12 } })
.then((response) => {
// CSS и JS уже загружены
BX.html(document.getElementById('slider'), response.data.html);
console.log(response.data.componentResult.ITEMS);
});
Уже подключённые файлы BX.load пропускает: isCssLoaded() / isScriptLoaded() сравнивают URL без ?timestamp и .min со списком из DOM и BX.setJSList() / BX.setCSSList(). При объединении CSS/JS сравнивается URL бандла. Компонент из рабочей области попадает в page_<hash>.js, а ответ приносит те же файлы как template_<hash>.js. Хэш совпадает, а URL разный, поэтому script.js шаблона выполнится повторно и должен это выдерживать. Inline-строки и теги extension/settings уходят в head при каждом запросе: дубли отсекаются только внутри одного ответа. Если дёрнуть тот же экшен голым fetch или BX.ajax, заголовок никто не обработает и ассеты придётся подключать вручную.
HtmlContent — более общий класс: он принимает любой ContentAreaInterface, то есть любой источник готового HTML, а секцию данных добавляет только тогда, когда объект дополнительно реализует DataSectionInterface. Так делают, например, в модуле sale — анонимный класс с одним методом getHtml(). Response\Component — частный случай для компонента: он собирает ContentArea\Component и передаёт его в родительский конструктор.
Итог
Response\Component возвращает HTML вместе с фактически использованными CSS, JS и inline-строками и помечает ответ заголовком X-Process-Assets. На фронте достаточно BX.ajax.runAction — ассеты подгрузятся до резолва промиса.