RxJS: реактивное расширение для фронтенд разработки

Про реактивное программирование уже написаны сотни статей. Фронтенд не смог избежать этого тренда, но интерес к теме до сих пор очень и очень высок. Поэтому мы просто не могли не взять интервью у одного из наших будущих докладчиков.
Итак, прошу любить и жаловать, Виктор Русакович. Родом из Минска, работает в компании GP Software.travel.
Виктор последние пять лет занимается (в основном) фронт-енд разработкой. Ну, а начинал, как и большинство из нас, с jQuery.
Потом был backbone, angular v1. Последние полгода он работает в проекте на Redux/React.js (часто их путают с RxJS, но это немного другое).
ae942edd4e494c6d95dcab098ccd23b5.jpg

В твоем проекте активно используется реактив. Можешь объяснить, что это такое и с чего это движение началось? Сейчас у всех на слуху RxJS, RxJava, RxPython, RxBasic, ну и разве что RxBrainfuck нет.

Действительно, один из моих предыдущих проектов был насквозь пронизан использованием библиотеки RxJS. Все AJAX-запросы, работа с DOM-событиями, порой просто обработка статичных данных — всё это проходило через RxJS.

Для начала пара слов о «реактивном программировании» как таковом. Например, в жизни с «реактивностью» вы сталкиваетесь в Excel:
823c27d2115a4da0af5e74fe6280bde4.jpg

Как видите, для автоматического вычисления суммы были созданы ячейки с формулами. На языке реактивного программирования это можно изобразить в виде двух потоков данных, gross и taxes, и третьего потока net, который будет собирать данные из net и gross и аггрегировать их по нашей формуле, превращая в итоговое значение.
Я буду пользоваться очень удобным онлайн-редактором, чтобы показывать все свои примеры. Этот редактор хорош тем, что прямо в брузере рендерит результат работы приложения. Ну и самая классная фича радактора в том, что потом все сниппеты останутся доступными по прямой ссылке. Мой первый пример тут.

var gross = Rx.Observable.just(5000)
var taxes = Rx.Observable.just(13)
var net = gross.combineLatest(taxes, (g, t) => g - t / 100 * g)

net.subscribe(money => document.getElementById('out').value = money)

В интернете можно найти много вариантов определения «реактивного программирования»: в Википедии, здесь, на Хабре. Тема серьёзная, даже существует специальный манифест, который можно подписать, если вы согласны с его идеями — только зачем? Лично мне все эти определения не нравятся, и я придумал свое:

Реактивное программирование — это когда ты вместо обработки событий по одному объединяешь их в поток и затем работаешь уже только с ним.
(Дата, Подпись, Печать).

4f5a3151ad83416bb45a42432d7f871b.png

Если же говорить о том, как все это зародилось, то история появления RxJS такая.
Году в 2010–2011 ребята из Microsoft, которые работали c .NET, решили, что неплохо было бы и для JS сделать реактивную библиотеку. Дело в том, что в .NET уже достаточно давно был популярен LINQ. Например, вот так с помощью LINQ можно подсчитать количество вхождений определенного слова в строке.

       string searchTerm = "data";
       //Convert the string into an array of words
        string[] source = text.Split(new char[] { '.', '?', '!', ' ', ';', ':', ',' }, StringSplitOptions.RemoveEmptyEntries);
        // Create the query.  Use ToLowerInvariant to match "data" and "Data" 
        var matchQuery = from word in source
                         where word.ToLowerInvariant() == searchTerm.ToLowerInvariant()
                         select word;
        // Count the matches, which executes the query.
        int wordCount = matchQuery.Count();

Нам посчастливлось начать работать с самой первой версии библиотеки. Код тогда ещё был не в GitHub, а в каком-то собственном репозитории от Microsoft. Кроме того, особенности лицензии не позволяли включать в проект неминифицированную версию. Огромной проблемой была документация — приходилось читать статьи, написанные для .NET, и пытаться понимать на уровне концепций, невзирая на различия в языках. Именно тогда я понял, что могу писать на любом языке программирования. :)

Как вообще вы пришли к идее использовать RxJS?

В каждом проекте есть свои особенности, которые влияют на выбор той или иной технологии. Решение может быть обсуловленно сложностью интерфейса, наличием или отсутствием внятного Roadmap«a, который показывает, что проект будет развиваться и усложняться как минимум несколько лет. А ещё в 2012 мы не смогли найти ничего другого, похожего на RxJS. Нам была важна хорошая техническая поддержка, и мы поддались бренду Microsoft.

