БлогВсё об AJAX в Drupal 7. Drupal Camp Minsk 2012.

14 апреля 2012 года в моём родном городе Минске прошёл очередной Drupal Camp. На нём я рассказывал об AJAX в семьмой версии Drupal. Видео прилагается:

А ниже предоставлен подробный план, по которому я рассказывал.

Введение

Ни для кого не секрет, что уже наступил 2012 год. Технологии развиваются семимильными шагами, и направление веб разработок, являясь одним из самых востребованных на сегодняшний день, в этом не отстаёт. Современные пользователи очень быстро привыкают к хорошему. Если раньше они были готовы сидеть и ждать по 10-15 секунд загрузки простой страницы, то сейчас же среднее время ожидания загрузки – 2 секунды, после чего вкладка с сайтом закрывается и открывается другой. Одной из технологий, ускоряющей процесс обновления обмена данными между пользователем и сервером стал AJAX. За счёт того, что страница обновляется не полностью, а лишь частично, что позволяет сэкономить время ожидания пользователя, AJAX стал крайне популярен среди веб разработчиков.

AJAX расшифровывается как Asynchronous Javascript and XML. Из названия можно заметить, что должен использоваться как javascript, так и xml. Те инструменты по работе с AJAX, которые находились ядре шестого Друпала, вместо XML использовал обычные HTML вставки. Поэтому там эта технология назвалась AHAH - Asynchronous HTML and HTTP. Реализация этой технологии в шестёрке имела ряд минусов:

  • Изменение только одной области страницы.
    Как я уже говорил, в шестёрке пользователю возвращается кусок HTML кода, который просто вставляется в указанный селектор. Это порождает некоторые неудобства, когда хочется изменить данные не только в рамках выбранного селектора, но и, например, обновить счётчик данных. Простейший пример с комментариями – если я добавил новый комментарий, я хочу, чтобы счётчик комментариев на странице материала увеличился. Из коробки так делать нельзя, приходилось писать js плагин к ajax-обработчику, который на ответе от сервера увеличивал бы этот счётчик. Ну а если хотелось не просто увеличить счётчик, а ещё и какой-нибудь контент добавить, то тут уже начинались серьёзные проблемы в реализации.
  • Нет инструментов, позволяющих динамически загрузить новый css, js или изменить Drupal.settings.
    Здесь всё предельно просто – в шестёрке просто нет возможности динамически работать с css или js. Конечно, можно было добавлять его просто куском кода на странице, но это не совсем удобно, и, говоря языком Друпала, не кошерно. А если же хотелось изменить данные в Drupal.settings, то приходилось пользоваться костылями.
  • Механизм по перезагрузке полей формы приходится писать самостоятельно.
    Данные в форме могут обновляться. Например, в зависимости от выбранной страны подгружать список городов. Это и быстро, и удобно. Однако Друпал – система с высоким уровнем безопасности. При каждой отправке формы с клиентской стороны на серверную Друпал сверяет полученные данные с теми, которые он отдавал пользователю (они берутся из кеша). И если в форме данные обновлялись, а в кеше - нет то Друпал заявлял, что он эту форму не создавал, и показывал пользователю ошибку. Для того, чтобы этого избежать, форму надо сохранять в кеше каждый раз после её изменения. В шестёрке такое кеширование не было реализовано, поэтому не все разработчики справлялись с реализацией перезагрузки форм.
  • Нет возможности загрузить новую форму через AJAX, которая также использовала бы AJAX
    К сожалению, в шестой версии Друпала этого сделать было просто нельзя. Форма, которая загружалась через AJAX, загружалась уже поддержки AJAX’a.
  • Нет единой страницы для AJAX callback'a форм.
    По своей сути, AJAX в формах всегда работает по одному принципу: передача данных с клиента на определённую страницу сайт; последующая загрузка формы из кеша; обработка формы; возвращение данных в формате json. В шестом Друпале каждый модуль, работающий с AJAX’ом, вынужден был создавать свою страницу, которая обрабатывала данные, переданные с помощью ajax. По хорошему, это включало в себя не просто создание страницы через hook_menu(), но и проверку полученных данных на валидность, на уязвимости и т.д. В добавок ко всему ещё и сеошникам приходилось следить за этими страницами, т.к. если их не закрыть в robots.txt, то поисковики с удовольствием их проиндексируют и пометят как «Формат документа не поддерживается».

