Разбираемся с Flux, реактивной архитектурой от facebook

https://scotch.io/tutorials/getting-to-know-flux-the-react-js-architecture
  • Перевод
  • Tutorial


Введение


Добро пожаловать в третью часть серии статей «Изучаем React». Сегодня мы будем изучать, как устроена архитектура Facebook Flux, и как использовать ее в своих проектах.

Прежде всего, я советую ознакомиться с двумя первыми статьями этой серии, Getting Started & Concepts и Building a Real Time Twitter Stream with Node and React. Их прочтение не является обязательным, однако наверняка может помочь вам понять эту статью, если вы еще недостаточно знакомы с React.js

Что такое Flux?


Flux — это архитектура, которую команда Facebook использует при работе с React. Это не фреймворк, или библиотека, это новый архитектурный подход, который дополняет React и принцип однонаправленного потока данных.

Тем не менее, Facebook предоставляет репозиторий, который содержит реализацию Dispatcher. Диспетчер играет роль глобального посредника в шаблоне «Издатель-подписчик» (Pub/sub) и рассылает полезную нагрузку зарегистрированным обработчикам.

Типичная реализация архитектуры Flux может использовать эту библиотеку вместе с классом EventEmitter из NodeJS, чтобы построить событийно-ориентированную систему, которая поможет управлять состоянием приложения.

Вероятно, Flux легче всего объяснить, исходя из составляющих его компонентов:
  • Actions / Действия — хелперы, упрощающие передачу данных Диспетчеру
  • Dispatcher / Диспетчер — принимает Действия и рассылает нагрузку зарегистрированным обработчикам
  • Stores / Хранилища — контейнеры для состояния приложения и бизнес-логики в обработчиках, зарегистрированных в Диспетчере
  • Controller Views / Представления — React-компоненты, которые собирают состояние хранилищ и передают его дочерним компонентам через свойства


Давайте посмотрим, как этот процесс выглядит в виде диаграммы:


Как к этому относится API?

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

Dispatcher / Диспетчер


Что же такое Диспетчер?

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

Так это, на самом деле, pub/sub?

Не совсем. Диспетчер рассылает данные ВСЕМ зарегистрированным в нём обработчикам и позволяет вызывать обработчики в определенном порядке, даже ожидать обновлений перед тем, как продолжить работу. Есть только один Диспетчер, и он действует как центральный узел всего вашего приложения.

Вот, как это может выглядеть:

var Dispatcher = require('flux').Dispatcher;
var AppDispatcher = new Dispatcher();

AppDispatcher.handleViewAction = function(action) {
  this.dispatch({
    source: 'VIEW_ACTION',
    action: action
  });
}

module.exports = AppDispatcher;

В примере выше мы создаем экземпляр Диспетчера и метод handleViewAction. Эта абстракция полезна, если вы собираетесь разделять действия, созданные в интерфейсе и действия, пришедшие от сервера / API.

Наш метод вызывает метод dispatch, который уже рассылает данные action всем зарегистрированным в нем обработчикам. Это действие затем может быть обработано Хранилищами, в результате чего состояние приложения будет обновлено.

Следующая диаграмма иллюстрирует этот процесс:


Зависимости


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

Чтобы использовать эту возможность, необходимо сохранить значение, возвращаемое из метода регистрации в Диспетчере, в свойстве dispatcherIndex Хранилища, как показано далее:

ShoeStore.dispatcherIndex = AppDispatcher.register(function(payload) {

});

Затем в Хранилище, при обработке Действия, мы можем использовать метод waitFor Диспетчера, чтобы убедиться, что к этому моменту ShoeStore уже успел обработать Действие и обновить данные:

case 'BUY_SHOES':
  AppDispatcher.waitFor([
    ShoeStore.dispatcherIndex
  ], function() {
    CheckoutStore.purchaseShoes(ShoeStore.getSelectedShoes());
  });
  break;

Прим. пер.: Ken Wheeler, очевидно, описывает устаревшую реализацию Диспетчера, т. к. в актуальной версии метод waitFor имеет другую сигнатуру.

Stores / Хранилища


Хранилища в Flux управляют состоянием определенных частей предметной области вашего приложения. На более высоком уровне это означает, что Хранилища хранят данные, методы получения этих данных и зарегистрированные в Диспетчере обработчики Действий.

Давайте взглянем на простое Хранилище:

var AppDispatcher = require('../dispatcher/AppDispatcher');
var ShoeConstants = require('../constants/ShoeConstants');
var EventEmitter = require('events').EventEmitter;
var merge = require('react/lib/merge');

// Внутренний объект для хранения shoes
var _shoes = {};

// Метод для загрузки shoes из данных Действия
function loadShoes(data) {
  _shoes = data.shoes;
}

// Добавить возможности Event Emitter из Node
var ShoeStore = merge(EventEmitter.prototype, {

  // Вернуть все shoes
  getShoes: function() {
    return _shoes;
  },

  emitChange: function() {
    this.emit('change');
  },

  addChangeListener: function(callback) {
    this.on('change', callback);
  },

  removeChangeListener: function(callback) {
    this.removeListener('change', callback);
  }

});

