Стандартизация Swiper и макетов в Битриксе: уходим от хаоса в inline-скриптах
Rus
Eng
Стандартизация Swiper и макетов в Битриксе: уходим от хаоса в inline-скриптах
Решаю сложные технические задачи для Bitrix и 1С: от аудита чужого кода до разработки сайтов и настройки сложных интеграций с гарантией результата
Рекомендую

Хостинг для сайта

Если выбираете хостинг под проект — советую 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-решений. Разметка описывает структуру, атрибуты — конфигурацию, 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-* выразить невозможно.

Breakpoints в data-options

Адаптивность слайдера — как раз тот случай, где плоские атрибуты не справляются. В Swiper за это отвечает объект breakpoints, где ключ — минимальная ширина вьюпорта, значение — набор переопределений. Через data-* это выражается десятком избыточных атрибутов, через data-options — одним блоком.

<div class="swiper card" data-options='<?=swiperDataOptions([
    'slidesPerView' => 1,
    'spaceBetween' => 10,
    'breakpoints' => [
        768 => [
            'slidesPerView' => 2,
            'spaceBetween' => 15,
        ],
        1024 => [
            'slidesPerView' => 3,
            'spaceBetween' => 20,
        ],
    ],
])?>'>
    <div class="swiper-wrapper">
        <div class="swiper-slide">…</div>
    </div>
</div>

Менеджер просто передаёт этот объект в Swiper, никакой дополнительной обработки не требуется.

Класс 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.destroyed = false;

        this._onReady = this._onReady.bind(this);
        this._onAjaxSuccess = this._onAjaxSuccess.bind(this);
        this._onMutation = this._onMutation.bind(this);

        this._bindEvents();
    }

    /**
     * Навешивает обработчики: DOMContentLoaded, Bitrix AJAX, MutationObserver.
     * Наблюдение идёт за documentElement, а не за document.body — если скрипт
     * подключён в head, body на этот момент ещё не существует.
     *
     * @returns {void}
     */
    _bindEvents() {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', this._onReady, { once: true });
        } else {
            this._onReady();
        }

        if (typeof BX !== 'undefined') {
            BX.ready(() => {
                BX.addCustomEvent('onAjaxSuccess', this._onAjaxSuccess);
            });
        }

        this.observer = new MutationObserver(this._onMutation);
        this.observer.observe(document.documentElement, { childList: true, subtree: true });
    }

    /**
     * Инициализирует все ещё не инициализированные слайдеры в контейнере.
     * Селектор :not(.swiper-initialized) опирается на класс, который Swiper
     * добавляет корневому элементу после успешного создания. Именно это
     * делает повторные вызовы init() безопасными.
     *
     * @param {Element|Document} container — область поиска.
     * @param {Object} [override={}] — параметры поверх data-options.
     * @returns {void}
     */
    init(container = document, override = {}) {
        if (this.destroyed) return;

        const nodes = container.querySelectorAll('.swiper:not(.swiper-initialized):not(.single)');
        if (!nodes.length) return;

        nodes.forEach((el) => {
            const params = this._buildParams(el, override);

            try {
                const instance = new Swiper(el, params);
                this.instances.set(el, instance);
            } catch (e) {
                console.error('[SwiperManager] Не удалось создать Swiper:', el, e);
            }
        });
    }

    /**
     * Уничтожает существующий инстанс на элементе и создаёт новый.
     *
     * @param {Element} el — корневой элемент слайдера.
     * @param {Object} [override={}] — параметры поверх data-options.
     * @returns {Swiper|null}
     */
    reinit(el, override = {}) {
        if (!el || this.destroyed) return null;

        const instance = this.instances.get(el);
        if (instance) {
            try {
                instance.destroy(true, true);
            } catch (e) {
                console.warn('[SwiperManager] Ошибка destroy:', e);
            }
            this.instances.delete(el);
        }

        el.classList.remove('swiper-initialized');

        const params = this._buildParams(el, override);

        try {
            const fresh = new Swiper(el, params);
            this.instances.set(el, fresh);
            return fresh;
        } catch (e) {
            console.error('[SwiperManager] Не удалось пересоздать Swiper:', el, e);
            return null;
        }
    }

    /**
     * Возвращает инстанс Swiper по элементу.
     *
     * @param {Element} el
     * @returns {Swiper|undefined}
     */
    get(el) {
        return this.instances.get(el);
    }

    /**
     * Отключает наблюдатель и помечает менеджер уничтоженным.
     * Нужен, если менеджер создаётся и уничтожается динамически.
     *
     * @returns {void}
     */
    destroy() {
        if (this.observer) {
            this.observer.disconnect();
            this.observer = null;
        }

        this.destroyed = true;
    }

    /**
     * Мержит глобальный конфиг, 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);
    }

    /**
     * Реагирует на добавление узлов в DOM. Скан ограничен mutation.target,
     * а не всем документом — на активном DOM это критично.
     *
     * @private
     */
    _onMutation(mutations) {
        if (this.destroyed) return;

        const targets = new Set();

        for (const mutation of mutations) {
            if (!mutation.addedNodes.length) continue;

            const node = mutation.target instanceof Element
                ? mutation.target
                : mutation.target.parentElement;

            if (node) {
                targets.add(node);
            }
        }

        targets.forEach((target) => {
            this.init(target);
        });
    }
}

