разработчикам11 марта 2020

Как фильтровать сложные данные с использованием JavaScript / TypeScript

Дмитрий ПереверзаСтарший фулстек-разработчик

Часто ли вам приходилось писать обработчики фильтрации для ваших данных? Это могут быть массивы для отрисовки таблиц, карточек, списков — чего угодно.

Когда фильтрация статическая, то тут все просто. Стандартных функций map, filter и reduce вполне достаточно. Но что делать, если данные имеют сложную структуру или вложенность, да еще и правил для фильтра может быть достаточно много. Правила могут повторяться, данные изменяться, и чем больше контролов фильтра будет появляться, тем сложнее и неустойчивее будет код обработчика.

Как же решить проблему возрастающей сложности?

Я сам столкнулся с данной проблемой, разрабатывая приложение, которое работает с огромным количеством данных. Постепенно добавлялись все новые и новые фильтры.

Поначалу все шло хорошо, отдельные обработчики фильтров выполняли свои задачи отлично. Со временем стали появляться страницы с незначительными изменениями в уже готовых фильтрах. Код дублировать не хотелось, так что пришлось выделять особенные варианты фильтра и его обработчика. Типовая страница собиралась за час, но вот обработчики для фильтров писались в два раза дольше. Нужно было что-то делать.

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

  1. ng-table — отрисовывает таблицы и предоставляет возможность простой фильтрации и сортировки. Фильтрации у нас были намного сложнее.
  2. List.js — как ng-table, только намного меньше функционала.
  3. filter.js — близко к тому, что было нужно, но не хватало гибкости.
  4. Isotope — привязывается к DOM элементам. У нас же просто данные.

Нужно было разработать собственное решение, удовлетворяющее следующим требованиям:

  1. Декларативное описание работы фильтра.
  2. Переиспользуемые правила.
  3. Возможность написания своих правил фильтрации.
  4. Композиция правил как в обычных условиях, с помощью and и or операторов.
  5. Фильтрация вложенных элементов и групп. Вложенность не фиксированная.
  6. Возможность задать стратегию фильтрации (к примеру, мы хотим, чтобы, если группа совпала с фильтром, но не совпал ни один из её элементов, все равно вывелась группа со всеми элементами)

В итоге мы имеем библиотеку awesome-data-filter.

Данная библиотека, используя декларативный подход, позволяет составлять сложные правила обработки ваших данных.

Установка

Сначала поставим библиотеку и опробуем ее в действии.

Начнем с простого примера. Предположим, у нас есть следующий массив пользователей:

И объект со значениями фильтра:

Допустим, нужно найти пересечение этих правил.

Используемые правила:

  • matchText — поиск подстроки в целевом поле;
  • equalProp — полное совпадение значений параметров;
    • betweenDates — проверяет вхождение определенной даты в диапазон;
    • equalOneOf — хотя бы один из переданных элементов должен соответствовать переданному правилу;
    • someInArray — хотя бы один из вложенных элементов объекта должен соответствовать переданному правилу;
    • isEmptyArray — проверка на пустой массив;
    • lessThen — значение меньше, чем;
    • moreThen — значение больше, чем;
    • not — функция отрицания возвращаемого функцией значения.

В наших примерах мы будем использовать только matchText и equalProp.

Для получения динамических значений:

  • filterField — получение свойства фильтра;
  • elementField — получение свойства текущего элемента списка.

Полученная функция filter принимает объект со значениями фильтра и фильтруемые данные в формате groups и elements.

Так как группы обрабатываются отдельно от элементов, они вынесены в отдельное поле. Внутри групп также могут находиться элементы.

В данном случае, так как у нас плоский список элементов, передаем только elements.

Если же заменим and на or, то получим объединение результатов работы 2х правил.

Благодаря функциям filterField, elementField мы можем динамически передавать параметры в созданные правила.

Так же есть функция constValue для передачи константных значений.Условия могут вкладываться друг в друга or(..., matchText, [and([..., matchText, ...]), or([..., ...])])

Также фильтр может работать со вложенными элементами и группами. Рассмотрим на примере ниже:

В таком случае можно передать в конфиг фильтра информацию об обходе данной структуры объекта в поле traversal:

До этого момента передавался только elementFilter параметр, который отвечает за правила фильтрации элементов. Также есть groupFilter для групп.

Группа с названием first group появилась в выборке, и так как фильтр не нашел совпадения по элементам, но нашел совпадение по группе, мы видим отображение всех вложенных элементов списка.В случае с fifth group совпадение было и по элементам, и по группе, поэтому оставляем только один элемент.

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

Если стандартная стратегия не подходит для вашего случая, можно написать свою и передать ее в поле фильтра filterStrategy.

Благодаря использованию библиотеки awesome-data-filter можно решить проблему со сложностью обработчиков фильтров и улучшить читабельность кода. Теперь можно визуально сравнить графики сложности обоих подходов.

Если на вашем проекте не используется много фильтров и проект сам по себе небольшой, то вам не нужна эта библиотека. Остальным же можно пробовать внедрять ее постепенно.