// Зарегистрировать обработчик в Диспетчере
AppDispatcher.register(function(payload) {
  var action = payload.action;
  var text;
  // Обработать Действие в зависимости от его типа
  switch(action.actionType) {
    case ShoeConstants.LOAD_SHOES:
      // Вызвать внутренний метод на основании полученного Действия
      loadShoes(action.data);
      break;

    default:
      return true;
  }
  
  // Если Действие было обработано, создать событие "change"
  ShoeStore.emitChange();

  return true;

});

module.exports = ShoeStore;

Самое важное, что мы сделали в примере выше — добавили к нашему хранилищу возможности EventEmitter из NodeJS. Это позволяет хранилищам слушать и рассылать события, что, в свою очередь, позволяет компонентам представления обновляться, отталкиваясь от этих событий. Так как наше представление слушает событие «change», создаваемое Хранилищами, оно узнаёт о том, что состояние приложения изменилось, и пора получить (и отобразить) актуальное состояние.

Также мы зарегистрировали обработчик в нашем AppDispatcher с помощью его метода register. Это означает, что теперь наше Хранилище теперь слушает оповещения от AppDispatcher. Исходя из полученных данных, оператор switch решает, можем ли мы обработать Действие. Если действие было обработано, создается событие «change», и Представления, подписавшиеся на это событие, реагируют на него обновлением своего состояния:


Представление использует метод getShoes интерфейса Хранилища для того, чтобы получить все shoes из внутреннего объекта _shoes и передать эти данные в компоненты. Это очень простой пример, однако такая архитектура позволяет компонентам оставаться достаточно аккуратными, даже если вместо Представлений использовать более сложную логику.

Action Creators & Actions / Фабрика Действий и Действия


Фабрика Действий — это набор методов, которые вызываются из Представлений (или из любых других мест), чтобы отправить Действия Диспетчеру. Действия и являются той полезной нагрузкой, которую Диспетчер рассылает подписчикам.

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

Вот как выглядят объявления констант:

var keyMirror = require('react/lib/keyMirror');

module.exports = keyMirror({
  LOAD_SHOES: null
});

Выше мы использовали библиотеку keyMirror из React чтобы, как вы догадались, создать объект со значениями, идентичными своим ключам. Просто посмотрев на этот файл, можно сказать, что наше приложение умеет загружать shoes. Использование констант позволяет всё упорядочить и помогает быстро оценить возможности приложения.

Давайте теперь посмотрим на объявление соответствующей Фабрики Действий:

var AppDispatcher = require('../dispatcher/AppDispatcher');
var ShoeStoreConstants = require('../constants/ShoeStoreConstants');

var ShoeStoreActions = {

  loadShoes: function(data) {
    AppDispatcher.handleAction({
      actionType: ShoeStoreConstants.LOAD_SHOES,
      data: data
    })
  }

};

module.exports = ShoeStoreActions;

В приведенном примере мы создали в нашем объекте ShoeStoreActions метод, который передает нашему Диспетчеру указанные данные. Теперь мы можем загрузить этот файл из нашего API (или, например, Представлений) и вызвать метод ShoeStoreActions.loadShoes(ourData), чтобы передать полезную нагрузку Диспетчеру, который разошлет её подписчикам. Таким образом ShoeStore узнает об этом событии и вызовет метод загрузки каких-нибудь shoes.

Controller Views / Представления


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


Вот, как это выглядит:

/** @jsx React.DOM */

var React = require('react');
var ShoesStore = require('../stores/ShoeStore');

// Метод для получения состояния приложения из хранилища
function getAppState() {
  return {
    shoes: ShoeStore.getShoes()
  };
}

// Создаем React-компонент
var ShoeStoreApp = React.createClass({

  // Используем метод getAppState, чтобы установить начальное состояние
  getInitialState: function() {
    return getAppState();
  },
  
  // Подписываемся на обновления
  componentDidMount: function() {
    ShoeStore.addChangeListener(this._onChange);
  },

  // Отписываемся от обновлений
  componentWillUnmount: function() {
    ShoesStore.removeChangeListener(this._onChange);
  },

  render: function() {
    return (
      <ShoeStore shoes={this.state.shoes} />
    );
  },
  
  // Обновляем состояние Представления в ответ на событие "change"
  _onChange: function() {
    this.setState(getAppState());
  }

});

module.exports = ShoeStoreApp;

Прим. пер.: В актуальной версии React компоненты создаются слегка по-другому.

В примере выше мы подписываемся на обновления Хранилища, используя addChangeListener, и обновляем наше состояние, когда получим событие «change».

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

Собираем всё вместе


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



Заключение


Надеюсь, эта статья помогла вам лучше понять архитектуру Flux от Facebook. Я даже не подозревал, насколько удобен React.js, пока не попробовал его в действии.

