Использование RxJs для связывания компонентов приложения

Способы «общения» компонентов


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

Первое, что приходит в голову, это публикация и подписка на именованные события. Один из компонентов посылает событие в «эфир», а остальные слушают этот «эфир» и ловят те сообщения, которые им нужны. Идея предельно простая и давно хорошо себя зарекомендовавшая.

image


Это чем-то напоминает WI-FI в кафе, когда все могут обмениваться сообщениями со всеми, но при этом есть роутер (диспетчер), который обеспечивает существование «эфира» и отдает сообщения только тем, кому они адресованы.

Такая организация позволяет, например, «бесплатно» получить слабое связывание компонентов. Недостаток ее в том, что при росте числа компонентов и соответственно числа событий становится сложно уследить за именами событий и за тем, кому какие события нужны для правильной работы. Появляются пространства имен и имена событий из чего-то типа «Событие1» превращаются в «Состояние_приложения1.Компонент2.Событие1». И что совсем невозможно делать при такой организации это компоновать события. Например требование «сделай что-то когда событие Б возникнет после двух событий A» выливается в тонну локальных переменных, хранящих последние данные из событий и счетчики самих событий.

Несколько облегчают дело promise’ы, они позволяют организовать очередность событий и практически являются первым шагом в организации потоков данных.

Другой способ организовать общение между компонентами это протянуть между ними «провода», так, чтобы связанными оказались только те компоненты, которым есть что «сказать» друг другу. Чтобы представить этот способ связывания визуально, достаточно взглянуть на любую печатную плату.

image


Здесь каждый компонент соединен с другими, ему необходимыми компонентами, дорожками («проводами») или просто «потоками», по которым передается сигнал. Теперь чтобы представить, какие данные нужны компоненту для работы, не нужно залезать в «черный ящик» и искать подписки на события. Достаточно посмотреть от каких компонентов приходят потоки и как преобразуются данные по пути. Именовать события при такой организации не нужно вовсе, скорее в имени нуждается поток, который несет данные. При этом задача компоновки событий сводится к компоновке содержащих их потоков.

Реализация потоков


Чтобы теперь перейти от слов к делу, нужно выбрать реализацию наших «проводов». Для меня такой реализацией стал RxJS, который представляет собой модульную библиотеку, позволяющую создавать и компоновать потоки данных. Подход, используемый в Rx, появился в .NET и оттуда был портирован во многие популярные языки. В зависимости от сложности реализуемой логики к проекту может подключаться как весь RxJS, и его отдельный модули. Рассмотрим ключевые понятия.

Создаваемые в Rx потоки реализуют паттерн Observable и наследуются от одноименного интерфейса, это значит каждый поток можно «слушать». Реализуется это при помощи метода subscribe, который принимает в качестве аргумента Observer.

observableStream.subscribe(someObserver)

В самом простом случае Observer это функция, которая принимает единственный аргумент – переданное сообщение из потока. Сообщение может быть как простым значением, так и сложным объектом.

function someObserver(streamEvent){ console.log('Received ' + streamEvent) }

Потоки, например, можно использовать для обработки DOM событий. Удобнее всего организовать подписку на DOM события при помощи плагина RxJS-jQuery.

Например, поток, который будет реагировать на нажатия кнопки и посылать в качестве данных DOM событие можно создать так:

var myAwesomeButtonClick = $('#my-awesome-button').onAsObservable('click')

Теперь поток myAwesomeButtonClick можно передать другому компоненту, если ему необходимо обрабатывать нажатия на кнопку. Для организации произвольного потока, используется Rx.Subject, который также реализует паттерн Observable, но дополнительно к возможностям Observable позволяет бросать в поток произвольные сообщения, для этого используется метод onNext.

subjectStream.onNext('new message')

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

image


Теперь как только в Компоненте 1 возникает событие, он вызывает onNext и Компонент 2 сразу его получает это событие и обрабатывает.
Усложняясь, каждый компонент начнет возвращать и принимать несколько потоков и превратится в подобие чипа, ножками которого являются потоки.

image


Но все это можно было организовать и при помощи promise’ов. В чем же здесь преимущество от использования потоков RxJs?

Все, что происходит в приложении, может быть представлено в виде потока данных:


  • нажатия клавиш
  • движения мыши
  • данные с сервера
  • сложное логическое что-то, которое случилось в одном из компонентов

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

Обработка событий в потоке, условия и сайд эффекты


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

var inputChanges = $('input.name').onAsObservable('change, keyup, paste')

