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

от автора

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

В переводе ниже в качестве перевода для термина Action из Reflux иногда используется термин «событие», а иногда — термин «экшен», в зависимости от контекста. Более удачного перевода мне подобрать не удалось. Если у вас есть варианты, жду предложений в комментариях 😉

Обзор

image image image image image

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`, если он существует.

Колофон

ссылка на оригинал статьи http://habrahabr.ru/post/250581/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *