Два мода на одной странице Тильды: сниппеты не мешают друг другу
Два самодостаточных сниппета на одной странице не конфликтуют, если второй экземпляр ядра не создаётся, а находит первый. Тогда два мода делят один наблюдатель, один таймер и один объект регистраций, а на странице остаётся один стиль на мод и одна метка на секцию. Цена за самодостаточность платится не поведением, а байтами: ядро лежит в каждом сниппете, и на этот счёт ниже цифры, а не слово «легковесно».
Совет «не вставляйте на страницу два сторонних скрипта» встречается часто, и за ним стоит настоящий опыт: два независимых куска кода правда умеют мешать друг другу. Но мешают они не потому, что их два, а из-за конкретных вещей в коде. Здесь разобрано, какие это вещи и как выглядит код, у которого этой проблемы нет.
Короткий ответ
Сниппет ModKod самодостаточен: внутри и ядро, и код мода. Второй такой блок
второе ядро не создаёт — он смотрит window.MK.version и, если ядро на странице
уже есть и не старше, работает с найденным. Дальше два мода делят один объект
регистраций, одну очередь, один таймер и один MutationObserver. Платить за это
приходится весом: ядро едет в каждом сниппете.
Что происходит, когда два сниппета объявляют одну глобальную переменную
Плохой сценарий выглядит так. Каждый из двух скриптов заводит себе имя в
window, и второй перетирает первый. Первый к этому моменту уже подписался на
скролл и повесил обработчики — они продолжают работать, но состояние, на которое
они смотрят, потеряно. Обратный случай не лучше: оба живут, оба делают одну и ту
же работу, и вы получаете две анимации на одном элементе, два кольца курсора и
два обработчика на одной кнопке.
Первое, что убирает половину этих случаев, — обёртка. Сниппет целиком выглядит так:
<script>
(function () {
'use strict';
var MK = (function () { /* ядро */ })();
/* код мода: MK.define('MOD-041', { … }) */
MK.run("MOD-041", { duration: 600, stagger: 70 });
})();
</script>
var MK объявлена внутри функции, а значит глобальной переменной не является:
снаружи её не видно, и второй сниппет со своим var MK про неё ничего не знает.
Единственное общее имя на всю страницу — window.MK, и его ставит только ядро,
последней строкой. Всё остальное — имена функций, счётчики, кеши — заперто в
своём вызове. Тильда, кстати, советует то же самое про стили: не брать в
селекторы теги и *, а использовать свои классы, чтобы не задеть чужое.
Защита версией: второй экземпляр берёт первый, а не перетирает
Начало ядра:
var MK = (function () {
var VERSION = 2;
// Ядро на странице уже есть и оно не старше нашего — берём его.
if (window.MK && window.MK.version >= VERSION) return window.MK;
Сравнение именно >=, а не ===, и это важно. Если сниппеты собраны в разное
время и во втором ядро постарше, оно увидит на странице более новое и уступит
ему: новое ядро умеет всё, что умело старое. Обратный порядок — сначала старое,
потом новое — единственный случай, когда на странице заводятся два ядра: новое
не может доверить работу старому, строит своё и занимает window.MK.
Этот случай не ломает страницу, и вот почему. Всё, что защищает от повторной работы, ядро хранит не в своих переменных, а в DOM: метка на секции — атрибут, ключ стиля — атрибут. Два ядра видят одну и ту же страницу, поэтому второе не запустит мод на секции, которую отметило первое, и не добавит второй такой же стиль. Хуже станет только с байтами и с лишним наблюдателем, а не с поведением.
Один стиль на страницу, сколько бы копий ни вставили
/** Стиль мода — один раз на страницу, сколько бы копий сниппета ни вставили. */
function css(id, text) {
var key = String(id).toLowerCase();
if (document.querySelector('style[data-mk="' + key + '"]')) return;
var style = document.createElement('style');
style.setAttribute('data-mk', key);
style.appendChild(document.createTextNode(text));
(document.head || document.documentElement).appendChild(style);
}
Проверка идёт по странице, а не по переменной вроде injected[id] = true. Разница
видна ровно в том пограничном случае из предыдущего раздела: переменная живёт
внутри одного ядра, а document.querySelector видит и то, что добавило чужое.
Второе ядро находит уже вставленный стиль и уходит.
Один наблюдатель на всех, а не по одному на мод
Секции в Тильде появляются и пересобираются после загрузки: артборд Zero Block
собирается позже DOMContentLoaded, ленивые блоки — ещё позже. Значит, страницу
нужно перепроверять. Наблюдатель при этом один на все моды:
function watch() {
if (watching) return;
watching = true;
// Zero Block пересобирает артборд на ресайзе — секцию нужно проверить снова.
window.addEventListener('resize', schedule);
var root = document.getElementById('allrecords') || document.body;
if (root && window.MutationObserver) {
new MutationObserver(schedule).observe(root, { childList: true, subtree: true });
}
}
Корень — #allrecords, а не document: это наименьший узел, внутри которого
Тильда создаёт и перерисовывает записи. Из типов изменений — только childList:
свои же атрибуты и классы наблюдателя не будят, иначе метка на секции вызывала
бы новую проверку, которая ставит метку.
Второй предохранитель — общий таймер:
function schedule() {
if (timer) return;
timer = setTimeout(function () {
timer = null;
rescan();
}, 80);
}
rescan() проходит по общей очереди и запускает все зарегистрированные моды.
Поэтому проверок ровно столько, сколько было изменений на странице, а не столько
же, умноженное на число модов. Считать тут есть что: каждый MK.run ставит
серию повторов на 150, 400, 1000 и 2500 мс — два мода дают восемь вызовов
schedule, к ним добавляется ресайз и каждое изменение разметки. Все они
сходятся в один проход раз в 80 мс.
Метка ставится до запуска: упавший мод не зовут второй раз
for (var i = 0; i < found.length; i++) {
var el = found[i];
if (el.hasAttribute(attr)) continue;
// Метку ставим до запуска: если мод упадёт, второй раз его не зовём.
el.setAttribute(attr, '1');
try {
mod.setup(el, params);
} catch (e) {
warn(id, e);
}
}
Порядок этих двух строк — не стиль, а поведение. Поставь метку после setup, и
любое исключение внутри мода означает, что метки нет: следующая перепроверка —
а она будет, их всегда несколько — вызовет setup на той же секции снова.
Каждый неудачный запуск успевает сделать часть работы: добавить элемент, повесить
слушателя. Через несколько проходов на секции висит десяток слушателей, а в
консоли — десяток одинаковых ошибок, из которых непонятно, что упало один раз.
Метка перед запуском превращает это в одну строку в консоли с идентификатором
мода: [ModKod] MOD-041: …. Остальная страница при этом работает — падение мода
поймано, соседний мод и рантайм Тильды его не заметят.
Сколько это весит на самом деле: цифры вместо «легковесно»
Ядро в собранном сниппете — 13 312 байт. Вот из чего складывается каждый:
| Мод | Сниппет | Код мода | Доля ядра |
|---|---|---|---|
| MOD-012 · кольцо-курсор | 22 231 Б | 8 919 Б | 60 % |
| MOD-041 · проявление текста | 25 410 Б | 12 098 Б | 52 % |
| MOD-077 · анимация чисел | 27 961 Б | 14 649 Б | 48 % |
| MOD-146 · зависимая анимация | 32 040 Б | 18 728 Б | 42 % |
| MOD-072 · проверка телефона | 36 361 Б | 23 049 Б | 37 % |
Два мода из первых строк — это 47 641 байт исходника в разметке страницы. Но
посетитель качает сжатое, и живая Тильда отдаёт страницы
с content-encoding: gzip — это проверено запросом, а не предположение.
У gzip окно поиска повторов
32 КБ, и от расстояния между блоками зависит, найдёт ли он второе ядро:
| Где стоит второй блок | Второй сниппет добавил |
|---|---|
| сразу за первым | 3 288 Б |
| через 20 КБ разметки | 7 632 Б |
| через 60 КБ разметки | 8 091 Б |
| он один на странице | 8 151 Б |
Отсюда практический вывод: держите блоки со скриптами рядом — например, все в конце страницы. Мод ищет свою секцию по классу, и где стоит сам блок с кодом, ему безразлично. Экономия — около пяти килобайт на паре модов, не больше, но она не стоит ничего.
Отдельный вопрос — не вес страницы, а лимит редактора, и здесь у пары модов есть своё следствие. Байты двух сниппетов в одном блоке складываются, в двух блоках — нет: это ещё один довод класть по моду в блок, кроме тех, что выше. Сам потолок блока с кодом — сколько он держит на самом деле и что про это говорит справка — разобран отдельно, куда на Тильде вставлять код, там же замер тринадцати блоков на живых страницах.
Если моды куплены у двух разных продавцов
Всё выше — про пару сниппетов одной библиотеки: у них общее ядро, и защита от двойной работы у них тоже общая. Спрашивают же обычно про другое: куплен аккордеон у одного автора и масштабирование в Zero Block у второго — их можно поставить вместе?
Можно, и место столкновения у такой пары одно. Не «два скрипта на странице», а
конкретное глобальное имя, которое оба захотят себе. На масштабировании это видно
буквально. Код Тильды, который считает масштаб, обёртки не имеет:
tilda-zero-scale-1.0.min.js начинается со строки function t396_scaleBlock(e){,
то есть все его функции — глобальные. Так устроены 33 из 44 файлов tilda-*.js в
рантайме. А сам масштаб этот файл спрашивает так:
"function" == typeof window.t396__getCurrentScaleFactor
? a = t396__getCurrentScaleFactor(e)
: (a = window.tn && window.tn["ab" + e] && window.tn["ab" + e].scaleFactor)
|| (a = window.tn_scale_factor);
Функция с этим именем объявлена в другом файле Тильды, tilda-zero-1.1.min.js, и
файл масштабирования берёт её через window по имени. Для мода это удобная точка
входа: нужно поменять масштаб — подмени window.t396__getCurrentScaleFactor.
Поэтому такие моды и работают.
Поэтому же два таких мода вместе не работают. Проверка
typeof … == "function" истинна для любой функции и не умеет заметить, что имя
уже занято чужим модом: масштаб посчитает та версия, которая лежит в window в
момент вызова, то есть определённая последней. Порядок задают блоки на странице, а
не авторы, и ошибки в консоли не будет — будет «второй мод не действует» или
«масштаб не тот».
Что с этим делать покупателю:
- Узнать имя. У каждого мода спросить продавца, какую глобальную функцию он
переопределяет, — или посмотреть самому: в консоли опубликованной страницы
набрать
t396__getCurrentScaleFactor.toString(). Родная версия Тильды короткая, в одну строку сwindow.tn[e].scaleFactor. Если там чужой код — имя занято, и видно, кем. - Имена разные — ставьте спокойно. Два мода, которые правят разные функции, не пересекаются вообще; общее у них только то, что оба лежат на странице.
- Имя одно — порядком блоков не лечится. Переставить блоки местами значит выбрать, какой из двух модов работает, а не заставить работать оба. Починка здесь одна: один из модов должен сохранить прежнюю функцию и вызвать её, а не выбросить. Это разница между модом, который умеет соседей, и модом, который просто занял место, — и спрашивать об этом стоит до покупки.
Чем от этого отличается общее ядро: у модов одной библиотеки на всю страницу одно
глобальное имя, window.MK, и второй сниппет его не занимает, а находит — как
разобрано выше. Столкнуться там нечем по устройству, а не по договорённости
авторов. Обратная сторона у этого честная: чем больше эффектов собрано в одном
ядре, тем больше завязано на одну библиотеку. Страховка от привязки — не в
обещании, а в том же правиле >=: новое ядро подхватывает страницу со старыми
сниппетами, поэтому обновление одного мода не требует переставлять остальные, а
сниппет остаётся простым текстом в блоке, который вы можете удалить в любой день.
Что проверить на своей странице за две минуты
Сначала опубликуйте страницу: в редакторе и в предпросмотре код блока T123 показывается как текст, а не работает — так и задумано. Дальше в консоли опубликованной страницы:
MK.version— число. Ядро на странице есть.Object.keys(MK.mods)— список идентификаторов. Здесь должны быть все моды, которые вы вставили. Нет одного — ищите его блок, а не конфликт.document.querySelectorAll('style[data-mk]').length— по одному на мод со своим стилем, не больше.document.querySelectorAll('[data-mk-mod-041]').length— сколько секций взял мод. Ноль означает, что класс не на той секции или не сохранён.- строки
[ModKod]в консоли — упавший мод или плохой селектор, с идентификатором.
Если на странице есть чужой мод, к этому добавляется одна проверка — не занято ли
глобальное имя. Для масштабирования в Zero Block:
t396__getCurrentScaleFactor.toString(). Короткая функция в одну строку — родная
Тильды, никто её не подменял. Длинная или незнакомая — имя занято модом, и если
таких модов два, работает только один.
И один взгляд на вес: вкладка Network, строка самого документа, колонки Size и Content — сколько приехало и сколько распаковалось. Это и есть настоящая цена всех ваших блоков с кодом, включая чужие.
Все моды библиотеки собраны из одного ядра и проверяются вместе, так что описанное здесь одинаково для любой пары — каталог модов. Если эффектов нужно больше двух и они завязаны друг на друга, разумнее не складывать сниппеты, а собрать один под задачу: что делаем на заказ.