Введение
Добро пожаловать в третью часть серии статей «Изучаем 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.