Наверное, каждый веб разработчик сталкивался с необходимостью в реализации поиска на сайте. Довольно распространенное решение — 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) {
  $requirements = 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 $requirements;
}


И пробрасываем флаг на 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 можно посмотреть здесь.
Поделиться с друзьями
-->

Комментарии (1)


  1. IlyinEugene
    12.05.2016 13:00
    +2

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