Будет создан поток inputChanges, который реагирует на некоторые события в поле ввода и в качестве данных передает DOM событие, но как правило внутренней логике компонента нужны не DOM события, а конкретные значения, причем желательно удовлетворяющие каким-либо правилам. Создадим новый поток, который реализует обработку событий для потока inputChanges.

var inputValueChanges = inputChanges
	.map(function(event){ return $(event.target).val() })
	.distinctUntilChanged()
	.where(isValidName)
	.do(someAction)

Новый поток inputValueChanges возвращает значения в поле ввода, причем новое событие в нем возникает только если значение в поле действительно изменилось и удовлетворяет некоторому формату. Разберем подробнее:

.map(function(event){ return $(event.target).val() })

Метод map (он же select) принимает функцию обработчик, которая в качестве аргумента принимает событие и возвращает некоторое значение, которое в дальнейшем будет использовано как событие потока.

.distinctUntilChanged()

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

.where(isValidName)

Метод where (он же filter) так же как distinctUntilChanged позволяет не пропускать дальше некоторые события, но в качестве условия принимает функцию, которая проверяет, удовлетворяют ли данные события некоторым требованиям.

.do(someAction)

do (или doAction) реализует сайд эффекты, при этом никак не влияя на само событие в потоке. Функция someAction так же как в случае с map принимает единственный аргумент – событие.

Компоновка потоков


Часто бывает, что один компонент ждет данные от двух других компонентов, чтобы совершить какое-то действие. Например, чтобы показать цену за поездку компоненту «сводка» необходимо получить данные от компонентов «календарь» и «маршрут» и далее обновлять ее как только в одном из компонентов что-то изменится.

image


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

var routeAndDateChangesStream = routeChangesStream
	.combineLatest(dateChangesStream, function(route, date){ 
return { date: date, route: route } 
})

Метод combineLatest объединяет два (и более) потока в один. Событие в получившемся потоке возникает при возникновении события в одном из объединяемых потоков, при условии, что в каждом из объединяемых потоков есть хотя бы одно событие. Первым аргументом combineLatest принимает поток, с которым нужно произвести слияние, вторым функцию, которая определяет как нужно слить данные из событий. При возникновении события в любом потоке для слияния из остальных потоков берутся последние события.

image


Кроме объединения последних событий, бывает необходимо объединить события попарно по порядку возникновения. Например, какое-то действие генерирует два Ajax запроса и нужно среагировать, когда оба запроса вернут данные, причем комбинировать данные нужно только из соответствующих друг другу запросов. В такой ситуации удобно использовать метод zip, который объединяет не последние события из потоков, а события с совпадающими порядковыми номерами.

var routeAndDateChangesStream = response1Stream
	.zip(response2Stream, function(data1, data2){ 
return _.extend(data1, data2) 
})

image

И наконец ситуация когда не нужно объединять данные из потоков, а просто компонент получает одну и ту же информацию из разных потоков, например на странице есть два календаря, один подробный на год, второй на ближайшие две недели, оба календаря передают дату компоненту «сводка». Здесь нужно просто объединить два потока в один.

var dataChangesStream = bigCalendarDateChange.merge(miniCalendarDateChanges)

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

image


Тестирование


Организация компонентов по типу «Чип» дает еще одно преимущество – тестируемость.

Если компонент получает все входящие данные через набор потоков и так же через набор потоков данные отправляет, то это сильно упрощает тестирование такого компонента. Достаточно создать набор пустых входящих потоков и отдать их компоненту в качестве входящих. Далее, отправляя в потоки те или иные комбинации событий, легко увидеть правильно ли себя ведет компонент и правильные ли данные он отдает в исходящие потоки.
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 3

    0
    Спасибо за статью! В свое время пробовал играться с похожим реактивным flapjax, правда, ничего не вышло, не смог правильно скомпоновать потоки для своей задачи, а так как это была не промышленная разработка, забил на продолжение. Жду свободного времени, чтобы вернуться к экспериментам как-нибудь, т.к. на работе проекты попадаются с подходами со связыванием данных в оппозицию реактивному программированию.
      +1
      Спасибо, рад, если статья оказалась полезной. Если сравнивать RxJs и flapjax (по крайней мере на сколько я его понял, посмотрев код примеров), то последний претендует на звание полноценного фреймворка и пытается решить весь спектр задач на клиенте, а Rx это все таки библиотека, которая конкретно позволяет создавать и компоновать потоки данных. Возможно в этом есть недостаток, так как приходится подбирать для остальных задач другие инструменты, зато опыт показывает, что по мере развития проекта иногда приходится менять какие-то инструменты и когда проект собран из библиотек, это более или менее просто заменить одну другой.
        0

        Спасибо за статью.

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

        Самое читаемое