Drupal: ajax_facets та history API

Напевно, кожен веб-розробник стикався з необхідністю реалізації пошуку на сайті. Досить поширене рішення — Apache Solr. У світі Drupal розробки це не виняток. Для інтеграції Solr з Drupal та реалізації фасетного пошуку існують модулі search_api, search_api_solr і facetapi. Але в більшості випадків нам би хотілося, щоб результати пошуку і фасетные фільтри оновлювалися без перезавантаження сторінки, тобто ajax'ом. І, як зазвичай у світі Drupal, на d.org знайдеться який-небудь перевірений часом і користувачами модуль (а може і не перевірений, як пощастить), який робить те, що нам потрібно. В даному випадку це ajax_facets.

Ajax facets — модуль, що надає кілька типів «віджетів», які можуть використовуватися у фільтрах фасетного пошуку. Це «range slider», «multiple checkboxes», «selectbox» і «links». При зміні значень в цих «віджети» фільтри і результат пошуку оновлюються ajax'ом. Здорово. Але було б ще краще, якби модуль дружив з history API. Тобто зберігав би кожне стан фільтру в історії, що дозволило б користувачам ходити по історії пошуку кнопки «назад» і «вперед» в браузері, знову ж таки, без перезавантаження сторінки.

Завдання

Звичайно, потреба в цій фиче і інтерес в реалізації виник не сам по собі. На одному з проектів була поставлена задача подружити ajax_facets з history API. Про що я і хочу розповісти.

Рішення

Як це зазвичай буває, вирішення проблеми починається з пошуку готового рішення чи принаймні патча. Готового рішення не знайшлося, зате був знайдений патч. Судячи за описом на issue трекері проекту він робив саме те, що потрібно. Але, на жаль, патч був старий і придатний тільки для старої гілки модуля (7.x-2.x). Ідея дуже проста: зберегти в історію браузера поточний стан фільтрів в той момент, коли ajax_facets отримує успішний відповідь від сервера на оновлення результату пошуку і самих фільтрів. А по натисненню кнопки «назад» і «вперед» діставати збережений стан фільтрів з історії і відсилати запит на оновлення фільтрів і результатів пошуку з параметрами з збереженого стану.

Для перевірки працездатності ідеї як такої я «портувати» знайдений патч в актуальну гілку модуля (7.x-3.x). Все працювало. Однак вимагав доопрацювання. А саме хотілося б щоб ця фіча працювала і в старих, не підтримують history API, браузерах. Завдання нескладне. history.js, який емулює history API. З іншого боку не хотілося додавати жорстку залежність на цю бібліотеку, так як це означало б додавання в залежності модуля libraries. Такий патч точно ніхто не прийняв. Уявіть, Ви оновлюєте ajax_facets модуль, а в залежностях у нього з'явився libraries, який Вам і не потрібен. Та й сама підтримка старих браузерів у вигляді history.js Вам теж не потрібна (просто не підтримуєте старі браузери, наприклад). Щоб уникнути таких ситуацій, вирішив зробити все трохи гнучкіше:

  1. На стороні сервера перевіряємо наявність libraries модуля і бібліотеки history.js. Якщо залежності знайдені, то передаємо на front-end бік прапор «history.js доступна, можна використовувати history API».
  2. На стороні клієнта перевіряємо, чи підтримує браузер history API (нативно або через history.js). Якщо так, то робимо все як годиться. Інакше отримуємо поведінку ajax_facets (як і було до патча).


Реалізація

Перший пункт досягається наступним чином:
Видаємо підказки «Status report» сторінці, якщо залежності не знайдені.
/**
* Implements hook_requirements().
*/
function ajax_facets_requirements($phase) {
$вимога = array();
$t = get_t();

switch ($phase) {
case 'runtime':
$description = $t('For now browser ajax history feature works only in HTML5 browsers. If you want to get this feature on HTML4 browsers you need to install libraries module and download history.js library.');
$value = $t('Libraries module not installed.');

if (module_exists('libraries')) {
if (!libraries_get_path('history.js')) {
$description = $t('For now browser ajax history feature works only in HTML5 browsers. If you want to get this feature on HTML4 browsers you need to download history.js library.');
$value = $t('Library history.js not found.');
}
else {
$description = $t('For now browser ajax history feature works both in HTML4 and HTML5 browsers.');
$value = $t('Works with history.js library');
}
}

$requirements['ajax_facets_message'] = array(
'title' => $t('Ajax Facets'),
'description' => $description,
'value' => $value,
'severity' => REQUIREMENT_INFO,
);
break;
}

return $вимога;
}


І пробрасываем прапор на front-end сторону у разі якщо history.js бібліотека знайдена.
/**
* Add required JS and handle single inclusion.
*/
function ajax_facets_add_ajax_js($facet) {
static $included = FALSE;
if (!$included) {

...

// Add history.js file if exists.
if (module_exists('libraries')) {
$history_js_path = libraries_get_path('history.js');

if ($history_js_path) {
$history_js_exists = TRUE;
drupal_add_js($history_js_path . '/scripts/bundled/html4+html5/jquery.history.js', array('group' => JS_LIBRARY));
}
}

...

$facet = $facet->getFacet();
$setting['facetapi'] = array(

....

'isHistoryJsExists' => $history_js_exists,
);
drupal_add_js($setting, 'setting');
drupal_add_library('system', 'drupal.ajax');
}
}


Реалізація другого пункту показана на прикладі функції-обгортки pushState:
/**
* Pushes new state to browser history.
*
* History.js library fires "statechange" event even on API push/replace calls.
* So before pushing new state to history we should unbind from this event and after bind again.
*/
Drupal.ajax_facets.pushState = function (state, title, stateUrl) {
// If history.js available - use it.
if (Drupal.settings.facetapi.isHistoryJsExists) {
var $window = $(window);
$window.unbind('statechange', Drupal.ajax_facets.reactOnStateChange);
History.pushState(state, title, stateUrl);
$window.bind('statechange', Drupal.ajax_facets.reactOnStateChange);
} else {
// Fallback to HTML5 history object.
if (typeof history.pushState != 'undefined') {
history.pushState(state, title, stateUrl);
}
}
};


До речі, в history.js є одна цікава особливість, яку потрібно враховувати: подія statechange викликається при натисканні кнопок історії браузера, а так само при програмному оновленні історії, наприклад викликом History.pushState() методу. У нативної реалізації history API браузерами є подія onpopstate, яке викликається тільки при натисканні на кнопки історії браузера. Щоб уникнути зайвого спрацьовування statechange потрібно відписатись від цього події перед оновленням історії браузера, а після підписатися на нього знову.

Висновок

Не завжди вдається знайти і застосувати готове рішення. Але це дуже здорово. Це дає можливість розібратися, подивитися як модуль, який використовував раніше, працює всередині. І, зрештою, просто приємно, коли запропоноване тобою рішення комитят у популярний проект. Це значить, наступного разу у кого-небудь іншого такої проблеми вже не виникне.

Повний diff можна подивитися здесь.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.