Для разработчика тем Drupal не секрет, что он имеет в своем арсенале механизм JavaScript behaviors, который при умелом использовании значительно улучшит структуру и модульность JavaScript вашей темы или модуля. Ниже я предлагаю вам для ознакомления перевод оригинальной статьи Understanding JavaScript behaviors in Drupal, в котором, возможно, вы найдете для себя то, чего вы еще не знали о behaviors в Drupal. Итак, приступим.
Первый взгляд на Drupal behaviors
Если мы обратимся к официальной документации по использованию JavaScript в Drupal, то мы узнаем, что модули должны использовать Drupal.behaviors
для предоставления своей JavaScript логики. Например, так:
Drupal.behaviors.exampleModule = { attach: function (context, settings) { $('.example', context).click(function () { $(this).next('ul').toggle('show'); }); } }; |
Ядро Drupal будет вызывать прикрепленные behaviors, когда DOM (объектная модуль документа HTML) будет полностью загружен, передавая два аргумента:
context
(по умолчанию весь DOM)settings
, который содержит объект настроек формируемый на стороне сервера
Мы можем убедиться в этом, если посмотрим на код в misc/drupal.js:
// Attach all behaviors. $(function () { Drupal.attachBehaviors(document, Drupal.settings); }); |
Заметьте: $(function () является шорткатом для $(document).ready().
И здесь есть важный момент на который мы должны обязательно обратить внимание: Drupal.attachBehaviors()
может быть вызван более одного раза после того, как DOM загружен. Ядро Drupal будет вызывать Drupal.attachBehaviors()
в следующих случаях:
- После того, как оверлей панели администратора будет вызван
- После того, как срабатывает сабмит формы AJAX Form API
- После того, как AJAX запрос модифицирует DOM, например ajax_command_replace()
Более того, сами модули также вызывают Drupal.attachBehaviors()
. Вот несколько примеров:
- CTools вызывает его после того, как загружает модальное окно.
- Media вызывает его после того, как Media Browser был загружен.
- Pannels вызывает его после того, как завершаете in-place редактирование блока или элемента.
- Views вызывает его после загрузки новой страницы, которая использует AJAX.
- Views Load More вызывает его после загрузки следующей страницы.
- JavaScript кастомных модулей может вызвать
Drupal.attachBehaviors()
, когда они добавляют или изменяют части страницы.
После первого вызова Drupal.attachBehaviors()
, переменная context
содержит объект документа, представляющий DOM, но при последующих вызовах она будет содержать только ту часть HTML, которая модифицируется. Это часто упускается из виду разработчиками, что приводит к созданию плохого кода и ошибкам JS в браузере.
Как мы можем убедиться, что существующие Drupal behaviors принимают это во внимание? В следующем разделе мы сделаем отладку на реальном Drupal проекте.
Углубляемся Drupal behaviors
Когда вы работаете с Drupal проектом, то можно установить брикпоинт в дебагере на Drupal.attachBehaviors()
и посмотреть где и как вызывается этот метод.
Здесь в качестве примера автор рассматривает проект Syfy. Используя Google Chrome Developer Tools, мы можем открыть главную страницу сайта Syfy и установить брикпоинт в том месте кода файла misc/drupal.js, которое указано на скриншоте ниже:
После того, как страница будет перезагружена, дебагер будет останавливаться и ждать наших действий каждый раз, как будет вызван Drupal.attachBehaviors()
Ранее мы сказали что Drupal будет вызывать Drupal.attachBehaviors()
после полной загрузки DOM. Давайте проверим это в дебагере. Ниже вы можете видеть переменные context
и settings
на панели области видимости переменных.
Как видите большинство кастомных behaviors вызываются единожды, и код кажется вполне нормальным на данном этапе. Но давайте проверим так ли это, и останется ли все в порядке после того, как Drupal.attachBehaviors()
будет вызван повторно на странице.
Тестируем behaviors при последующих вызовах
Главная страница сайта Syfy использует Views и Views load more для построения плитки материалов. Давайте проскроллим вниз и нажмем «Load More», чтобы увидеть что происходит при этом:
Drupal вызывает /views/ajax, чтобы загрузить еще список плиток и после того, как это происходит повторно срабатывает Drupal.attachBehaviors()
. Мы можем заметить это при просмотре окна переменных в дебагере:
Переменная context
содержит только обновляемую область вместо всего HTML документа. И это значительное упущение. Drupal behaviors, которые принимают используют переменную context
в качестве второго аргумента в стандартной функции jQuery поиска элементов DOM, например: $('#some-id', context)
, не будут выполнятся, если в области элементов контекста не будет искомого. Многие вовсе не используют переменную context
при работе с селекторами jQuery — это распространенная ошибка. Ниже пример одного behavior, где виден ошибочный подход:
/** * Hide menu when Esc is pressed. */ Drupal.behaviors.syfyGlobalHideMenu = { attach: function (context, settings) { $(document).keyup(function (e) { if (e.keyCode == 27) { $('.nav-flyout', context).removeClass('js-flyout-active'); } }); } }; |
Данный код устанавливает слушатель на уровне всего документа на событие нажатия клавиши «Esc», давая тем самым возможность спрятать меню.
В чем же здесь проблема? в коде используется переменная context
, чтобы найти элемент меню $('.nav-flyout', context)
, но не используется эта же переменная, когда мы устанавливаем слушатель $(document).keyup(function(e)
, это значит что слушатель будет добавляться каждый раз, ка будет выполнятся Drupal behaviors.
Делаем правильные behaviors
Чтобы исправить ситуацию выше, есть два варианта решений. Давайте рассмотрим оба.
Использование jQuery Once
Данный подход рекомендуется в документации по JavaScript на Drupal.org. jQuery Once позволяет предотвратить повторный запуск кода, добавляя специальный CSS класс к элементу, если код уже был выполнен.
/** * Hide menu when Esc is pressed. */ Drupal.behaviors.syfyGlobalHideMenu = { attach: function (context, settings) { $('.nav-flyout', context).once('remove-modals', function () { $(document).keyup(function (e) { if (e.keyCode == 27) { $('.nav-flyout', context).removeClass('js-flyout-active'); } }); }); } }; |
В данном случае будет добавлен класс remove-modals-processed, после того, как код будет выполнен в первый раз. В следующий раз, когда Drupal.attachBehaviors()
будет вызван, .once()
обнаружит данный класс у элемента и пропустит выполнение кода.
Используем переменную context в jQuery селекторе
Очень часто не использование данного подхода может послужить причиной довольного сложного дебага непредвиденных ошибок в behaviors. Ниже пример, где мы используем переменную context
в селекторах:
/** * Hide menu when Esc is pressed. */ Drupal.behaviors.syfyGlobalHideMenu = { attach: function (context, settings) { $(document, context).keyup(function (e) { if (e.keyCode == 27) { $('.nav-flyout', context).removeClass('js-flyout-active'); } }); } }; |
Данный подход прост и эффективен. jQuery найдет объект document
внутри переменной context
только при первом вызове Drupal.attachBehaviors()
. Вторично выполнится данный код может только лишь в том случае, если где-либо будет вызываться Drupal.attachBehaviors(document)
, по сути ситуация абсурдная, но стоит иметь это ввиду.
Альтернатива
В данной ситуации мы вовсе игнорируем Drupal behaviors и просто ждем пока загрузится DOM:
/** * Hide menu when Esc is pressed. */ $(function () { $(document).keyup(function (e) { if (e.keyCode == 27) { $('.nav-flyout').removeClass('js-flyout-active'); } }); }); |
Данный код будет работать как нужно, и если вам не нужен доступ к полезным вещам, которые нам дает Drupal behaviors, то вы можете также использовать его. Это нормально.
Заключение
Самое главное, что мы должны почерпнуть из этой статьи — это то, что Behaviors всегда будет вызываться, когда DOM загружен, и может вызываться еще в измененным содержанием переменной context
в случае, если DOM был изменен или дополнен. Поэтому наш код должен быть построен с учетом этого.
Спонсоры статьи: