Статьи / JavaScript / Reactjs (документация, руководство, примеры, flux) / Flux


Учебное пособие по Flux TodoMVC

Список задач

Чтобы продемонстрировать архитектуру Flux с примерами кода, давайте возьмем классическое приложение TodoMVC. Всё приложение доступно в React GitHub репозитории в каталоге примеров flux-todomvc, но пройдем через его развитие шаг за шагом.

Для начала, нам нужен некоторый шаблон и работа с системой модулей. Модульный системный узел основывается на CommonJS и будет отвечать всем требованиям, и мы можем начать строить от react-boilerplate. Предполагая, что у вас установлен npm, просто клонируйте код react-boilerplate с GitHub, и перейдите в результирующий каталог в терминале (или любое другое CLI приложение, которое вы хотите). Затем запустите npm скрипты, чтобы установить и запустить: npm install , то npm run build и, наконец, npm start для последующей сборки через Browserify.

Пример TodoMVC имеет все это встроенным в него, но если вы начинаете с react-boilerplate убедитесь, что вы изменили свой ​​package.json файл, чтобы сопоставить структуру файла и зависимостей, описанных в примере TodoMVC в package.json, иначе ваши код не будет совпадать с пояснениями ниже.

Структура исходного кода


Файл index.html может быть использован в качестве точки входа в нашем приложении, которая загружает полученный bundle.js файл, но мы разместим большую часть нашего кода в директории js. Позволим Browserify сделать это дело. Теперь мы откроем новую вкладку в терминале (или GUI файловый браузер), чтобы посмотреть на директорию. Она должна выглядеть примерно так:

myapp
|
+ ...
+ js
|
+ app.js
+ bundle.js // сгенерирован Browserify, но мы можем изменить
+ index.html
+ ...


Далее мы нырнем в каталог JS и оформим первичную структуру каталогов нашего приложения:

myapp
|
+ ...
+ js
|
+ actions
+ components // все компоненты React, views и controller-views
+ constants
+ dispatcher
+ stores
+ app.js
+ bundle.js
+ index.html
+ ...


Создание диспетчера


Теперь мы готовы создать диспетчер. Вот простейший пример класса Dispatcher, написанный с JavaScript promises, полифил модулем Jake Archibald's ES6-Promises.

var Promise = require('es6-promise').Promise;
var assign = require('object-assign');

var _callbacks = [];
var _promises = [];

var Dispatcher = function() {};
Dispatcher.prototype = assign({}, Dispatcher.prototype, {

/**
* Регистрируем обратный вызов Хранилища так, что он может быть вызван действием.
* @param {function} callback Зарег-ая ф-ия обратного вызова.
* @return {number} index ф-ии обр. вызова в массиве _callbacks. */

register: function(callback) {
_callbacks.push(callback);
return _callbacks.length - 1; // index
},

/**
* dispatch
* @param {object} payload данные из действия(action).
*/

dispatch: function(payload) {
// Сначала создаем массив promises for callbacks to reference.
var resolves = [];
var rejects = [];
_promises = _callbacks.map(function(_, i) {
return new Promise(function(resolve, reject) {
resolves[i] = resolve;
rejects[i] = reject;
});
});
// Dispatch to callbacks and resolve/reject promises.
_callbacks.forEach(function(callback, i) {
// Callback может вернуть obj, to resolve или promise, to chain.
// Смотри waitFor() для чего это может быть полезно
Promise.resolve(callback(payload)).then(function() {
resolves[i](payload);
}, function() {
rejects[i](new Error('Dispatcher callback unsuccessful'));
});
});
_promises = [];
}
});

module.exports = Dispatcher;


public API этого основного диспетчера состоит только из двух методов: register() и dispatch(). Мы будем использовать register() в наших хранилищах, чтобы регистрировать каждый обратный вызов хранилища. Мы будем использовать dispatch() в наших действиях, чтобы вызывать колбеки(ф-и обратного вызова).

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

var Dispatcher = require('./Dispatcher');
var assign = require('object-assign');