Работа с AJAX в формах (интеграция с Forms API)

AJAX может добавляться практически к любому элементу формы, который несёт в себе какой-либо функционал. Например, textfield, select, tableselect, password, submit и т.д. Делается это добавлением к элементу атрибута #ajax с указанием пути или колбэка, по которому будут переданные данные.

  • #ajax['callback'] – фунцкия, которой передаётся форма и её состояние при срабатывании аякс-тригера. В этом случае данные передаются на путь system/ajax. Если нет необходимости в собственном обработчике страницы, то лучше всего использовать callback вместо path. Кстати, callback и path являются взаимоисключающими параметрами. При использовании callback форма автоматически сабмитится, т.е. вызываются все валидаторы и сабмитеры для данной формы. Причём важно понимать, что эта функция вызовется в любом случае, даже если форма не пройдёт валидацию.
  • #ajax['path'] – страница, которая получает данные, отправленные формой. В этом случае если необходима форма – её надо загружать самостоятельно с помощью функции ajax_get_form(), а так же обработчик формы тоже вызывать вручную через drupal_process_form().
  • #ajax['wrapper'] – селектор, куда будет помещены данные, полученные в результате выполнения аякс колбэка из #ajax[‘callback’].
  • #ajax['effect'] – Эффект добавления содержимого в селектор, указанный в #ajax[‘wrapper’].
  • #ajax['event'] – Событие, при котором сработает триггер аякса.
  • #ajax['keypress'] – Если установить значение в TRUE, то по нажатию на ENTER запустится триггер аякса на этом элементе, при условии, что фокус находится на этом элементе.
  • #ajax['method'] – Указывает параметры вставки данных в селектор, указанный в #ajax[‘wrapper’]. Допустимые значения: 'replace' (по умолчанию), 'after', 'append', 'before', 'prepend'.
  • #ajax['progress'] – Визуальный обработчик ожидания пользователя. Может быть вращающимся синим шариком (throbber, выбран по-умолчанию) или же полосой с индикатором загрузки (bar). Во втором случае можно указывать урл, по которому будет определяться статус выполнения процедуры.
  • #ajax['trigger_as'] – Для не-кнопок на форме позволяет указать кнопку, которая будет запущена, когда вызовется триггер для данного элемента.

Создание собственных AJAX страниц

Если для форм в большинстве случаев можно пользоваться #ajax[‘callback’] и не создавать дополнительных страниц, то для ссылок, работающих через ajax, вам придётся создавать собственные страницы, которые получают ajax данные и возвращают клиенту ответ. Страницы по-прежнему создаются в hook_menu(), однако есть несколько особенностей, на которые стоит обратить внимание:

/**
 * Implements hook_menu().
 */
function hook_menu() {
  $items['module_ajax_path/%'] = array(
    'page callback' => 'module_page_callback',
    'page arguments' => array(1),
    'access callback' => 'module_access_callback',
    'access arguments' => array(1),
    'delivery callback' => 'ajax_deliver',
    'theme callback' => 'ajax_base_page_theme',
    'type' => MENU_CALLBACK,
  );
}

Page callback должен возвращать массив с командами AJAX Framework’а. О них я расскажу чуть позже. В работе page arguments, access callback и access arguments никаких отличий нет – работать с ними надо продолжать согласно правилам создания страниц в Друпале. Теперь подходим к самому интересному: delivery callback. Это нововведение появилось в седьмом Друпале. После получения результата из page callback данные попадают в функцию, описанную в delivery callback. Для обычной загрузки страниц в качестве delivery callback’a вызывается drupal_deliver_html_page(). В зависимости от результата page callback’a эта функция выставляет правильные хедеры, выполняет финальные инструкции и рендерит страницу. То есть по сути, это своего рода конечная функция, которая выполняет общие для всех страниц действия.

