Стандартизация Swiper и макетов в Битриксе: уходим от хаоса в inline-скриптах
Rus
Eng
Стандартизация 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, та же разметка — но управляемая.

Комментарии

Комментариев еще нет, Вы можете стать первым кто его оставит

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

На сайте используется система премодерирования комментариев, поэтому ваше сообщение будет опубликовано лишь после одобрения модератором

Вы отвечаете на комментарий пользователя

Отправить

ОБРАТНАЯ СВЯЗЬ

Напишите мне

Напишите мне, чтобы обсудить задачу. Работаю удаленно, официально (самозанятый), стоимость часа — 1800 ₽, в день могу уделять проекту 3–4 часа. Отвечаю быстро, консультация бесплатная.

Во время отправки произошла ошибка, пожалуйста попробуйте еще раз через некоторое время
Сообщение отправлено успешно

Телефоны

+7(993) 007-18-96

Email

info@tichiy.ru

Адрес

Россия, г. Москва

Отправляя форму Вы автоматически подтверждаете, что ознакомились и принимаете Политику конфиденциальности сайта