var AppDispatcher = assign({}, Dispatcher.prototype, {

/**
* Соединительная функция между видами и диспетчером,
* маркировка действия, как действие вида
* Другим вариантом здесь мог быть handleServerAction.
* @param {object} action данные приходящие из вида.
*/

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

module.exports = AppDispatcher;

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

Создание Хранилища


Мы можем использовать Node EventEmitter, чтобы начать работать с хранилищем. Нам нужен EventEmitter, чтобы транслировать событие 'change' (изменение) для нашего контроллера-видов.

Примечание переводчика: А можем использовать любой подписчик|излучатель искусственных событий, даже EventEmitter без Node.
Итак, давайте взглянем на то, как это выглядит. Опустим часть кода для краткости, полную версию см. TodoStore.js в примере кода TodoMVC.

var AppDispatcher = require('../dispatcher/AppDispatcher');
var EventEmitter = require('events').EventEmitter;
var TodoConstants = require('../constants/TodoConstants');
var assign = require('object-assign');

var CHANGE_EVENT = 'change';

var _todos = {}; // колекция элементов задач(список задач)

/**
* Создать элемент задачи.
* @param {string} text содержимое задачи
*/
function create(text) {
// Используем текущий timestamp вместо реального id.
var id = Date.now();
_todos[id] = {
id: id,
complete: false,
text: text
};
}

/**
* Удаляем элемент задачи.
* @param {string} id
*/
function destroy(id) {
delete _todos[id];
}

var TodoStore = assign({}, EventEmitter.prototype, {

/**
* Получаем всю колекцию задач.
* @return {object}
*/
getAll: function() {
return _todos;
},

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

/**
* @param {function} callback
*/
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},

/**
* @param {function} callback
*/
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
},

dispatcherIndex: AppDispatcher.register(function(payload) {
var action = payload.action;
var text;

switch(action.actionType) {
case TodoConstants.TODO_CREATE:
text = action.text.trim();
if (text !== '') {
create(text);
TodoStore.emitChange();
}
break;

case TodoConstants.TODO_DESTROY:
destroy(action.id);
TodoStore.emitChange();
break;

// добавляем больше случаев для других actionTypes, типа TODO_UPDATE, и т.д.
}

return true; // No errors. Нужно promise в Dispatcher.
})

});

module.exports = TodoStore;

Есть несколько важных вещей, которые нужно отметить в коде выше. Для начала, мы поддерживаем private структуру данных под названием _todos. Этот объект содержит все индивидуальные элементы списка задач. Потому что эта переменная живет за пределами класса, но внутри замыкания модуля, она остается private(частной) - она не может быть напрямую изменена за пределами модуля. Это помогает сохранить отличный интерфейс ввода/вывода для потока данных, делая невозможным обновления хранилища без использования действия.

Другой важной частью является регистрация обратного вызова хранилища с помощью диспетчера. Мы передаем наш обработчик полезной нагрузки диспетчеру и сохраняем индекс, который указан в реестре диспетчера для этого хранилища. Функция обратного вызова в настоящее время обрабатывает только два actionTypes, но позже мы можем добавить столько, сколько нам нужно.

Прослушивание изменений с Controller-View


Нам нужен React компонент в верхней части нашей иерархии компонентов для прослушивания изменений в хранилище. При росте приложения, у нас бы было больше этих прослушивающих компонентов, возможно, один для каждого раздела страницы. В Facebook's Ads Creation Tool(инструмент для создания рекламы), у нас есть много этих контроллеро-подобных видов, каждый руководит специальным раздел в пользовательском интерфейсе. В Lookback Video Editor у нас был только два: один для анимированного предпросмотра и один для интерфейса выбора изображения. Ниже один из них для нашего примера TodoMVC. Опять же, он немного сокращенный, полный код вы можете посмотреть в примерах TodoMVC в TodoApp.react.js

var Footer = require('./Footer.react');
var Header = require('./Header.react');
var MainSection = require('./MainSection.react');
var React = require('react');
var TodoStore = require('../stores/TodoStore');

function getTodoState() {
return {
allTodos: TodoStore.getAll()
};
}

var TodoApp = React.createClass({

getInitialState: function() {
return getTodoState();
},

componentDidMount: function() {
TodoStore.addChangeListener(this._onChange);
},

componentWillUnmount: function() {
TodoStore.removeChangeListener(this._onChange);
},

/**
* @return {object}
*/
render: function() {
return (
<div>
<Header />
<MainSection
allTodos={this.state.allTodos}
areAllComplete={this.state.areAllComplete}
/>
<Footer allTodos={this.state.allTodos} />
</div>
);
},

_onChange: function() {
this.setState(getTodoState());
}

});

module.exports = TodoApp;

Теперь мы в нашей привычной React территории, используем React методы жизненного цикла. Мы установили начальное состояние этого контроллера-вида в getInitialState(), зарегистрировали слушатель событий в componentDidMount(), а затем убрали за собой в componentWillUnmount(). Мы рендерим наполненный div и передаем вниз коллекцию состояний, которые мы получили от TodoStore.

Компонент Header содержит основной input текста для приложения, но ему не нужно знать состояние хранилища. MainSection и Footer же нужны эти данные, так что мы передаем их им.

Больше Видов(Представлений)


На высоком уровне иерархии компонентов Reac приложение выглядит следующим образом:
<TodoApp>
<Header>
<TodoTextInput />

<MainSection>
<ul>
<TodoItem />
</ul>
</MainSection>

</TodoApp>

Если TodoItem находится в режиме редактирования, он также отрендерит TodoTextInput, как ребенка. Давайте взглянем на то, как некоторые из этих компонентов отображают данные, которые они получают в качестве свойств и как они общаются через действия с диспетчером. MainSection необходимо перебрать коллекцию задач, полученных от TodoApp, чтобы создать список TodoItems. В методе компонента render(), мы можем сделать так, что итерации выглядят следующим образом:
var allTodos = this.props.allTodos;

for (var key in allTodos) {
todos.push(<TodoItem key={key} todo={allTodos[key]} />);
}

return (
<section id="main">
<ul id="todo-list">{todos}</ul>
);

Теперь каждый TodoItem может отображать свой собственный текст и выполнять действия с использованием его собственного ID. Объяснения всех различных действий, которые TodoItem может вызывать в примере TodoMVC выходит за рамки данной статьи, но давайте просто посмотрим на действие, которое удаляет один из элементов дел. Вот сокращенный вариант TodoItem:
var React = require('react');
var TodoActions = require('../actions/TodoActions');
var TodoTextInput = require('./TodoTextInput.react');

var TodoItem = React.createClass({

propTypes: {
todo: React.PropTypes.object.isRequired
},

render: function() {
var todo = this.props.todo;

return (
<li
key={todo.id}>
<label>
{todo.text}
</label>
<button className="destroy" onClick={this._onDestroyClick} />
</li>
);
},

_onDestroyClick: function() {
TodoActions.destroy(this.props.todo.id);
}

});

module.exports = TodoItem;

Вместе с действием уничтожения, доступном в нашей библиотеке TodoActions и хранилищем готовым обработать это, соединение взаимодействия пользователя с изменениями состояния приложения не может быть проще. Мы просто обернули наш OnClick обработчик вокруг действия уничтожения, обеспечив его идентификатором. Теперь пользователь может нажать кнопку уничтожить и стартует цикл Flux обновления rest приложения.

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

Как вы увидите ниже, при каждом изменении в input, React ожидает от нас обновления состояния компонента. Поэтому, когда мы, наконец, готовы сохранить текст внутри input, Мы будем вставлять значение захвата в состояние компонента в полезной нагрузке действия. Это состояние UI, а не состояния приложения, и имейте это различие в виду, хорошее руководство для того, где состояние должно находиться. Всё состояние приложения должно находиться в хранилищее, в то время как компоненты иногда прикреплены в состояние UI. В идеале, компоненты React должны сохранить как можно меньше состояния, насколько это возможно.

Из-за того, что TodoTextInput используется в нескольких местах нашего приложения, с разным поведением, мы должны будем передать метод onSave в качестве свойства от родительского компонента. Это позволяет onSave ссылаться на различные действия в зависимости от того, где он используется.
var React = require('react');
var ReactPropTypes = React.PropTypes;

var ENTER_KEY_CODE = 13;

var TodoTextInput = React.createClass({

propTypes: {
className: ReactPropTypes.string,
id: ReactPropTypes.string,
placeholder: ReactPropTypes.string,
onSave: ReactPropTypes.func.isRequired,
value: ReactPropTypes.string
},

getInitialState: function() {
return {
value: this.props.value || ''
};
},

/**
* @return {object}
*/
render: function() /*object*/ {
return (
<input
className={this.props.className}
id={this.props.id}
placeholder={this.props.placeholder}
onBlur={this._save}
onChange={this._onChange}
onKeyDown={this._onKeyDown}
value={this.state.value}
autoFocus={true}
/>
);
},

/**
* Invokes the callback passed in as onSave, allowing this component to be
* used in different ways.
*/
_save: function() {
this.props.onSave(this.state.value);
this.setState({
value: ''
});
},

/**
* @param {object} event
*/
_onChange: function(/*object*/ event) {
this.setState({
value: event.target.value
});
},

/**
* @param {object} event
*/

_onKeyDown: function(event) {
if (event.keyCode === ENTER_KEY_CODE) {
this._save();
}
}

});