Ключевые моменты:

  • WeakMap хранит соответствие «элемент → инстанс». Это позволяет вызывать destroy() на конкретном слайдере, не задевая соседей.
  • _buildParams гарантирует приоритет: глобальный конфиг → data-options → override. Предсказуемая цепочка вместо разрозненных Object.assign по компонентам.
  • watchOverflow: true в базовом конфиге скрывает слайдер, если слайдов меньше, чем slidesPerView. Без этой опции пустой слайдер выглядит сломанным.
  • MutationObserver ловит случаи, которые пропускает onAjaxSuccess: вставки через innerHTML, компоненты сторонних библиотек, динамические модалки.
  • Наблюдение идёт за document.documentElement, а не за document.body. Если скрипт подключён в <head>, document.body в этот момент ещё null, и observer.observe() выбросит исключение.
  • Класс swiper-initialized — служебный, его добавляет сам Swiper после успешного создания инстанса. Селектор :not(.swiper-initialized) опирается именно на него, и это делает повторные вызовы init() идемпотентными.
  • Никаких принудительных el.style.display = 'block'. Слайдер может быть скрыт по бизнес-логике — например, до раскрытия аккордеона. Менеджер не имеет права менять это состояние.
  • Вокруг new Swiper() стоит try/catch: невалидный селектор, отсутствующий модуль, конфликт с другим плагином — всё это логируется, но не валит остальные слайдеры на странице.
  • Метод destroy() отключает наблюдателя. Если менеджер создаётся динамически (например, в SPA-переходе), без него останется висеть MutationObserver.

Запуск

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, что добавит лишний цикл отрисовки.

Производительность

MutationObserver на documentElement — это глобальный наблюдатель, и наивная реализация превращает его в узкое место: каждое изменение DOM вызывает полный скан документа. Правил три.

Скан ограничен mutation.target, а не document. Когда Битрикс подгружает блок через AJAX, mutation.target — это контейнер, в который вставили HTML. Искать слайдеры нужно только там, а не по всей странице. На каталоге в 500 карточек это разница между querySelectorAll по 500 элементам и по одному блоку.

Один наблюдатель на странице, не на компонент. SwiperManager создаётся один раз, в основном скрипте шаблона сайта. Если создавать его в каждом component_epilog.php, на странице будет столько же наблюдателей, сколько компонентов со слайдерами, и каждый будет получать свою копию всех мутаций DOM.

Batch-обработка через Set. Если за один тик пришло десять мутаций в разных узлах, _onMutation собирает их в Set уникальных целей и вызывает init() один раз на каждую уникальную цель, а не десять раз подряд по одному и тому же контейнеру.

Отдельно стоит помнить про сам Swiper: каждый инстанс создаёт свои слушатели событий и держит ссылки на DOM. Если у вас на странице пятьдесят слайдеров — это пятьдесят объектов с requestAnimationFrame внутри. watchOverflow: true в базовом конфиге скрывает пустые слайдеры, но не мешает им создаваться. Если в списке заведомо много элементов без слайдов, лучше не навешивать класс .swiper в шаблоне вовсе.

Работа со «сложными» слайдерами

Не все слайдеры укладываются в единый конфиг. Синхронизация thumbs, кастомные события, привязка к другим компонентам — такие случаи исключаются из автоматической инициализации классом .single. Ответственность за их создание переносится в код компонента.

Важно: если у элемента есть класс .swiper и нет .single, менеджер инициализирует его сам на DOMContentLoaded. Если разработчик потом вызовет reinit(), инстанс будет уничтожен и создан заново — это лишняя работа. Поэтому в примерах ниже у слайдеров, которыми управляет компонент, обязательно стоит .single.

Для подключения скрипта из 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 },
    });
});

В разметке:

<div id="productGallery" class="swiper single">…</div>
<div id="productThumbs" class="swiper single">…</div>

Контракт простой: если разработчик ставит класс .single, он обязан вызвать reinit() сам. Взамен — полная свобода в конфигурации без риска, что менеджер вмешается и перезапишет настройки.

Вложенные слайдеры и конфликты селекторов

Отдельный случай — слайдер внутри слайдера. Например, галерея товара открывается в модальном окне, а внутри галереи есть свой мини-слайдер с логотипами брендов. Если оба элемента помечены классом .swiper без .single, менеджер инициализирует оба, но при пересоздании внешнего он уничтожит свой инстанс и создаст заново, не тронув внутренний. Это нормально, пока их настройки не конфликтуют.

Проблема начинается, когда селекторы в опциях написаны без контекста. Пример плохого data-options:

{
  "pagination": { "el": ".swiper-pagination" },
  "navigation": { "nextEl": ".swiper-button-next" }
}

Внутри внешнего слайдера есть и свой .swiper-pagination, и у вложенного тоже. Swiper берёт первый найденный в документе элемент и подключает пагинацию внешнего слайдера к узлу внутреннего. Визуально это выглядит как «пагинация не работает» или «слайды дублируются».

Правильно — привязывать элементы к конкретному слайдеру через data-*-атрибуты внутри контейнера:

<div class="swiper card" data-options='<?=swiperDataOptions([
    "pagination" => ["el" => "[data-swiper-pagination]"],
    "navigation" => [
        "nextEl" => "[data-swiper-next]",
        "prevEl" => "[data-swiper-prev]",
    ],
])?>'>
    <div class="swiper-wrapper">…</div>
    <div class="swiper-pagination" data-swiper-pagination></div>
    <button type="button" class="swiper-button-next" data-swiper-next aria-label="Следующий слайд"></button>
    <button type="button" class="swiper-button-prev" data-swiper-prev aria-label="Предыдущий слайд"></button>
</div>

Селектор [data-swiper-pagination] остаётся уникальным внутри компонента, даже если на странице тридцать слайдеров с такой же разметкой. Для вложенных случаев, где нужно явное исключение, дополнительно вешают .single на внутренний и управляют им из кода.

Жизненный цикл: 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

Первая мысль — включить freeMode: true и autoplay с delay: 0. Для многих конфигураций это действительно работает: freeMode снимает фиксацию на позициях слайдов, а нулевая задержка даёт непрерывное движение. Но поведение зависит от версии Swiper и от того, как реализован режим остановки автоплея при взаимодействии. В некоторых сборках freeMode начинает перехватывать события drag и останавливать автоплей даже при disableOnInteraction: false, в других — нет.

Универсально безопасная конфигурация marquee — без freeMode, но с loop, длинным speed и delay: 0:

const marqueeConfig = {
    slidesPerView: 'auto',
    spaceBetween: 20,
    loop: true,
    speed: 6000,
    allowTouchMove: true,
    simulateTouch: true,
    grabCursor: false,
    autoplay: {
        delay: 0,
        disableOnInteraction: false,
        pauseOnMouseEnter: false,
    },
};

Если проект уже опирается на freeMode: true и работает стабильно — не трогайте. Если поведение плавает — уберите freeMode, как в примере выше.

delay: 0 и большое speed дают непрерывное движение. Но одного этого мало: по умолчанию Swiper использует функцию перехода с ускорением, из-за чего marquee «пульсирует» — на каждом слайде движение замедляется и снова ускоряется. Лечится одной строкой в CSS, подключаемой через Asset::addCss() или styles.css шаблона:

.swiper-marquee .swiper-wrapper {
    transition-timing-function: linear !important;
}

Если проект должен работать в очень старых браузерах, где анимации переходов отключены пользователем (prefers-reduced-motion: reduce), стоит подстраховаться:

@media (prefers-reduced-motion: reduce) {
    .swiper-marquee .swiper-wrapper {
        transition-duration: 0.01ms !important;
    }
}

Это не отключает сам marquee, но убирает длинную анимацию перехода, оставляя мгновенную смену кадров — приемлемая деградация для пользователей с вестибулярными расстройствами.

Порог движения и интерактивные элементы

Swiper стартует drag при малейшем сдвиге курсора. Пользователь кликает ссылку в слайде, рука дрогнула на 2–3 пикселя — сработал drag, ссылка не открылась. Решается комбинацией порога и временного отключения allowTouchMove над интерактивными элементами.