Использовав однажды Flux, вы почувствуете, что написание приложений на React без Flux похоже на манипуляции с DOM без jQuery. Да, это возможно, но выглядит менее изящно и упорядочено.

Если вы хотите придерживаться архитектуры Flux, но вам не нравится React, попробуйте Delorean, Flux-фреймворк, который можно совместить с Ractive.js или Flight. Еще одна заслуживающая внимания библиотека — Fluxxor, которая использует немного иной подход к архитектуре Flux и предполагает более жесткую связь компонентов Flux в составе единого экземпляра.

Я полагаю, что для того, чтобы полностью понять Flux, его необходимо испытать в деле, поэтому оставайтесь с нами, чтобы прочитать четвертую, заключительную часть цикла статей по изучению React, где мы создадим простой онлайн-магазин, используя React.js и архитектуру Flux.
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 16
  • +2
    Это мой первый перевод, так что буду рад критике и советам. Например, я старался держаться как можно ближе к тексту оригинала, хоть и не во всем с ним согласен, а также по возможности использовать «официальные» версии английских слов (в духе View — Представление) вместо того, чтобы оставить их в оригинале, или использовать их общеупотребительные, неформальные аналоги (Вьюшка).
    • НЛО прилетело и опубликовало эту надпись здесь
    • +1
      В развитие этой темы могу предложить хорошее сравнение популярных реализаций Flux
      • +2
        Хочется особо подчеркнуть, что flux это не использование приведенных библиотек, а подход. Мы например не используем ни одной из этих библиотек, однако подход такой же. Есть модели которые просто выбрасывают события на изменения, добавления и тд. На события реагируют компоненты и тд.
        • +3
          Отличная статья, лично мне её не хватало на хабре.
          И да, отдельный респект за перевод картинок, получилось очень качественно.
          Спасибо.
          • +2
            Рад, что понравилось. Таки не зря потратил время :)
          • 0
            Рекомендую посмотреть reflux реализацию.
            В ней убрали Dispatcher за ненадобностью.
            • +1
              Оставлю это (react doc rus) здесь. Переведена половина Guides из раздела DOCS, вскоре будет переведена и выложена остальная часть. Со временем переводы исправляются и дополняются. Переводы обновляются 1-2 раза в неделю.
              • 0
                Спасибо за перевод, но нет возможности его почитать — на мобильном андроиде ваш не отображается. Вы не могли бы сделать страницу на гитхабе?
                • +1
                  Если у вас на андроиде нет интернета, то можно сохранять страницы с сайта в pdf и преспокойно читать pdf. Я иногда так ошибки проверяю. printpdf.pf-control.de/index.php/en/convert_actual_page.html

                  Половина переводов выложены через андроид, поэтому ваша проблема непонятна.

                  На гит выкладывать пока не планируется, тяжело форматировать, долго, картинки, нужно сохранить перелинковку и мне лень. Кто-то захочет, чтобы я сделал справку chm, кто-то еще что-то. Я лучше еще пару статей переведу.
              • 0
                А когда мы добавляем так
                ShoeStore.addChangeListener(this._onChange);
                Разве this тут не бует ссылаться на объект ShoesStore?
                _onChange: function() {
                this.setState(getAppState());
                }
                • +1
                  У Реакта все методы компонента при создании биндятся к экземпляру. В смысле this.setState = this.setState.bind(this), _onChange аналогично.
                • 0
                  Мне вот интересно следующая. допустим мы делаем переиспользуемый компонент на Реакте. И вдруг на одной и той же странице у нас отрендерились два одинаковых компонента с одинаковым названием событий и эти же события привязываются к одним и тем же сторам. Не будут ли в таком случае сторы влиять друг на друга?
                  Один из выходов это добавлять id к каждому store и на его основе производить обновление, хотелось бы знать, кто как решает эту задачу?
                  • 0
                    Не очень понял, что такое события, привязанные к сторам, и почему сторы будут влиять друг на друга.

                    Допустим, есть компонент-список сообщений в чате с полем ввода, которое создаёт действие CreateMessage. Это действие принимает MessageStore, добавляя новое сообщение в список, после чего создаёт событие changed. На событие changed этого стора подписан вышеуказанный компонент.

                    В случае двух одинаковых компонентов (и одном сторе MessageStore) эти два компонента ведут себя идентично, и сообщение из каждого из них отображается в обоих.
                    • 0
                      Store используется для хранения состояния приложения. Если мы делаем какой-либо переиспользуемый компонент, он не использует store — все параметры ему передаются через props. Если мы делаем компонент бизнес-логики, то это значит, что нам нужно отобразить данные из одного store по определённому фильтру или категории (которые передаются ему через props). Это возможности для этого должны быть в реализацию самого store.
                      Подробней о предназначении state и props можно почитать здесь.
                      • 0
                        Подробней о предназначении state и props можно почитать здесь.

                        или на русском тоже самое здесь

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