Зачем же нам понадобился RxJS в тот момент?
Представьте себе приложение, в котором много независимых UI компонентов.
Теперь добавляем сюда штук 10 API запросов (от авторизации до проверки цены продукта). Так, есть.
Затем группируем компонеты во View так, что некоторые используются в разных View, а некоторые — нет. Хорошо получается.
Теперь нам надо научить компоненты менять свое состояние между запросами. Где-то показать слой со спиннером, где-то спрятать данные, когда вернется ошибочный запрос. Причём, компонент может зависить от нескольких API и, более того, иногда имеет значение не одиночный запрос, а серия запросов и их ответы. И вишенку на наш торт — на улице 2012 год, Angular нету, Backbone не подходит — значит, будем работать только с jQuery. Без фреймворка.

RxJS подошел идеально. Мы перестали ловить отдельные события и строить огромные пирамиды callback«ов. Что такое пирамида из callback объяснять не нужно, надеюсь? :)
Вместо этого между компонентами у нас появились потоки с данными, из которых мы могли создавать новые потоки, нужные в конкретной ситуации. Например, есть у нас поток с выбором какого-то продукта. Если пользователь добавляет что-то в корзину, то в этом потоке появляется событие с добавленным продуктом. Если пользователь удаляет продукт — в потоке появляется событие с пустым массивом. Допустим, мы хотим, чтобы таблица моргала красным каждый раз, когда удаляется что-то из корзины:

var cart = require(‘streams/cart’)
var removalsFromCart = cart
  .bufferWithCount(2,1)  
  .where(history => history[1].length < history[0].length)

removalsFromCart.subscribe(() => $(‘table’).setClassFor1Sec(‘warning))



Ссылка на сниппет.

.bufferWithCount () будет брать предыдущее и текущее события и передавать их массивом дальше. В .where () мы проверяем, что текущая корзина (она будет вторым в массиве после .bufferWithCount) содержит меньше элементов, чем предыдущая. Если условие правдивое, то данные передаются дальше в метод .subscribe (), попасть в который является целью каждого события в потоке. Если б не использование метода .where (), то каждое изменение корзины вызывало бы моргание.

Значит, причины были именно такие. Как ты считаешь, а по каким еще причинам имеет смысл начинать использовать реактивный подход в проектах?

В самом начале мы все-все события превращали в Rx.Observable. Со временем мы поняли, что это не всегда оправдано. Например, если мы на 100% уверены, что клик по этой кнопке вызывает перезагрузку страницы, то никаких дополнительных изменений на странице не потребуется. Теперь такие простые и не связанные с другими события мы обрабатываем без RxJS.

Но как только вам надо следить за многими событиями, тут без RxJS не обойтись.
Например, это могут быть вебсокеты, создающие огромные потоки данных.
Это могут быть анимации, которые начинаются при получении одного запроса, а при окончании должны запустить следующий запрос.

Но вот за что мы особенно полюбили RxJS, так это за то, что при написании всех этих .where (), .buffer (), .map () и пр. методов нам совершенно не важно, что является источником данных. Это может быть массив или же вебсокет, который может прислать по 100 объектов в секунду, а потом вдруг остановиться на минуту. Все эти события равны для нас и обрабатываются одинаково.

А это значит, что, разобравшись с асинхронными вещами, мы можем начать использовать RxJS для обработки абсолютно синхронных данных, например, пометить жирным все внешние ссылки:

Rx.Observable.from(document.querySelectorAll('a'))
  .where(el => el.getAttribute('href').indexOf('http') === 0)
  .subscribe(el => el.style.fontWeight = 'bold')

Ссылка на сниппет.
Резюмируя, скажу, что, как мне кажется, RxJS идеально подойдёт для проектов с большими потоками данных, на которые нужно реагировать — игры, чаты и простые сайты, в которых есть какие-то сложные обновления на странице. Не зря, думаю, RxJS включили в Angular 2.

А какие еще решения есть для JS?

Сходу вспоминается несколько альтернатив. В первую очередь это BaconJS. Библиотека создана примерно в то же самое время, когда и RxJS. Что интересно, в одном из проектов мы даже пытались выбрать между RxJS и Bacon — это было года 4 назад, когда обе библиотеки только-только вышли. Но тогда выбор склонился в сторону RxJS, так как Bacon проигрывал по количеству стандартных методов и более простому API в целом. Ну, и еще одним немаловажным фактором, как я уже сказал, было то, что развитием и поддержкой занимался лишь Juha Paananen, а за RxJS стояла Microsoft. Сегодня пользоваться BaconJS можно без каких-либо опасений, потому что хорошее сообщество уже сформировалось, API хорошо задокументировано, и можно найти много отличных примеров.

