...

понедельник, 13 апреля 2015 г.

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


Колофон




This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://fivefilters.org/content-only/faq.php#publishers.


Комментариев нет:

Отправить комментарий