Однако для аякса эти действия, совершаемые функцией drupal_deliver_html_page() не подходят – там работа с совершенно другим форматом данных. Поэтому в качестве delivery callback’a указывается функция ajax_deliver(). Она также выставляет правильные хедеры страницы, но уже для ответа в формате JSON. После чего подготавливается ответ от сервера (например, для результата MENU_NOT_FOUND добавляется команда, которая покажет пользователю alert() с текстом, что у него нет доступа к этой странице) в функции ajax_prepare_response().

После подготовки ответа запускается его рендер (ajax_render()). Работа с ajax в седьмом Друпале не поднимает систему темизации Друпала. Поэтому первое, что делает рендер – это подгружает css и js для страницы. Кстати, для указания темы, из которой брать css и js и необходимо указывать theme callback в меню – ведь сайт может использовать несколько тем, а их css и js могут конфликтовать между собой. Поэтому для того, чтобы ajax при ответе от сервера загружал те же скрипты и стили, которые были использованы при загрузке текущей страницы, в theme callback указывается ajax_base_page_theme(). Итак, после загрузки скриптов и стилей, напоследок вызывается хук hook_ajax_render_alter(), которому передаются все команды AJAX Framework’a, готовые для запуска. Далее команды переводятся в формат JSON и возвращаются на сторону клиента.

AJAX Framework Commands

Любая команда AJAX Framework’a двух частей – клиенской и серверной.

Серверная часть выглядит как небольшой массив, содержащий в себе данные, которые будут переданы в формате JSON на сторону клиента. Он содержит в себе параметр ‘command’, который в дальнейшем будет вызван как javascript функция с передачей ему остальных параметров этого массива.

Клиентская часть выглядит как кусок js кода, являющийся частью массива Drupal.ajax.prototype.commands, с названием функции равной названию команды на серверной части.

Описание ajax команд:

  • ajax_command_after – вставляет html код после указанного селектора
  • ajax_command_alert – Вызывает обычную javascript alert() с текстом, переданным в качестве параметра
  • ajax_command_append – Добавляет html кода в конец указанного селектора
  • ajax_command_before – Добавляет html код перед указанным селектором
  • ajax_command_changed – Помечает указанный селектор классом ajax-changed
  • ajax_command_css – Меняет свойства стилей у указанного селектора
  • ajax_command_data – Добавляет или меняет атрибуты вида name = value у указанного селектора
  • ajax_command_html - Вставляет html код в указанный селектор
  • ajax_command_insert - Вставляет html код в указанный селектор
  • ajax_command_invoke – Самая интересная функция. Позволяет запустить любую функцию для указанного селектора с указанными настройками. Позволяет заменить больше половины функций ajax-framework’a 
  • ajax_command_prepend – Добавляет html код в начало указанного селектора
  • ajax_command_remove – Удаляет указанный селектор из DOMa
  • ajax_command_replace – Заменяет указанный селектор данными, переданными с сервера
  • ajax_command_restripe – Обновляет классы odd и even (зебра) у таблиц с указанным селектором. Полезна в применении после добавления или удаления строк из таблицы
  • ajax_command_settings – Передаёт данные в глобальный массив настроек Друпала Drupal.settings.

Создание собственной AJAX команды

Для начала пишем клиентскую часть и помещаем в файл с расширением .js:

(function($) {
 
    Drupal.ajax.prototype.commands.page_reload = function(ajax, response, status) {
      location.reload(true);
    }
 
}(jQuery));

После этого при необходимости использования команды подключите этот файл через drupal_add_js().

После этого на PHP пишем функцию-обёртку для нашей команды:

function mymodule_ajax_command_page_reload() {
   return array(
    'command' => 'page_reload',
  );
}

И используем эту команду при необходимости:

$commands = array();
$commands[] = mymodule_ajax_command_page_reload();
return array('#type' => 'ajax', '#commands' => $commands);

Это CTOOLS, детка!

Говоря об Ajax просто невозможно не упомянуть модуль Chaos Tools. Да да, этот большой модуль, который приходится ставить, чтобы включить Views или Panels. И это не случайно. Он действительно хранит в себе большое количество готовых решений, в том числе и по работе с AJAX’ом. Например, он несколько расширяет список ajax команд ядра:

  • ctools_ajax_command_attr() – меняет или добавляет атрибуты у указанного селектора
  • ctools_ajax_command_redirect() – перенаправляет пользователя на указанный урл
  • ctools_ajax_command_reload() – перезагружает страницу
  • ctools_ajax_command_submit() – сабмитит указанную форму

Естественно, это не все достоинства CTools. Он предоставляет механизмы по работе с мультистеп формами, а так же с модальными окнами. Для того чтобы увидеть его в действии достаточно скачать его и включить модуль ctools_ajax_sample. На странице /ctools_ajax_sample вы найдёте несколько занимательных примеров по работе с ctools.

Послесловие

Конечно, текстом я привёл далеко не полное содержание доклада, а лишь его первоначальный план. Чтобы узнать чуть больше об AJAX - посмотрите видео.

Кстати, я специально к докладу писал модуль по демонстрации работы AJAX в Drupal 7. Вот его исходники. Демонстрация работы - на видео или тут.

Добавлено 11.09.2013

Дмитрий сделал полезное дополнение в данную статью:

Допустим мы создаем собственную команду, и выполняем ее после отправки формы, если нам необходимо выполнить какой-либо код непосредственно перед отправкой аякс запроса, сделать это можно следующим образом:

Drupal.behaviors.module_name = {
  attach: function (context, settings) {
    Drupal.ajax['submit_button_id'].options.beforeSubmit = function(form_values, element, options) {
	  // your code
    }  
  }
};

submit_button_id - можно посмотреть например так:

console.log(settings.ajax);

Кроме метода beforeSubmit, еще доступны complete, success иbeforeSerialize.

Комментарии

Аватар пользователя bigferumdron
bigferumdron написал:

Привет! Спасибо за твой замечательный блог! выручает. Поглядел видео, но казалось бы элементарную задачу решить не получается. Не могу расставить все по полочкам в своей голове.

Задача: нужно обновлять определенный элемент на странице при каждой ее загрузке. В моем случае это количество просмотров на закэшированых страницах.

Какова последовательность действий, когда нет ссылки а просто нужно обновлять элемент?
Подправьте и дополните пожалуйста.

1. Сперва создать элемент с уник. классом, где будет отображатся кол-во просмотров. Например 15
2. В hook_menu наверное нужно определить путь, по которому определить page_calback которая будет лезть в базу и возвращать количество просмотров для данной ноды. Или возможно как-то по-другому, без собственного пути? может как-то реально через ajax-calback

3. Каким-то образом, сделать чтобы при загрузке страницы, послался запрос на нужную страницу и вернулся результат.. Но как это сделать в рамках идеологии друпал?

Пожалуйста, найдите время для своего читателя, думаю другим людям также будет полезна эта информация. Спасибо.

15.05.2012 21:47
Аватар пользователя Spleshka
Spleshka написал:

Загрузка страницы не имеет ничего общего с аяксом. Если хотите обновлять данные каждый раз при загрузке страницы - уберите их кэширование (иначе смысл кэшировать то, что постоянно обновляется?). Поэтому моё мнение - Ajax здесь вообще лишний.

16.05.2012 01:03
Аватар пользователя bigferumdron
bigferumdron написал:

И что же получится, что невозможно использовать кэш страницы, в которой делается сотни запросов к базе, только потому, что нужно отображать реальное количество просмотров? Нет, это неверно. В таком случае, самый правильный выход - это кэшировать страницу, и при этом, аяксом каждый раз вытаскивать количество просмотров.
Вот и спрашиваю, как это сделать.

16.05.2012 01:57
Аватар пользователя bigferumdron
bigferumdron написал:

Если имеется возможность из всех страницы не кэшировать только количество просмотров, это очень круто, а есть ли такая возможность?

16.05.2012 01:59
Аватар пользователя Spleshka
Spleshka написал:

Так а что вам мешает вывести количество просмотров вручную?

$statistics = statistics_get($node->nid);
print $statistics['totalcount'];

Вот и всё. Вынесите это, например, на уровень темы - и это значение не будет кэшироваться. У меня в блоге статистика так и работает. А она, как вы могли заметить, обновляется постоянно даже для анонимов.

16.05.2012 13:15
Аватар пользователя bigferumdron
bigferumdron написал:

Хм, то что нужно спасибо! Походу нужно разбираться с кэщированием. Т.к. непонятно почему оно не будет кэшироваться.

16.05.2012 13:56
Аватар пользователя bigferumdron
bigferumdron написал:

А что значит вынести на уровень темы? я вывел в node.tpl.php - все равно кэшируется.

16.05.2012 14:09
Аватар пользователя nabusi
nabusi написал:

спасибо за лекцию :)

23.06.2012 22:19
Аватар пользователя alexweb
alexweb написал:

Доброго времени суток.
Спасибо за такое подробное описание ajax, но возникла проблема, в вашем модуле "из коробки" не всё срабатывает. В частности не работает ссылка на ноду в попапе. Что-то нужно доустанавливать?
Установил друпал 7,15 + ctools 7.12 + ваш тестовый модуль.
Буду благодарен если подталкнёте в нужном напрвлении.

28.08.2012 16:42
Аватар пользователя sergeybelya
sergeybelya написал:

Как правильнее всего загрузить на страницу форму через ajax, которую затем можно будет обрабатывать тоже через ajax? Т.е. как вместе с html формы подгрузить еще и необходимый js-код?

22.05.2013 14:11
Аватар пользователя Дмитрий
Дмитрий написал:

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

Drupal.behaviors.module_name = {
  attach: function (context, settings) {
    Drupal.ajax['submit_button_id'].options.beforeSubmit = function(form_values, element, options) {
	  // your code
    }  
  }
};

submit_button_id - можно посмотреть например здесь - console.log(settings.ajax);

Кроме метода beforeSubmit, еще доступны complete, success, beforeSerialize

Для поиска данного решения понадобилось некоторое время, поэтому захотелось его где-то сохранить :)

11.09.2013 01:20
Аватар пользователя Spleshka
Spleshka написал:

Спасибо, дополнил статью!

11.09.2013 15:42
Аватар пользователя Игорь777
Игорь777 написал:

+1

10.02.2014 15:15
Аватар пользователя Виталий Янчук
Виталий Янчук написал:

Очень хорошая и доступная статья! Скажу больше, дополняйте еще свои статьи картинками, я вот подсел на блог https://shneider-host.ru/blog/animaciya-zagruzki-stranicy-v-drupal-7.html изучаю сейчас анимацию загрузки страницы. Визуально так лучше запоминается. А все равно спасибо)))

22.01.2016 17:34
Аватар пользователя postgres
postgres написал:

К посту №11

А если мы хотим просто выполнить нажатие кнопки к которой приаттачен ajax запрос, то сделать это можно так:

Drupal.behaviors.module_name = {
  attach: function (context, settings) {
     if (!($('#my-form').hasClass('wasinit'))){
         $('#my-form').addClass('wasinit')
         Drupal.ajax['submit_button_id'].eventResponse(now, {});
    }
 
  }
};

Удивительно, но вопрос "как автоматически нажать кнопку, на которую подвешен ajax, сразу после загрузки формы внятного ответа на просторах инета нет.
Потратил целый и только пост №11 навел на мысль.
Спасибо.

06.05.2016 17:15
Аватар пользователя postgres
postgres написал:

ну и до кучи - ошибочку поправить:
now - это
now = Drupal.ajax['submit_button_id'];

в две строки надо писать тогда:
now = Drupal.ajax['submit_button_id'];
now.eventResponse(now, {});

06.05.2016 17:19

Комментировать