Стандартизация Swiper и макетов в Битриксе: уходим от хаоса в inline-скриптах
Последние записи
Хостинг для сайта
Если выбираете хостинг под проект — советую Timeweb. Современная инфраструктура, широкий выбор тарифов от стартовых до VPS под нагрузку, отзывчивая техподдержка, которая отвечает по существу, а не отписками. Я сам держу на нём часть своих проектов и рекомендую клиентам.
Перейти Партнёрская ссылка. Для вас цена не меняется.Проблема: inline-скрипты и иллюзия контроля
В большинстве проектов на 1С-Битрикс инициализация Swiper выглядит одинаково. В каждом template.php компонента — свой inline-скрипт:
new Swiper('.card', {
slidesPerView: 3,
spaceBetween: 20,
pagination: { el: '.swiper-pagination' },
});
Это работает ровно до первого AJAX-запроса. Пагинация, фильтр, обновление корзины — и новые слайды появляются в DOM, но Swiper о них не знает. Типичные «лечения»:
- обернуть инициализацию в
setTimeout(500)— угадывать момент появления DOM; - вызвать
swiper.update()— не помогает, если слайдер вообще не создан; - продублировать inline-скрипт в каждый компонент, где есть AJAX — копипаст растёт вместе с проектом.
Корневая причина — не в Swiper, а в архитектуре. Инициализация привязана к моменту вывода HTML, а не к моменту появления элемента в DOM. Пока эти два момента совпадают — всё работает. Как только Битрикс начинает подгружать блоки асинхронно, совпадение ломается.
Второй источник проблем — конфигурация внутри скрипта. Настройки слайдера размазаны по десяткам файлов: где-то loop: true, где-то speed: 0, где-то передан неверный селектор пагинации. Понять, какой слайдер на странице как настроен, можно только поиском по коду.
Архитектура: три уровня конфигурации
Вместо разрозненных inline-скриптов предлагается единая схема из трёх уровней. Каждый следующий переопределяет предыдущий:
| Уровень | Где задаётся | Что определяет | Пример |
|---|---|---|---|
| Глобальный | Объект swiperConfig в основном скрипте | Базовые настройки для всех слайдеров | speed: 300, slidesPerView: 1 |
| Компонентный | Атрибут data-options в HTML | Переопределения для конкретного слайдера | loop: false, effect: 'slide' |
| Императивный | JS-код в component_epilog.php | Настройки для сложных случаев | Синхронизация thumbs, кастомные события |
Такая схема — стандарт для production-решений. В библиотеке swiper-attribute-system для Webflow используется тот же принцип: разметка описывает структуру, атрибуты — конфигурацию, JS вмешивается только там, где атрибутов не хватает.
Почему data-options, а не набор data-*
Два подхода к атрибутной конфигурации:
- Плоские атрибуты:
data-slides-per-view="3",data-space-between="20",data-loop="true". - Один JSON:
<div data-options='{"slidesPerView": 3}'>.
Для Битрикса выигрывает JSON. Причина — в природе template.php: настройки рождаются как PHP-массив и передаются в json_encode() одной строкой. Плоские атрибуты потребовали бы ручного маппинга каждого ключа. Плюс вложенные структуры — breakpoints, pagination.el — через data-* выразить невозможно.
Класс SwiperManager
Инициализацию, переинициализацию и парсинг опций логично собрать в один класс. Это даёт пространство имён, единую точку входа и удобство для внешних вызовов из component_epilog.php.
class SwiperManager {
constructor(config = {}) {
this.config = Object.assign({
slidesPerView: 1,
spaceBetween: 0,
speed: 300,
watchOverflow: true,
observer: true,
observeParents: true,
}, config);
this.instances = new WeakMap();
this.observer = null;
this._onReady = this._onReady.bind(this);
this._onAjaxSuccess = this._onAjaxSuccess.bind(this);
this._onMutation = this._onMutation.bind(this);
this._bindEvents();
}
/**
* Навешивает обработчики: DOMContentLoaded, Bitrix AJAX, MutationObserver.
* @returns {void}
*/
_bindEvents() {
document.addEventListener('DOMContentLoaded', this._onReady);
if (typeof BX !== 'undefined') {
BX.ready(() => {
BX.addCustomEvent('onAjaxSuccess', this._onAjaxSuccess);
});
}
this.observer = new MutationObserver(this._onMutation);
this.observer.observe(document.body, { childList: true, subtree: true });
}
/**
* Инициализирует все ещё не инициализированные слайдеры в контейнере.
* @param {Element|Document} container — область поиска.
* @param {Object} [override={}] — параметры поверх data-options.
* @returns {void}
*/
init(container = document, override = {}) {
const nodes = container.querySelectorAll('.swiper:not(.swiper-initialized):not(.single)');
if (!nodes.length) return;
nodes.forEach((el) => {
el.style.display = 'block';
const params = this._buildParams(el, override);
const instance = new Swiper(el, params);
this.instances.set(el, instance);
});
}
/**
* Уничтожает существующий инстанс на элементе и создаёт новый.
* @param {Element} el — корневой элемент слайдера.
* @param {Object} [override={}] — параметры поверх data-options.
* @returns {Swiper|null}
*/
reinit(el, override = {}) {
if (!el) return null;
const instance = this.instances.get(el);
if (instance) {
instance.destroy(true, true);
this.instances.delete(el);
}
el.classList.remove('swiper-initialized');
el.style.display = 'block';
const params = this._buildParams(el, override);
const fresh = new Swiper(el, params);
this.instances.set(el, fresh);
return fresh;
}
/**
* Возвращает инстанс Swiper по элементу.
* @param {Element} el
* @returns {Swiper|undefined}
*/
get(el) {
return this.instances.get(el);
}
/**
* Мержит глобальный конфиг, data-options и override.
* @private
*/
_buildParams(el, override) {
return Object.assign({}, this.config, this._parseOptions(el), override);
}
/**
* Безопасно парсит data-options.
* @private
*/
_parseOptions(el) {
const raw = el.dataset.options;
if (!raw) return {};
try {
return JSON.parse(raw);
} catch (e) {
console.warn('[SwiperManager] Невалидный data-options:', el, e);
return {};
}
}
_onReady() {
this.init();
}
_onAjaxSuccess() {
setTimeout(() => this.init(), 100);
}
_onMutation(mutations) {
for (const mutation of mutations) {
if (mutation.addedNodes.length) {
this.init();
return;
}
}
}
}
Ключевые моменты:
WeakMapхранит соответствие «элемент → инстанс». Это позволяет вызыватьdestroy()на конкретном слайдере, не задевая соседей._buildParamsгарантирует приоритет: глобальный конфиг →data-options→ override. Предсказуемая цепочка вместо разрозненныхObject.assignпо компонентам.watchOverflow: trueв базовом конфиге скрывает слайдер, если слайдов меньше, чемslidesPerView. Без этой опции пустой слайдер выглядит сломанным.MutationObserverловит случаи, которые пропускаетonAjaxSuccess: вставки черезinnerHTML, компоненты сторонних библиотек, динамические модалки.
Запуск
window.swiperManager = new SwiperManager({
speed: 400,
slidesPerView: 1,
});
Дальше — единая точка доступа ко всем слайдерам страницы. Никаких window.reloadCatalog, никаких «не забудь вызвать в новом компоненте».
Обработка AJAX и динамического контента
Классическая схема «onAjaxSuccess + setTimeout» покрывает только запросы, инициированные Битриксом. Её хватает, пока проект не выйдет за пределы штатного bitrix:news.list. Дальше начинаются случаи, которые она не видит:
- модальное окно, содержимое которого вставлено через
innerHTML; - компонент сторонней библиотеки (
Vue,React,Alpine), отрендерившийся после AJAX; - слайдер внутри блока, добавленного пользовательским скриптом.
MutationObserver в классе закрывает эти случаи. Повторные вызовы init() безопасны благодаря селектору :not(.swiper-initialized): уже созданные слайдеры пропускаются. Это устраняет главный риск «наблюдателя» — многократную инициализацию одного элемента.
Событие onAjaxSuccess при этом остаётся как fallback. Задержка setTimeout(100) нужна, потому что Битрикс генерирует событие до того, как вставит HTML в DOM. Убрать её полностью нельзя — придётся ждать MutationObserver, что добавит лишний цикл отрисовки.
Работа со «сложными» слайдерами
Не все слайдеры укладываются в единый конфиг. Синхронизация thumbs, кастомные события, привязка к другим компонентам — такие случаи исключаются из автоматической инициализации классом .single. Ответственность за их создание переносится в код компонента.
Для этого в классе есть публичный reinit(el, override). Пример подключения скрипта из component_epilog.php — через Asset::addJs():
use Bitrix\Main\Page\Asset;
Asset::addJs('/local/templates/.default/js/product-gallery.js');
Содержимое product-gallery.js:
document.addEventListener('DOMContentLoaded', () => {
const gallery = document.querySelector('#productGallery');
const thumbs = document.querySelector('#productThumbs');
if (!gallery || !thumbs) return;
const thumbsInstance = new Swiper(thumbs, {
slidesPerView: 4,
spaceBetween: 10,
watchSlidesProgress: true,
});
window.swiperManager.reinit(gallery, {
thumbs: { swiper: thumbsInstance },
});
});
Контракт простой: если разработчик ставит класс .single, он обязан вызвать reinit() сам. Взамен — полная свобода в конфигурации без риска, что менеджер вмешается и перезапишет настройки.
Жизненный цикл: destroy вместо reInit
Метод reInit() был в старых версиях Swiper и убран в актуальных. Причина — непредсказуемое поведение при смене набора слайдов: остаются клоны в loop-режиме, кэшированные размеры, устаревшие ссылки на события. Правильный путь один — destroy() + новый инстанс.
const instance = window.swiperManager.get(el);
if (instance) {
instance.destroy(true, true);
}
Два аргумента destroy важны:
deleteInstance = true— удаляетel.swiperи связанные данные;cleanStyles = true— удаляет inline-стили, добавленныеSwiperпри инициализации.
Без cleanStyles = true при повторной инициализации стили наслаиваются: слайды прыгают, ширина контейнера не пересчитывается, loop дублирует уже имеющиеся клоны. Именно из-за этого «reInit() не работает» — на самом деле он просто не убирает то, что нужно убрать.
Marquee с drag по удержанию
Бесконечный marquee с drag по удержанию — задача, где стандартные настройки Swiper конфликтуют друг с другом. Разберём по пунктам.
Конфликт freeMode и autoplay
Первая мысль — включить freeMode: true и autoplay с delay: 0. Результат — слайдер дёргается или останавливается после первого взаимодействия. В Swiper 10 freeMode перехватывает события взаимодействия и принудительно останавливает автоплей, даже при disableOnInteraction: false. Для чистого marquee freeMode исключается.
const marqueeConfig = {
slidesPerView: 'auto',
spaceBetween: 20,
loop: true,
speed: 6000,
allowTouchMove: true,
simulateTouch: true,
grabCursor: false,
autoplay: {
delay: 0,
disableOnInteraction: false,
pauseOnMouseEnter: false,
},
};
delay: 0 и большое speed дают непрерывное движение. Но одного этого мало: по умолчанию Swiper использует функцию перехода с ускорением, из-за чего marquee «пульсирует» — на каждом слайде движение замедляется и снова ускоряется. Лечится одной строкой в CSS, подключаемой через Asset::addCss() или styles.css шаблона:
.swiper-marquee .swiper-wrapper {
transition-timing-function: linear !important;
}
Порог движения и интерактивные элементы
Swiper стартует drag при малейшем сдвиге курсора. Пользователь кликает ссылку в слайде, рука дрогнула на 2–3 пикселя — сработал drag, ссылка не открылась. Решается комбинацией порога и временного отключения allowTouchMove над интерактивными элементами.
const marqueeEl = document.querySelector('.swiper-marquee');
const marqueeSwiper = window.swiperManager.reinit(marqueeEl, {
...marqueeConfig,
threshold: 10,
});
marqueeEl.addEventListener('mouseover', (e) => {
if (e.target.closest('a, button, [role="button"]')) {
marqueeSwiper.allowTouchMove = false;
}
});
marqueeEl.addEventListener('mouseout', (e) => {
if (e.target.closest('a, button, [role="button"]')) {
marqueeSwiper.allowTouchMove = true;
}
});
threshold: 10 означает: drag начнётся только после смещения курсора на 10 пикселей. Для кликов этого достаточно. Для touch-устройств дополнительная защита не нужна — там threshold работает как системный порог распознавания жеста.
Конфигурация в шаблоне компонента
На стороне PHP изменения минимальны. Единственное улучшение — вынести json_encode в хелпер, чтобы не дублировать параметры экранирования в каждом template.php:
function swiperDataOptions(array $options): string {
return htmlspecialchars(
json_encode($options, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
ENT_QUOTES,
'UTF-8'
);
}
Пример использования в шаблоне компонента:
<div class="swiper card" data-options='<?=swiperDataOptions([
'pagination' => [
'el' => '.swiper-pagination'
],
'loop' => false,
'effect' => 'slide',
'speed' => 0,
])?>'>
<div class="swiper-wrapper">
<?foreach ($morePhoto as $IMAGE){?>
<div class="swiper-slide"><img src="<?=$IMAGE;?>" alt="<?=$productTitle?>"></div>
<?}?>
</div>
<div class="swiper-pagination"></div>
</div>
ENT_QUOTES защищает атрибут от поломки, если в JSON попадут кавычки или <. JSON_UNESCAPED_UNICODE сохраняет русские строки читаемыми при отладке в DevTools. JSON_UNESCAPED_SLASHES — чтобы селекторы вида .swiper-pagination не превращались в .swiper-pagination с экранированными слешами.
Итог
Стандартный подход — inline-скрипт в каждом компоненте, конфиг вразнобой, реакция на AJAX через setTimeout — работает, пока проект маленький. Как только появляется динамика, он превращается в набор костылей. Класс SwiperManager меняет точку зрения: слайдер — это не часть шаблона, а объект с жизненным циклом. Его создаёт, пересоздаёт и уничтожает один менеджер, а разметка лишь описывает конфигурацию через data-options.
Результат — три уровня конфигурации вместо разрозненных скриптов, MutationObserver вместо setTimeout, публичный API для сложных случаев вместо копипаста, и явный жизненный цикл через destroy(true, true). Тот же Swiper, та же разметка — но управляемая.
Комментарии