Ключевой момент: mouseover/mouseout не покрывают touch-устройства — там эти события либо не приходят, либо приходят с задержкой. Правильно использовать pointerdown, который срабатывает и для мыши, и для пальца, и для стилуса. Мы временно отключаем allowTouchMove в момент нажатия и возвращаем обратно на pointerup или pointercancel.

const marqueeEl = document.querySelector('.swiper-marquee');

const marqueeSwiper = window.swiperManager.reinit(marqueeEl, {
    ...marqueeConfig,
    threshold: 10,
});

marqueeEl.addEventListener('pointerdown', (e) => {
    const interactive = e.target.closest('a, button, input, select, textarea, [role="button"]');
    marqueeSwiper.allowTouchMove = !interactive;
});

marqueeEl.addEventListener('pointerup', () => {
    marqueeSwiper.allowTouchMove = true;
});

marqueeEl.addEventListener('pointercancel', () => {
    marqueeSwiper.allowTouchMove = true;
});

threshold: 10 означает: drag начнётся только после смещения курсора на 10 пикселей. Для кликов этого достаточно — короткий тап не превратится в drag. Для touch-устройств порог работает как системный порог распознавания жеста, а pointerdown с проверкой на интерактивный элемент отключает drag ещё до того, как он начнётся.

Отдельно стоит помнить, что allowTouchMove — обычное свойство инстанса Swiper. Если по клику оно переключается быстро и часто, могут оставаться «залипшие» состояния: пользователь нажал на кнопку, браузер не прислал pointerup (например, из-за жеста масштабирования). В таких случаях полезно дополнительно сбрасывать состояние на pointerleave.

Доступность

Слайдер, который выглядит хорошо, но недоступен с клавиатуры, — это половина слайдера. У Swiper есть встроенный модуль a11y, который включается опцией и расставляет большинство нужных атрибутов автоматически.

{
  "a11y": {
    "enabled": true,
    "prevSlideMessage": "Предыдущий слайд",
    "nextSlideMessage": "Следующий слайд",
    "firstSlideMessage": "Это первый слайд",
    "lastSlideMessage": "Это последний слайд",
    "paginationBulletMessage": "Перейти к слайду {{index}}"
  },
  "keyboard": {
    "enabled": true,
    "onlyInViewport": true
  }
}

Что это даёт:

  • корневой контейнер получает role="region" и aria-roledescription="carousel";
  • каждый слайд получает role="group", aria-roledescription="slide" и aria-label с номером;
  • кнопки навигации и точки пагинации получают aria-label и корректный tabindex;
  • клавиши ← и → переключают слайды, когда фокус внутри слайдера;
  • скринридер объявляет, на каком слайде пользователь находится.

Отдельно нужно позаботиться о кнопках навигации, если они кастомные:

<button type="button" class="swiper-button-prev" data-swiper-prev aria-label="Предыдущий слайд">
    <svg aria-hidden="true"><use href="/local/templates/.default/img/sprite.svg#arrow-left"></use></svg>
</button>

У самой иконки стоит aria-hidden="true", чтобы скринридер не дублировал название кнопки именем спрайта. Для декоративных элементов внутри слайда — alt="" у картинок, которые не несут смысла. Для информативных — alt с описанием товара.

Конфигурация в шаблоне компонента

На стороне PHP изменения минимальны. Единственное улучшение — вынести json_encode в хелпер, чтобы не дублировать параметры экранирования в каждом template.php. Обязательное условие — обернуть объявление в function_exists, иначе на странице с двумя шаблонами получится fatal error из-за повторного объявления.

if (!function_exists('swiperDataOptions')) {
    /**
     * Готовит JSON-строку для атрибута data-options.
     *
     * @param array $options Параметры Swiper
     * @return string Экранированный JSON
     */
    function swiperDataOptions(array $options): string
    {
        return htmlspecialchars(
            json_encode($options, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
            ENT_QUOTES,
            'UTF-8'
        );
    }
}

Хелпер кладётся в functions.php шаблона сайта, чтобы был доступен из любого компонента без дополнительных require.

Пример использования в шаблоне компонента:

<div class="swiper card" data-options='<?=swiperDataOptions([
    'pagination' => ['el' => '[data-swiper-pagination]'],
    'loop' => false,
    'effect' => 'slide',
    'speed' => 0,
])?>'>
    <div class="swiper-wrapper">
        <?foreach ($morePhoto as $IMAGE):?>
            <div class="swiper-slide">
                <img src="<?=htmlspecialcharsbx($IMAGE)?>" alt="<?=htmlspecialcharsbx($productTitle)?>">
            </div>
        <?endforeach;?>
    </div>
    <div class="swiper-pagination" data-swiper-pagination></div>
</div>

Про флаги json_encode:

  • ENT_QUOTES — защищает атрибут от поломки, если в JSON попадут одинарные или двойные кавычки, а также < и >. Без него первый же заголовок с апострофом порвёт разметку.
  • JSON_UNESCAPED_UNICODE — сохраняет русские строки читаемыми при отладке в DevTools.
  • JSON_UNESCAPED_SLASHES — полезен для URL. Без него строка https://domain.ru/ превратится в https:\/\/domain.ru\/ — визуально шумно и хуже читается. К селекторам вроде .swiper-pagination этот флаг отношения не имеет.

Обработка ошибок и логирование

Ошибки внутри SwiperManager не должны валить страницу целиком. Правила простые.

Создание и пересоздание инстанса — в try/catch. Невалидный селектор, отсутствующий модуль, конфликт с другим плагином, который уже навесил свои обработчики на тот же элемент — всё это логируется через console.error и позволяет остальным слайдерам на странице инициализироваться нормально.

Парсинг data-options — в try/catch с предупреждением. Если разработчик забыл запятую в JSON — пользователь увидит слайдер без настроек, а не пустую страницу. В консоль уходит console.warn с указанием конкретного элемента, что сразу локализует проблему.

Ошибки внутри destroy() — не критичны, но логируются. Если инстанс уже был уничтожен или Swiper в момент вызова destroy ещё не до конца инициализирован, исключение не должно прерывать выполнение reinit().

Для продакшена имеет смысл заменить console.* на собственный логгер с уровнями и отправкой критичных ошибок в систему мониторинга. Но это уже отдельная задача, за пределами статьи.

Тестирование

Три сценария, которые обязательно проверяются руками перед выкладкой.

Каталог с AJAX-пагинацией. Открыть каталог, пролистать три-четыре страницы. Каждый раз после подгрузки слайдеры в карточках должны быть инициализированы, кнопки навигации работать, а в консоли — ни одной ошибки. Параллельно проверить во вкладке Performance, что MutationObserver не растёт в количестве и что каждая пагинация добавляет один вызов init, а не десять.

Модальное окно со слайдером. Открыть модалку, закрыть, открыть заново пять раз. Внутри модалки должен создаваться новый инстанс, а старый — уничтожаться. Проверить через window.swiperManager.get(el): после закрытия модалки метод должен вернуть либо новый инстанс (при повторном открытии), либо undefined (если DOM удалён). Если возвращается старый инстанс с мёртвыми ссылками — где-то пропущен destroy().

Marquee с drag. Открыть страницу с marquee на десктопе и на мобильном. На десктопе — проверить, что клик по ссылке внутри слайда открывает ссылку, а не запускает drag. На мобильном — проверить, что короткий тап по ссылке не превращается в drag и что долгое удержание и перетаскивание работает. В DevTools есть режим эмуляции тач-событий: More tools → Sensors → Touch.

Дополнительно стоит проверить сценарий с медленной сетью: включить троттлинг до Slow 3G, обновить страницу и убедиться, что слайдеры инициализируются после полной загрузки, а не с пустыми контейнерами. Если страница успевает отрисоваться до того, как Swiper подгрузился, MutationObserver должен поймать изменения и инициализировать слайдеры повторно.

Итог

Стандартный подход — inline-скрипт в каждом компоненте, конфиг вразнобой, реакция на AJAX через setTimeout — работает, пока проект маленький. Как только появляется динамика, он превращается в набор костылей. Класс SwiperManager меняет точку зрения: слайдер — это не часть шаблона, а объект с жизненным циклом. Его создаёт, пересоздаёт и уничтожает один менеджер, а разметка лишь описывает конфигурацию через data-options.

Результат — три уровня конфигурации вместо разрозненных скриптов, MutationObserver с ограничением по mutation.target вместо скана всего документа, публичный API для сложных случаев вместо копипаста, явный жизненный цикл через destroy(true, true) и доступность через встроенный модуль a11y. Тот же Swiper, та же разметка — но управляемая.

Александр Тихий
Автор статьи

Александр Тихий

Fullstack-разработчик · 1С-Битрикс

Разрабатываю и развиваю интернет-магазины и корпоративные порталы на 1С-Битрикс. Интеграции с 1С, highload-нагрузки, безопасность по 152-ФЗ.


Комментарии

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

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

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

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

Отправить

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

Напишите мне

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

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

Телефоны

+7(993) 007-18-96

Email

info@tichiy.ru

Адрес

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

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