+7 495 008 8452 пн.-пт. 10:00 – 17:00
Загрузка...
Если у вас возникли какие либо вопросы которые вы не смогли решить по нашим публикациям самостоятельно,
то ждем ваше обращение в нашей службе тех поддержки.


История одного бага

Сегодня описывал техподдержке один нетривиальный баг в работе компонентов в режиме ajax, да столько понаписал, что появилось желание посвятить этому багу пост во блоге.

Итак. Как известно, любой компонент битрикса гипотетически может работать в режиме ajax — для этого лишь надо в параметрах подключения компонента указать AJAX_MODE => Y. При наличии этой опции система сама подменяет нужные ссылки и сабмиты форм на js-вызовы (документация).

При клике по ссылке, ведущей на этот же компонент, серверу отправляется асинхронный запрос через XHR. При отправке же данных из формы передача данных происходит через скрытый iframe. Результат запроса в обоих случаях вставляется через innerHTML и, как следствие, js-скрипты при такой вставке не исполняются. Для обхода этого ограничения в битриксе были разработаны специальные методы для отработки javascript в контексте страницы.

Возвращаемся к проблеме. В 10 версии продукта сабмиты форм отлавливались путём добавления атрибута onsubmit к форме (bitrix/modules/main/ajax_tools.php):
function GetFormEvent($container_id)
{
    return 'onsubmit="BX.ajax.submitComponentForm(this, \''.htmlspecialchars(CUtil::JSEscape($container_id)).'\') ; "' ;
}

И всё было прекрасно вне зависимости от набора данных, возвращаемых аякс-запросом.

В 11 версии обработчик на сабмит стал вешаться не через атрибут onsubmit, а байндингом с помощью addEventListener, т.е. подписка на событие происходит путем вызова js-кода. Вроде бы всё ок и в преобладающем большинстве случаев оно работает. Однако, из-за особенностей реализации выполнения js-кода, передаваемого компонентом в ответе, возможна ситуация, когда байндинг не будет происходить после получения ajax-ответа и всё будет ломаться. Вот в такой ситуации и оказались читатели одного интернет-СМИ.

Пользователь открывает страницу, на которой размещён компонент с формой и включенным аяксом. Система генерирует js-код, который навешивает обработчик сабмита формы:
top.BX.bind(top, 'load', function(){
var obForm = top.BX('bxajaxid_7def71ebae1f4bad7f02ae3f6f4647ad_941').form;
top.BX.bind(obForm, 'submit', function() {BX.ajax.submitComponentForm(this, 'comp_7def71ebae1f4bad7f02ae3f6f4647ad')});
});

Код выполняется, обработчик навешивается.
Пользователь нажимает кнопку «Отправить» в форме. Обработчик перехватывает этот вызов, создаёт скрытый ифрэйм, в который сабмитится форма. В этот же момент к ифрэйму добавляется обработчик на событие onload — замена текущего вывода компонента новым html-кодом, пришедшим в качестве ответа:
obForm.BXFormCallback = function(d) {BX(container).innerHTML = d;};

