[Перевод] RefluxJS — альтернативный взгляд на Flux архитектуру от Facebook

От переводчика: посмотрев на ReactJS и вдохновившись его простотой, начал искать библиотеку, которая бы обеспечивала такой же простой обмен данными внутри моего приложения. Наткнулся на Flux, увидел примеры кода и пошел искать альтернативу. Набрел на RefluxJS, немедленно полюбил и пошел переводить официальную доку. Она написана как раз в стиле статьи, поэтому в первую очередь решил поделиться ей с Хабрасообществом :) Перевод несколько вольный. Кое-где, если мне казалось, что что-то нуждается в дополнительном пояснении или примере, я не стеснялся.В переводе ниже в качестве перевода для термина Action из Reflux иногда используется термин «событие», а иногда — термин «экшен», в зависимости от контекста. Более удачного перевода мне подобрать не удалось. Если у вас есть варианты, жду предложений в комментариях ;)

Обзорimageimageimageimageimage

RefluxJS — простая библиотека, обеспечивающая в вашем приложении однонаправленный поток данных, использующая концепцию Flux от Facebook.

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

╔═════════╗ ╔════════╗ ╔═════════════════╗ ║ Actions ║──────>║ Stores ║──────>║ View Components ║ ╚═════════╝ ╚════════╝ ╚═════════════════╝ ^ │ └──────────────────────────────────────┘ Паттерн состоит из экшенов (actions) и хранилищ данных (stores). Экшены инициируют движение данных с помощью событий через хранилища к визуальным компонентам. Если пользователь сделал что-то, с помощью экшена генерируется соответствующее событие. На это событие подписано хранилище данных. Оно обрабатывает событие и, возможно, в свою очередь генерирует какое-то свое.

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

Содержание Сравнение Reflux и Facebook Flux Примеры Установка ИспользованиеСобытия Хранилища Использование с компонентами ReactJS Детали Эпилог Сравнение Reflux и Facebook Flux Цель проекта RefluxJS — более простая и быстрая интеграция Flux архитектуры в ваш проект как на стороне клиента, так и на стороне сервера. Однако существуют некоторые различия между тем, как работает RefluxJS и тем, что предлагает классическая Flux-архитектура. Более подробная информация есть в этом блог-посте.Сходства с Flux Некоторые концепции RefluxJS сходны с Flux: Есть экшены Есть хранилища данных Данные движутся только в одном направлении. Отличия от Flux RefluxJS — улучшенная версия Flux-концепции, более динамичная и более дружелюбная к функциональному реактивному программированию: Диспетчера событий (dispatcher), который в Flux был синглтоном, в RefluxJS нет. Вместо этого каждое событие (экшен) является своим собственным диспетчером. Поскольку на экшены можно подписываться, хранилища могут это делать напрямую без использования громоздких операторов switch для отделения мух от котлет. Хранилища могут подписываться на другие хранилища. То есть, появляется возможность создавать хранилища, которые агрегируют и обрабатывают данные в стиле map-reduce. Вызов waitFor () удален. Вместо него обработка данных может производиться последовательно или параллельно.Хранилища, агрегирующие данные (см. выше) могут подписываться на другие хранилища, обрабатывая сообщения последовательно Для ожидания обработки других событий можно использовать метод join () Специальные фабрики экшенов (action creators) не нужны вовсе, поскольку экшены RefluxJS являются функциями, передающими нужные данные всем, кто на них подписался. Примеры Некоторые примеры можно найти по следующим адресам: Установка В настоящий момент RefluxJS можно установить с помощью npm или с помощью bower.

NPM Для установки с помощью npm выполните следующую команду: npm install reflux Bower Для установки с помощью bower: bower install reflux ES5 Как и React, RefluxJS требует наличия es5-shim для устаревших браузеров. Его можно взять тутИспользование Полноценный пример можно найти тут.Создаем экшены Экшены создаются с помощью вызова `Reflux.createAction ()`. В качестве параметра можно передать список опций. var statusUpdate = Reflux.createAction (options); Объект экшена является функтором, поэтому его можно вызвать, обратившись к объекту как к функции:

statusUpdate (data); // Вызываем экшен statusUpdate, передавая в качестве данных data statusUpdate.triggerAsync (data); // Тоже самое, что выше Если `options.sync` установлено в значение «истина», событие будет инициировано как синхронная операция. Эту настройку можно изменить в любой момент. Все следующие вызовы будут использовать установленное значение.

Для удобного создания большого количества экшенов можно сделать так:

var Actions = Reflux.createActions ([ «statusUpdate», «statusEdited», «statusAdded» ]);

// Теперь объект Actions содержит экшены с именами, которые мы передали в вызов createActions (). // Инициировать события можно как обычно

Actions.statusUpdate (); Асинхронная работа с экшенами Для событий, которые могут обрабатываться асинхронно (например, вызовы API) есть несколько различных вариантов работы. В самом общем случае мы рассматриваем успешное завершение обработки и ошибку. Для создания различных событий в таком варианте можно использовать `options.children`.

// Создаем экшены 'load', 'load.completed' и 'load.failed' var Actions = Reflux.createActions ({ «load»: {children: [«completed», «failed»]} });

// При получении данных от экшена 'load', асинхронно выполняем операцию, //, а затем в зависимости от результата, вызываем экшены failed или completed Actions.load.listen (function () { // По умолчанию обработчик привязан к событию. //Поэтому его дочерние элементы доступны через this someAsyncOperation () .then (this.completed) .catch (this.failed); }); Для рассмотренного случая есть специальная опция: `options.asyncResult`. Следующие определения экшенов эквивалентны:

createAction ({ children: [«progressed», «completed», «failed»] });

createAction ({ asyncResult: true, children: [«progressed»] }); Для автоматического вызова дочерних экшенов `completed` и `failed` есть следующие методы:

`promise` — В качестве параметра ожидает объект промиса и привязывает вызов `completed` и `failed` к этому промису с использованием `then ()` и `catch ()`. `listenAndPromise` — В качестве параметра ожидает функцию, которая возвращает объект промиса. Он (объект промиса, который вернула функция) будет вызван при наступлении события. Соответственно, по `then ()` и `catch ()` промиса автоматически вызваны completed и failed Следующие три определения эквивалентны: asyncResultAction.listen (function (arguments) { someAsyncOperation (arguments) .then (asyncResultAction.completed) .catch (asyncResultAction.failed); });

asyncResultAction.listen (function (arguments) { asyncResultAction.promise (someAsyncOperation (arguments)); });

asyncResultAction.listenAndPromise (someAsyncOperation); Асинхронные экшены как промисы Асинхронные экшены можно использовать как промисы. Особенно это удобно для рендеринга на сервере, когда вам требуется дождаться успешного (или нет) завершения некоторого события перед рендерингом.

Предположим, у нас есть экшен и хранилище и нам нужно выполнить API запрос:

// Создаем асинхронный экшен с `completed` & `failed` «подэкшенами» var makeRequest = Reflux.createAction ({ asyncResult: true });