Следующая альтернатива — это KefirJS (в языке еще остались слова, к которым не добавили JS? :). Замечательная библиотека для реактивного программированния, поддерживаемая нашим соотечественником Романом Поминовым. Роман создавал KefirJS, стараясь взять простоту API от BaconJS (по сравнению с RxJS) и сразу исправляя ошибки в производительности. И знаете, получилось хорошо! Мы, например, пользуемся кефиром каждый день в одном из проектов.

Неужели всё так радужно? Можо просто взять и использовать эти фреймворки?

Есть нюансы.
Года 3 назад после доклада про RxJS на конференции у меня спросили: «Правильно ли я понял, что для того, чтобы использовать реактивное программирование, нужно изменить образ мышления?» Это был самый правильный вопрос за все мои доклады! Очень часто он читался в глазах слушателей, но задали его только раз. И ответ на него: «Да!»

Реактивный подход очень отличается от того, как мы привыкли писать программы. Теперь надо сначала написать код, который будет описывать пути прохождения событий, а уже потом запускать эти события в программу, где они автоматически будут вызывать необходимые действия.
Это первая проблема.

Вторая проблема состоит в том, что порой сложно понять, какой именно метод можно применить в конкретном случае. Например, вы превратили какие-то скалярные данные в запросы. Как теперь получить доступ к ответу сервера? Допустим, вы прочитали описание всех методов в RxJS и даже запомнили .switch (): «Transforms an observable sequence of observable sequences into an observable sequence producing values only from the most recent observable sequence»…
Ну вы поняли — без jsfiddle не разобраться.

С этим сложно бороться, надо будет пережить момент, пока знаний API недостаточно для безостановочного кодинга. Тут пригодится проект rxmarbles.com — его автор (André Staltz) создал большое количество динамичных временных диаграмм, по которым можно проследить, как именно влияет порядок данных на итоговый поток. Однако, метода .switch () там не нашлось. :(

К счастью есть reactivex.io/documentation/operators/switch.html, где можно найти подробное и человеческое описание основных методов и их временных диаграмм, и switch () в том числе:
53197d4bc5854d9597f6031e841f8170.png

Видно, что метод .switch () возвращает данные из вложенных потоков. Причем, что важно, данные из вложенного потока поступают на выход до тех пор, пока не появляется новый вложенный поток, который скоро начнет производить данные: обратите внимание на то, что желтый круг не прошел, хотя желтый треугольник еще не появился.
Именно этот метод мы обычно используем, когда работаем с ajax — как только отправлен новый запрос, данные, которые могут вернуться из предыдущего запроса, нам не интересны. Теперь мы никогда не увидим устаревший ответ сервера.

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

Сегодня ресурсов очень много. Только ресурсов со ссылками на ресурсы десятки, на все вкусы: кто-то любит видео-курсы, кто-то — чтение документации с примерами, кому-то книги подавай. Я бы начал вот с этого списка:

  • egghead.io/series/introduction-to-reactive-programming — отличный вводный курс от автора RxMarbles;
  • reactivex.io/intro.html — много теории от авторов RxJS;
  • www.reactivemanifesto.org — манифест (куда без него?);
  • xgrommx.github.io/rx-book — много документации из репозитория RxJS, но есть и много своих хороших статей, например про Backpresure в RxJS (а это, между прочим, часть манифеста!);
  • www.introtorx.com — сборник статей, которые вошли в одноименную книгу (на сайте есть бесплатная mobi версия для Kindle). Не пугайтесь, что всё на .NET — отличное чтение, чтобы понять концепцию. Да и .NET очень похож на TypeScript;
  • Статьи по тегу FRP на Хабре;
  • А начать я советую с подключения в проект rx.lite.js и переводе ваших Ajax (Fetch) запросов на Rx.Observable.


Надеюсь, что в вас пробудилось желание выбросить императивный подход и начать наконец следовать реактивной парадигме! Хватит тянуть данные! Пусть данные сами пройдут по созданым цепочкам!
Кстати, доклад для holyjs.ru уже готов, остались небольшие доработки. В докладе я расскажу на примерах о практическом применении RxJS в ваших проектах. Так что увидимся в Петербурге на конференции 5-ого июня!

© Habrahabr.ru