При генерации ответа у нас опять же система генерирует код для навешивания обработчика сабмита (ибо AJAX_MODE в параметрах подключения равен Y). Однако, теперь у нас пришёл запрос с AJAX_CALL=Y, поэтому система находит все вставки js-кода в выводе компонента, вырезает их и пакует в js-объект. При этом генерируется код для отложенного на 300 мс («640K ought to be enough for anybody» ©) выполнения всего вырезанного js в контексте главного окна (window.top):
top.BX.CaptureEventsGet();
top.BX.CaptureEvents(top, 'load');
top.BX.evalPack(".CUtil::PhpToJsObject($arScripts).";
setTimeout('top.BX.ajax.__runOnload();', 300);

Таким образом, при получении результата аякс-запроса мы имеем два асинхронных потока выполнения: первый поток в событии load ифрэйма, второй - в самом ифрейме (назовём его ready аналогично jQuery):
  • ready выполняется, когда html-код (именно код, не ресурсы) страницы полностью загружен в ифрэйм
  • load выполняется, когда все ресурсы (стили, картинки, т.п.) загрузились. Т.е. фактически, когда вся страница готова к просмотру.
Если в ответе приходит html-код, не обременённый большим количеством ресурсов, всё работает замечательно. Однако, если в теле ответа на ajax-запрос присутствует, например, тяжёлая/долго отдаваемая картинка, происходит нештатная для текущего алгоритма ситуация: отложенный js-код начинает исполняться, а DOM ещё прежний — не произошла подмена старой области вывода компонента новой. Конкретно — не происходит байндинг события на форму, потому что формы с таким ID ещё не существует. Мы натолкнулись на эту проблему при использовании iblock.element.add.form с аяксом, в котором включена капча - если оная не успевается сгенерироваться и отдаться за ~300 миллисекунд, байндинг сабмита не происходит и следующий сабмит формы идёт уже не аяксом, а обычной перезагрузкой страницы, в итоге пользователь видит вывод компонента на пустой странице (т.к. AJAX_CALL=Y).

По этой проблеме был создан тикет, но был получен отлуп — «У компонента iblock.element.add.form нет ajax режима работы». Формально всё верно, т.к. галочки соответствующей у компонента действительно нет.

Однако, есть документация по работе компонентов в режиме ajax, которая регламентирует аякс-режим работы любого правильно сделанного компонента. Что ж, не будем ничего доказывать, копаем сами. Возникновения подобной проблемы можно добиться на любом стандартном компоненте с долго отдаваемой фотографией. Сделать это очень просто — втыкаем на страницу комплексный компонент catalog со включённым компонентом фильтра. В шаблон каталога производим вставку картинки:
<img src="/formtest_delay.php"/>

Содержимое скрипта-«картинки»:
<?php
delay(2);
?>

Отключаем временно показ этой картинки. Открываем каталог (ссылка вырезана цензурой:-)), нажимаем кнопку «Сбросить» в фильтре — каталог нормально отображается. Нажимаем ещё раз — всё ок.

Теперь подключаем картинку, проводим те же самые манипуляции. Нажимаем первый раз «Сбросить» — каталог прогрузился, однако, выскочила js-ошибка вида (копипаст из firebug):
top.BX("bxajaxid_9a85781ad70d7286c61a136298f44d6e_693") is null
[Break On This Error] <meta name="keywords" content="1С-Битр...rix, система управления контентом" />

При повторном нажатии на «Сбросить» страница уже перегружается и на ней отображается вывод компонента без шаблона сайта. Как раз то, что нужно — неведомый баг, который я отлавливал на протяжении полутора дней. Просто, быстро, но печально.

Стабильная повторяемость бага достигнута, теперь бы надо его пофиксить. Часик поразмышляв над нарисованными в тетради схемами вызовов всех функций, приводящих к багу, родилось два вариант
Из очевидных решений на ум пришло два:

1. Реализация deferred-обработчиков load+ready для ифрэйма с костылем под обычный xhr.

2. Двухстрочный файлов ядра, позволящий сделать исполнение js-кода, выводимого компонентов, всегда после замены вывода компонента, тем самым решая проблема с асинхронностью. Сурово, но иначе никак. Без копания в ядре нижеприведённый патч вам ничего не скажет, но разобрать проблему и не показать решение было бы подло:-)

./bitrix/modules/main/classes/general/component_ajax.php, __prepareScripts().
Исходная строка:
setTimeout('top.BX.ajax.__runOnload();', 300);
Модифицированная строка:
top === self ? setTimeout('top.BX.ajax.__runOnload();', 300) : null;

./bitrix/js/main/core/core_ajax.js, BX.ajax.submitComponentForm().
Исходная строка:
obForm.BXFormCallback = function(d) {BX(container).innerHTML = d;};
Модифицированная строка:
obForm.BXFormCallback = function(d) {BX(container).innerHTML = d; BX.ajax.__runOnload();};
Как ни странно, столь развёрнутый разбор проблемы не вызвал никаких возражений со стороны саппорта, поэтому тикет погулял по нескольким ответственным и направился в отдел разработки. Бага признана незначительной, однако надеюсь, что фикс выйдет действительно в ближайших обновлениях системы, а не в далёком и туманном будущем

Назад в раздел

Подписаться на новые материалы раздела:
Загрузка...