var RequestStore = Reflux.createStore ({ init: function () { this.listenTo (makeRequest, 'onMakeRequest'); },

onMakeRequest: function (url) { // Предположим, что `request` — какая-то HTTP библиотека request (url, function (response) { if (response.ok) { makeRequest.completed (response.body); } else { makeRequest.failed (response.error); } }) } }); В этом случае на сервере можно использовать промисы для того, чтобы либо выполнить запрос и либо отрендерить что-то, либо вернуть ошибку:

makeRequest ('/api/something').then (function (body) { // Render the response body }).catch (function (err) { // Handle the API error object }); Хуки, доступные при обработке событий Для каждого события доступно несколько хуков.

`preEmit` — вызывается перед тем, как экшен передаст информацию о событии подписчикам. В качестве аргументов хук получает аргументы, использованнные при отправке события. Если хук вернет что-либо, отличное от undefined, возвращаемое значение будет использовано как параметры для хука `shouldEmit` и заменит собой отправленные данные `shouldEmit` — вызывается после `preEmit`, но до того, как экшен передаст информацию о событии подписчикам. По умолчанию этот обработчик возвращает true, что разрешает отправку данных. Это поведение можно переопределить, например, чтобы проверить аргументы и решить, должно ли событие быть отправлено в цепочку или нет. Пример использования:

Actions.statusUpdate.preEmit = function () { console.log (arguments); }; Actions.statusUpdate.shouldEmit = function (value) { return value > 0; };

Actions.statusUpdate (0); Actions.statusUpdate (1); // Должно быть выведено: 1 Определять хуки можно прямо при объявлении экшенов:

var action = Reflux.createAction ({ preEmit: function (){…}, shouldEmit: function (){…} }); Reflux.ActionMethods Если вам нужно, на объектах всех экшенов можно было выполнить какой-то метод, для этого вы можете расширить объект`Reflux.ActionMethods`, который автоматически подмешивается ко всем экшенам при создании.

Пример использования:

Reflux.ActionMethods.exampleMethod = function () { console.log (arguments); };

Actions.statusUpdate.exampleMethod ('arg1'); // Выведет: 'arg1' Создание хранилищ Хранилища создаются примерно так же, как и классы компонентов ReactJS (`React.createClass`) — путем передачи объекта, определяющего параметры хранилища методу `Reflux.createStore`. Все обработчики событий можно проинициализовать в методе `init` хранилища, вызывав собственный метод хранилища `listenTo`.

// Создаем хранилище var statusStore = Reflux.createStore ({

// Начальная настройка init: function () {

// Подписываемся на экшен statusUpdate this.listenTo (statusUpdate, this.output); },

// Определяем сам обработчик события, отправляемого экшеном output: function (flag) { var status = flag? 'ONLINE' : 'OFFLINE';

// Используем хранилище как источник события, передавая статус как данные this.trigger (status); }

}); В примере выше, при вызове экшена `statusUpdate`, будет вызыван метод хранилища `output` со всеми параметрами, переданными при отправке. Например, если событие было отправлено с помощью вызова `statusUpdate (true)` в функцию `output` будет передан флаг `true`. А после этого само хранилище сработает как экшен и передаст своим подписчикам в качестве данных `status`.

Поскольку хранилища сами являются инициаторами отправки событий, у них тоже есть хуки `preEmit` и`shouldEmit`.

Reflux.StoreMethods Если необходимо сделать так, чтобы определенный набор методов был доступен сразу во всех хранилищах, для этого можно расширить объект `Reflux.StoreMethods`, который подмешивается во все хранилища при их создании.

Пример использования:

Reflux.StoreMethods.exampleMethod = function () { console.log (arguments); };

statusStore.exampleMethod ('arg1'); // Будет выведено: 'arg1' Примеси (mixins) в хранилищах Точно также, как вы подмешиваете объекты в компоненты React, вы можете подмешивать их к вашим хранилищам:

var MyMixin = { foo: function () { console.log ('bar!'); } } var Store = Reflux.createStore ({ mixins: [MyMixin] }); Store.foo (); // Выведет «bar!» в консоль Методы примесей доступны точно также, как и собственные методы, объявленные в хранилищах. Поэтому `this` из любого метода будет указывать на экземпляр хранилища:

var MyMixin = { mixinMethod: function () { console.log (this.foo); } } var Store = Reflux.createStore ({ mixins: [MyMixin], foo: 'bar!', storeMethod: function () { this.mixinMethod (); // Выведет «bar!» } }); Удобно, что если в хранилище подмешано несколько примесей, определяющих одни и те же методы жизненного цикла событий (`init`, `preEmit`, `shouldEmit`), все эти методы будут гарантировано вызваны (как и в ReactJS, собственно)

Удобная подписка на большое количество экшенов Поскольку обычно в методе init хранилища выполняется подписка на все зарегистрированные экшены, у хранилищ имеется метод `listenToMany`, который принимает в качестве аргумента объект со всеми созданными событиями. Вместо вот такого кода:

var actions = Reflux.createActions ([«fireBall», «magicMissile»]);

var Store = Reflux.createStore ({ init: function () { this.listenTo (actions.fireBall, this.onFireBall); this.listenTo (actions.magicMissile, this.onMagicMissile); }, onFireBall: function (){ // whoooosh! }, onMagicMissile: function (){ // bzzzzapp! } }); … можно использовать такой:

var actions = Reflux.createActions ([«fireBall», «magicMissile»]);

var Store = Reflux.createStore ({ init: function () { this.listenToMany (actions); }, onFireBall: function (){ // whoooosh! }, onMagicMissile: function (){ // bzzzzapp! } }); Подобный код добавит обработчики для всех экшенов `actionName`, для которых есть соответствующий метод хранилища `onActionName` (или `actionName` если вам так удобнее). В примере выше, если бы объект `actions` содержал также экшен `iceShard` он просто был бы проигнорирован (поскольку для него нет соответствующего обработчика).

Свойство `listenables` Чтобы вам было еще более удобно, вы можете присвоить свойству хранилища `listenables` объект с экшенами, он он будет автоматически передан в `listenToMany`. Поэтому пример выше можно упростить до такого:

var actions = Reflux.createActions ([«fireBall», «magicMissile»]);

var Store = Reflux.createStore ({ listenables: actions, onFireBall: function (){ // whoooosh! }, onMagicMissile: function (){ // bzzzzapp! } }); Свойство `listenables` может представлять собой и массив подобных объектов. В этом случае каждый объект будет передан в `listenToMany`.Это позволяет удобно делать следующее:

var Store = Reflux.createStore ({ listenables: [require ('./darkspells'), require ('./lightspells'),{healthChange: require ('./healthstore')}], // остальной код удален для улучшения читаемости }); Подписка на хранилища (обработка событий, отправляемых хранилищами) В вашем компоненте вы можете подписаться на обработку событий от хранилища вот так:

// Хранилище данных для статуса var statusStore = Reflux.createStore ({

// Начальная настройка init: function () {

// Подписываемся на экшен statusUpdate this.listenTo (statusUpdate, this.output); },

// Обработчик output: function (flag) { var status = flag? 'ONLINE' : 'OFFLINE';

// Инициируем собственное событие this.trigger (status); } });

// Очень простой компонент, который просто выводит данные в консоль function ConsoleComponent () {

// Регистрируем обработчик протоколирования statusStore.listen (function (status) { console.log ('status: ', status); }); }; var consoleComponent = new ConsoleComponent (); Отправляем события по цепочке, используя объект экшена `statusUpdate` как функции:

statusUpdate (true); statusUpdate (false); Если сделать все, как указано выше, вывод должен получиться вот таким:

status: ONLINE status: OFFLINE Пример работы с компонентами React Подписываться на экшены в компоненте React можно в методе `componentDidMount` [lifecycle method](), а отписываться в методе `componentWillUnmount` примерно вот так:

var Status = React.createClass ({ initialize: function () { }, onStatusChange: function (status) { this.setState ({ currentStatus: status }); }, componentDidMount: function () { this.unsubscribe = statusStore.listen (this.onStatusChange); }, componentWillUnmount: function () { this.unsubscribe (); }, render: function () { // Рендеринг компонента } }); Примеси для удобной работы внутри компонентов React Поскольку в компонентах необходимо постоянно подписываться / отписываться от событий в нужные моменты, для удобства использования можно использовать примесь `Reflux.ListenerMixin`. С его использованием пример выше можно переписать так:

var Status = React.createClass ({ mixins: [Reflux.ListenerMixin], onStatusChange: function (status) { this.setState ({ currentStatus: status }); }, componentDidMount: function () { this.listenTo (statusStore, this.onStatusChange); }, render: function () { // render specifics } }); Эта примесь делает доступным для вызова внутри компонента метод `listenTo, который работает точно также, как одноименный метод хранилищ. Можно использовать и метод `listenToMany`.

Использование Reflux.listenTo Если вы не используете никакой специфичной логики в отношении `this.listenTo ()` внутри `componentDidMount ()`, вы можете использовать вызов `Reflux.listenTo ()` как примесь. В этом случае `componentDidMount ()` будет автоматически сконфигурирован требуемым образом, а вы получите примесь `ListenerMixin` в вашем компоненте. Таким образом пример выше может быть переписан так:

var Status = React.createClass ({ mixins: [Reflux.listenTo (statusStore, «onStatusChange»)], onStatusChange: function (status) { this.setState ({ currentStatus: status }); }, render: function () { // Рендеринг с использованием `this.state.currentStatus` } }); Можно вставлять несколько вызовов `Reflux.listenTo` внутри одного и того же массива`mixins`.

Существует также `Reflux.listenToMany` который работает аналогичным образом, позволяя использовать `listener.listenToMany`.

Использование Reflux.connect Если все, что вам нужно, это обновить состояние компонента при получении данных от хранилища, вы можете воспользоваться выражением `Reflux.connect (listener,[stateKey])` как примесью компонента ReactJS. Если передать туда необязательный ключ `stateKey`, состояние компонента будет автоматически обновлено с помощью `this.setState ({: data})`. Если `stateKey` не передан, будет сделан вызов `this.setState (data)`. Вот пример выше, переписанный с учетом новых возможностей:

var Status = React.createClass ({ mixins: [Reflux.connect (statusStore, «currentStatus»)], render: function () { // render using `this.state.currentStatus` } }); Использование Reflux.connectFilter `Reflux.connectFilter` можно использовать точно также, как `Reflux.connect`. Используйте `connectFilter` в качестве примеси в случае, если вам требуется передавать в компонент только часть состояния хранилища. Скажем, блог, написанный с использованием Reflux, скорее всего будет держать в хранилище все публикации. А на странице отдельного поста можно использовать `Reflux.connectFilter` для фильтрации постов.

var PostView = React.createClass ({ mixins: [Reflux.connectFilter (postStore, «post», function (posts) { posts.filter (function (post) { post.id === this.props.id; }); })], render: function () { // Отрисовываем, используя `this.state.post` } }); Обработка событий об изменениях от других хранилищ Хранилище может подписаться на изменения в других хранилищах, позволяя выстраивать цепочки передачи данных между хранилищами для агрегирования данных без затрагивания других частей приложения. Хранилище может подписаться на изменения, происходящие в других хранилищах с использованием метода `listenTo` точно также, как это происходит с объектами экшенов:

// Создаем хранилище, которое реагирует на изменения, происходящие в statusStore var statusHistoryStore = Reflux.createStore ({ init: function () {

// Подписываемся на хранилище как на экшен this.listenTo (statusStore, this.output);

this.history = []; },

// Обработчик экшена output: function (statusString) { this.history.push ({ date: new Date (), status: statusString }); // Инициируем собственное событие this.trigger (this.history); } }); Дополнительные возможности Использование альтернативной библиотеки управления событиями Не нравится `EventEmitter`, предоставляемый по умолчанию? Вы можете переключиться на использование любого другого, в том числе и встроенного в Node вот так:

// Это нужно сделать до создания экшенов и хранилищ Reflux.setEventEmitter (require ('events').EventEmitter); Использование альтернативной библиотеки промисов Не нравится библиотека, реализующая функциональность промисов, предоставляемая по умолчанию? Вы можете переключиться на использование любой другой (например, Bluebird вот так:

// Это нужно сделать до вызова любых экшенов Reflux.setPromise (require ('bluebird')); Имейте ввиду, что промисы в RefluxJS создаются с помощью вызова `new Promise (…)`. Если ваша библиотека использует фабрики, используйте вызов `Reflux.setPromiseFactory ()`.

Использование фабрики промисов Поскольку большая часть библиотек для работы с промисами не использует конструкторы (`new Promise (…)`), настраивать фабрику не нужно.

Однако, если вы используете что-нибудь вроде `Q` или какую-нибудь другую библиотеку, которая использует для создания промисов фабричный метод, используйте вызов `Reflux.setPromiseFactory` чтобы его указать.

// Это нужно сделать до использования экшенов Reflux.setPromiseFactory (require ('Q').Promise); Использование альтернативы nextTick Когда вызывается экшен вызывается как функтор, это происходит асинхронно. Возврат управления производится немедленно, а соответствующий обработчик вызывается через `setTimeout` (функция `nextTick`) внутри RefluxJS.

Вы можете выбрать ту реализацию отложенного вызова методов (`setTimeout`, `nextTick`, `setImmediate` и т.д.) которая вас устраивает.

// node.js env Reflux.nextTick (process.nextTick); В качестве альтернатив получше, вам может понадобится полифил `setImmediate` или `macrotask`

Ожидание завершения работы всех экшенов в цепочке В Reflux API есть методы `join`, которые обеспечивают удобную агрегацию источников, отправляющих события параллельно. Это тоже самое, что делает метод `waitFor` в оригинальной реализации Flux от Facebook.

Отслеживание аргументов Обработчик, переданный соответствующему `join ()` вызову будет вызыван как только все участники отправят событие как минимум единожды. Обработчику будут переданы параметры каждого события в том порядке, в котором участники операции объявлялись при вызове `join`.

Существует четыре варианта `join`, каждый из которых представляет собой особую стратегию работы с данными:

`joinLeading`: От каждого издателя сохраняется только результат первого вызова события. Все остальные данные игнорируются `joinTrailing`: От каждого издателя сохраняется только результат последнего вызова события. Все остальные данные игнорируются `joinConcat`: Все результаты сохраняются в массиве. `joinStrict`: Повторный вызов события от одного и того же издателя приводит к ошибке. Сигнатуры всех методов выглядят одинаково:

joinXyz (…publisher, callback) Как только `join ()` выполнится, все связанные с ним ограничения будут сняты и он снова сможет сработать, если издатели снова отправят события в цепочку.

Использование методов экземпляра для управления событиями Все объекты, использующие listener API (хранилища, компоненты React, подмешавшие `ListenerMixin`, или другие компоненты, использующие `ListenerMethods`) получают доступ к четырем вариантам метода `join`, о которых мы говорили выше:

var gainHeroBadgeStore = Reflux.createStore ({ init: function () { this.joinTrailing (actions.disarmBomb, actions.saveHostage, actions.recoverData, this.triggerAsync); } });

actions.disarmBomb («warehouse»); actions.recoverData («seedyletter»); actions.disarmBomb («docks»); actions.saveHostage («offices»,3); // `gainHeroBadgeStore` в этом месте кода хранилище отправит событие в цепочку с параметрами `[[«docks»],[«offices»,3],[«seedyletter»]]` Использование статических методов Поскольку использование методов `join`, а затем отправки события в цепочку является обычным делом для хранилища, все методы join имеют свои статические эквиваленты в объекте `Reflux`, которые возвращают объект хранилища, подписанный на указанные события. Используя эти методы пример выше можно переписать так:

var gainHeroBadgeStore = Reflux.joinTrailing (actions.disarmBomb, actions.saveHostage, actions.recoverData); Отправка состояния по умолчанию с использованием метода listenTo Функция `listenTo`, предоставляемая хранилищем и `ListenerMixin` имеет третий параметр, который может быть функцией. Эта функция будет вызвана в момент регистрации обработчика с результатом вызова `getInitialState` в качестве параметров.

var exampleStore = Reflux.createStore ({ init: function () {}, getInitialState: function () { return «какие-то данные по умолчанию»; } });

// Подписываемся на события от хранилища this.listenTo (exampleStore, onChangeCallback, initialCallback)

// initialCallback будет вызван немедленно с параметром «какие-то данные по умолчанию» Помните метод `listenToMany`? Если вы используете его с другими хранилищами, он тоже поддерживает `getInitialState`. Данные, возвращаемые этим методом будут переданы обычному обработчику, либо в метод `this.onDefault`, если он существует.

Колофон

© Habrahabr.ru