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

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

Обзор


image image image image image

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

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

г=========¬       г========¬       г=================¬
¦ Actions ¦------>¦ Stores ¦------>¦ View Components ¦
L=========-       L========-       L=================-
     ^                                      ¦
     L---------------------------------------

Паттерн состоит из экшенов (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`, если он существует.

Колофон


Комментарии (29)


  1. akira
    13.04.2015 11:21
    +1

    Почему «экшн» не переводить как «действие»? Событие же, не переводите как «ивент»?

    Юзер экшн вызывает ивет, что бы воркать со стореджем.


    1. FractalizeR Автор
      13.04.2015 11:37

      Возможно, вы правы. Мне вообще сложно было с этим термином. С одной стороны, это действие, с другой — событие. Кроме того, действие — достаточно общий термин, который может означать вообще все что угодно. А мне нужно было, чтобы читатель понимал, что речь идет именно об объекте Action RefluxJS.


      1. akira
        13.04.2015 11:47
        +1

        так и называйте — «объект действия».


        1. FractalizeR Автор
          13.04.2015 11:58
          +1

          Спасибо за совет, однако чем это лучше, чем «экшен»?

          Пример, который вы привели с event, на мой взгляд, отличается от нашего случая. События — устоявшийся за много лет термин. И действительно глупо переводить «event» как «ивент». Однако, случай с actions Reflux мне кажется немного другим. Этот термин мне не кажется ясным, очевидным и устоявшимся. Кроме того, я не считаю себя таким уж ярым борцом за чистоту русского языка и иногда мне проще (ясности ради) использовать англицизм, чем судорожно думать об адекватном переводе.


          1. akira
            13.04.2015 11:58
            +1

            Конечно лучше т.к. он объясняет что это не просто действие, а объект Reflux.


            1. FractalizeR Автор
              13.04.2015 15:12
              +1

              Я так не думаю, но спасибо за предложение.


  1. KlonD90
    13.04.2015 14:04

    А зачем экшен диспатчит сам себя? Что-то не то по-моему. Лучше уж диспатчер обогатить это тема.


    1. FractalizeR Автор
      13.04.2015 15:13

      Я думаю, это сделано для удобства, чтобы диспетчер вообще убрать из схемы. Что-то вроде «идеального объекта» из ТРИЗ. Лучше, когда его нет, а его обязанности выполняет кто-то другой.

      Мне нравится, как это сделано в Reflux. Довольно удобно. Вас не устраивает что-то конкретное или просто смущает то, что есть такое отклонение от Flux?


      1. KlonD90
        13.04.2015 15:17

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


        1. FractalizeR Автор
          13.04.2015 15:46

          С точки зрения чистой идеологии, наверное, вы правы. Но ведь Reflux отличается и по идеологии от Flux. Это некоторый шаг в сторону. Скажем, хранилища в Reflux могут подписываться на другие хранилища.

          Мне Reflux в этом смысле нравится больше.


  1. at0mic
    13.04.2015 15:08

    Что меня смутило, так это асинхронные экшены. Насколько я помню, на экшны не должна ложиться задача производить какие-либо операции, кроме как диспатчить самих себя. В примерах же видно, что экшн может выполнять действие и возвращать промис. Но ведь именно этим должен заниматься store, разве нет?


    1. FractalizeR Автор
      13.04.2015 15:16

      Какой конкретно пример вы имеете ввиду?


      1. at0mic
        13.04.2015 15:27

        asyncResultAction.listenAndPromise( someAsyncOperation );
        вот этот например


        1. FractalizeR Автор
          13.04.2015 15:53

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

          asyncResultAction.listenAndPromise( someAsyncOperation );
          


          Это означает, что производится подписка на событие asyncResultAction (в качестве обработчика используется someAsyncOperation). После того, как обработчик выполнится, будет произведена рассылка дочерних событий в зависимости от результата.

          Не слишком канонично, но удобно.


          1. at0mic
            13.04.2015 16:08

            Я бы предостерег от использования. В крупном проекте последнее место, где будут искать какую-то БЛ — это экшны. Видимо автор не смог побороть искушения встроить педали туда, где они, в общем-то, не нужны.


            1. FractalizeR Автор
              13.04.2015 16:21

              Да, согласен. Я у себя такое точно использовать не буду. Никто потом не поймет, что этот код делает вообще.


  1. dark_ruby
    14.04.2015 00:34

    так кто в итоге отвественнен за асинхронный вызов API — action или store?


    1. FractalizeR Автор
      14.04.2015 10:05

      Какого API? Я вроде тут все написал.


      1. dark_ruby
        14.04.2015 11:23

        я имел ввиду коммуникации с сервером — но судя по вашим примерам, они происходят в сторах. спасибо.


        1. FractalizeR Автор
          14.04.2015 14:18

          Да, в сторах. Только примеры не мои. Я их взял из документации по RefluxJS.


          1. dark_ruby
            14.04.2015 16:54

            а мне вот на SO не в сторах посоветовали это делать, но там правда и не про ReFlux речь, а про Flux


            1. FractalizeR Автор
              14.04.2015 17:22

              Я не думаю, что:

              • … есть один единственный правильный ответ на столь абстрактный вопрос
              • … стоит тупо, не думая самостоятельно, делать то, что вам сказали на каком-то ресурсе, включая Хабр и SO

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

              Если бы код работы с API разросся, я бы его абстрагировал в отдельный модуль. Но в моем случае скорее всего именно хранилище обращалось бы к API через фасад.

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


              1. dark_ruby
                14.04.2015 19:38

                я не делаю тупо, что мне сказали — я рассматриваю и сравниваю варианты, в противном случае ни здесь ни на СО этих тредов бы не было. Однако я подозреваю что flux это некий паттерн, reflux — это реализация этого паттерна, хоть и не много не каконичная с точки зрения фб — но как паттерн он подразумевает общение с внешним API — так вот мне интересно какая его часть за это должна быть отвественна. Понятное дело что сам код работы с API необходимо выделить в отдельный модуль — но будет ли он вызван в экшене или сторе — для меня остается вопрос открытый.


                1. FractalizeR Автор
                  15.04.2015 10:55

                  Прошу прощения, если мой ответ показался резким.

                  Я прочитал ваш вопрос и ответы к нему на SO. Я думаю, вы задали слишком общий вопрос. У вас есть какая-то конкретная ситуация, которую можно рассмотреть?

                  В самом экшене ничего вызвать нельзя, поскольку он не является полноценным объектом, который можно расширить (как хранилище). API можно вызывать в обработчике экшена (Action.listen(() => {})). Стоит ли это делать в обработчике или в хранилище, я полагаю, сильно зависит от ситуации.


                1. FractalizeR Автор
                  15.04.2015 12:13

                  Кстати, возможно вас заинтересует вот эта статья. Автор описывает недостатки вызова API из Datastore.


                1. wheercool
                  15.04.2015 16:44

                  Поделюсь своим видением Flux. Для меня это один из вариантов реализации классического MVC паттерна (к-ый используется в ASP.NET MVC а не двусторонние байндинги и т.п.) где
                  Store = Model
                  Action = Controller
                  View = React Components

                  С точки зрения MVC Controller должен синхронизировать Model с внешними источниками данных. Model про них вообще ничего не должна знать. Поэтому я бы общение с внешним API поместил в Controller (Action)


                  1. FractalizeR Автор
                    15.04.2015 18:23

                    Action не является объектом. Это событие.


                    1. wheercool
                      16.04.2015 10:47

                      Ну ок, если хотите формально, то в Flux Controller = Action Creator. Хотя сути то это не меняет )


                      1. FractalizeR Автор
                        16.04.2015 11:18

                        Да, я понял аналогию, но в Reflux нет Action creators в полном смысле. Произвольную логику, завязанную на action dispatch не поместить никуда. Разве что воспользоваться preEmit хуком у экшена, но это идеологически не совсем то будет.