module.exports = TodoTextInput;

Header передается в метод onSave в качестве свойства, чтобы позволить TodoTextInput создавать новые элементы списка:
var React = require('react');
var TodoActions = require('../actions/TodoActions');
var TodoTextInput = require('./TodoTextInput.react');

var Header = React.createClass({

/**
* @return {object}
*/
render: function() {
return (
<header id="header">
<h1>todos</h1>
<TodoTextInput
id="new-todo"
placeholder="What needs to be done?"
onSave={this._onSave}
/>
</header>
);
},

/**
* Event handler called within TodoTextInput.
* Defining this here allows TodoTextInput to be used in multiple places
* in different ways.
* @param {string} text
*/
_onSave: function(text) {
TodoActions.create(text);
}

});

module.exports = Header;

Вместо этого, в другом контексте, например в режиме редактирования для существующего элемента списка дел мы могли бы передать onSave функцию обратного вызова, которая вызывает TodoActions.update(text).

Создание семантических действий


Вот основной код для двух действий, которые мы использовали выше в наших видах:
/**
* TodoActions
*/

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

var TodoActions = {

/**
* @param {string} text
*/
create: function(text) {
AppDispatcher.handleViewAction({
actionType: TodoConstants.TODO_CREATE,
text: text
});
},

/**
* @param {string} id
*/
destroy: function(id) {
AppDispatcher.handleViewAction({
actionType: TodoConstants.TODO_DESTROY,
id: id
});
},

};

module.exports = TodoActions;

Как вы можете видеть, нам действительно не нужны помощники AppDispatcher.handleViewAction() or TodoActions.create(). Мы могли бы, в теории, вызвать напрямую AppDispatcher.dispatch() и предоставить полезную нагрузку. Но так как наше приложение растет, наличие этих помощников позволяет хранить код чистым и семантическим. Просто намного чище писать TodoActions.destroy(id), а не писать кучу вещей, которые наш TodoItem не должен знать.

Полезная нагрузка произведенная TodoActions.create() будет выглядеть следующим образом:
{
source: 'VIEW_ACTION',
action: {
type: 'TODO_CREATE',
text: 'Write blog post about Flux'
}
}

Эта полезная нагрузка подается TodoStore через зарегистрированный обратный вызов. TodoStore затем передает событие 'change' (изменить), а MainSection отвечает выборкой новой коллекции элементов списка из TodoStore и изменяет его состояние. Это изменение в состоянии вызывает TodoApp компонент, чтобы он вызывал свой метод render(), и метод render() из всех его потомков.

Запусти меня


Файл загрузки нашего приложения - app.js. Он просто берет компонент TodoApp и рендерит его в корневом элементе приложения.
var React = require('react');

var TodoApp = require('./components/TodoApp.react');

React.render(
<TodoApp />,
document.getElementById('todoapp')
);


Добавление зависимостей управления диспетчеру


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

Нам потребуется еще public метод, waitFor(). Обратите внимание, что он возвращает Promise (обещание), который в свою очередь может быть возвращен из функции обратного вызова Store.
  /**
* @param {array} promisesIndexes
* @param {function} callback
*/
waitFor: function(promiseIndexes, callback) {
var selectedPromises = promiseIndexes.map(function(index) {
return _promises[index];
});
return Promise.all(selectedPromises).then(callback);
}

Теперь в ф-и обратного вызова TodoStore можно явно ждать каких-то зависимостей в первом обновлении прежде чем двигаться вперед. Однако, если хранилище Store A, ждет Store B, а B ждет A, то произойдет циклическая зависимость. Более надежный диспетчер обязан сообщить об этом сценарий предупреждениями в консоли.

Будущее Flux


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

Спасибо, что нашли время, чтобы прочитать о том, как мы строим клиентские приложения на Facebook. Мы надеемся, что Flux будет полезен для вас, как он полезен для нас.