Клиентский код в шаблоне - инициализация, делегирование, дубли
Пишем скрипт шаблона компонента так, чтобы он не удваивал обработчики, не терял контекст и не тормозил страницу со списком товаров.
Что нужно знать заранее
Шаблон компонента выводится на одной странице сколько угодно раз подряд. Два одинаковых блока на странице - обычное дело, и код шаблона обязан это переживать без удвоения обработчиков.
Обработчик на каждый элемент списка стоит дорого и заметен на длинных списках. Один обработчик на общий контейнер решает ту же задачу и продолжает работать после подгрузки новых элементов.
Данные с сервера отдают отдельным объектом данных, а не готовой разметкой. Разбор атрибутов и текста узлов превращает шаблон в хрупкую конструкцию, которая ломается от любой правки вёрстки.
Шаги
- Обернуть объявление класса проверкой: он мог быть уже объявлен предыдущим таким блоком.
- Привязывать обработчики к корневому узлу конкретного блока, а не ко всему документу.
- Передавать данные компонента одним объектом данных рядом с разметкой самого этого блока.
- Использовать делегирование событий для повторяющихся элементов внутри любого длинного списка.
- Проверить страницу с двумя блоками того же компонента и с подгрузкой списка.
Решение
Объявляем класс один раз:
if (typeof window.VendorGallery === 'undefined') { window.VendorGallery = class { // повторный вывод шаблона не переобъявит constructor(node, params) { this.node = node; this.params = params; } };}Повторное объявление класса перетирает предыдущее и рвёт уже созданные объекты. Проверка перед объявлением стоит одну строку и снимает целый класс плавающих ошибок на страницах с двумя блоками.
Передаём данные компонента объектом:
<div class="vendor-gallery" data-params='<?= \Bitrix\Main\Web\Json::encode([ "iblockId" => $arParams["IBLOCK_ID"], "pageSize" => 12,]) ?>'>Создаём по объекту на каждый блок:
document.querySelectorAll('.vendor-gallery').forEach((node) => { new window.VendorGallery(node, JSON.parse(node.dataset.params)); // свой объект на блок});Каждый блок получает свой объект и свои параметры. Так два одинаковых компонента на странице не мешают друг другу, а данные не собираются разбором вёрстки.
Делегируем обработчики внутри блока:
this.node.addEventListener('click', (event) => { const button = event.target.closest('.js-add-to-basket'); if (!button) { return; } // клик мимо кнопки игнорируем this.addToBasket(button.dataset.productId);});// один обработчик работает и для элементов, подгруженных позжеДелегирование избавляет от повторной привязки после подгрузки. Список дополнился новыми карточками - обработчик уже на месте, потому что он висит на общем контейнере блока.
Не теряем контекст в колбэках:
const self = this;BX.ajax.runComponentAction('vendor:gallery', 'load', { mode: 'class' }) .then((response) => self.render(response.data)) // стрелочная функция тоже подходит .catch((response) => self.showError(response.errors)); // ошибки приходят отдельным полемБережём браузер на длинных списках:
const fragment = document.createDocumentFragment();items.forEach((item) => fragment.append(this.renderItem(item)));this.node.querySelector('.list').append(fragment); // одна вставка вместо сотни// перерисовка страницы происходит один раз, а не на каждый элемент спискаОдна вставка вместо цикла заметна на списках от сотни элементов. Там же полезно убрать лишние таймеры: страница с несколькими бесконечными интервалами греет процессор без всякой пользы.
Типичные проблемы
Обработчик срабатывает дважды на одно нажатие.
Шаблон выведен на одной странице дважды и привязал свои обработчики повторно. Обработчики вешают на корневой узел этого блока, а класс объявляют с проверкой.
После подгрузки списка кнопки перестают работать.
Обработчики привязаны к элементам, которых на момент их подгрузки ещё не было. Делегирование событий на общий контейнер решает это без всякой повторной привязки.
В обработчике теряется объект компонента.
Контекст вызова не сохранён при передаче метода объекта в чужой колбэк. Используют стрелочную функцию либо явную привязку контекста к нужному объекту.
Скрипт ломается после правки вёрстки.
Данные собираются разбором текста и классов уже готовой разметки самого блока. Параметры передают отдельным объектом данных рядом с самим блоком его разметки.
Страница с большим списком заметно тормозит.
Элементы вставляются в цикле по одному и каждый раз вызывают перерисовку. Готовят весь набор элементов целиком и вставляют его одной общей операцией вставки.
Частые вопросы
Где размещать скрипт шаблона компонента?
В отдельном файле шаблона, подключённом штатными средствами. Скрипт прямо в разметке дублируется при повторном выводе и не кэшируется браузером.
Как передать в скрипт большой массив данных?
Объектом в данных узла или отдельным запросом действия компонента. Огромный объект в разметке раздувает страницу и замедляет её разбор.
Нужно ли ждать готовности документа?
Если скрипт подключён после разметки блока, ждать нечего. При подключении в шапке инициализацию откладывают до готовности документа.
Можно ли обойтись без класса?
Для одного простого блока - да, хватит функции инициализации. Класс окупается там, где у блока есть состояние и несколько действий.
Что делать с двумя одинаковыми блоками на странице?
Создавать по объекту на каждый корневой узел и хранить состояние в нём. Общие переменные на весь скрипт при этом заводить нельзя.
Смежное
- Свои расширения JS - оглавление подтемы
- Cannot read properties of undefined: разбор причин на витрине - разбор падения такого кода
- Своё расширение JS: каталог, зависимости, подключение - куда выносят общий клиентский код
- Сборка фронтенда рядом с шаблоном: исходники, бандл, приложение - сборка исходников шаблона
- AJAX-действия своего компонента: действия, параметры, ошибки - запросы из этого кода
- Кнопки, меню и диалоги ядра UI - готовые элементы интерфейса
- Шаблон чужого компонента: копия вместо правки исходника - где живёт этот шаблон
- Ядро BX и AJAX - устройство клиентского ядра