Flux и Redux

Введение в Flux

Последнее обновление: 02.04.2022

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

Приложения, использующие Flux, имеют три основные части: диспетчер (dispatcher) , хранилище данных (store) и представления (view) - стандартные компоненты React.

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

Хранилища содержат состояние приложения и его логику. По своему действию они могут напоминать модель из паттерна MVC, в то же время они не представляют один объект, а управляют группой объектов. Каждое отдельное хранилище управляет определенной областью или доменом приложения.

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

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

Когда controller-view получает событие от хранилища, то вначале controller-view запрашивает у хранилища все необходимые данные. Затем он вызывает свой метод setState() или forceUpdate(), который приводит к выполнению у компонента метода render(). А это в свою очередь приводит к вызову метода render() и обновлению дочерних компонентов.

Нередко состояние хранилища передается по иерархии компонентов в виде единого объекта, а компоненты извлекают из него только те данные, которые им непосредственно нужны.

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

Весь механизм взаимодействия можно представить в виде однонаправленного потока от действия (action) к представлению (view):

Механизм работы Flux в React

Когда пользователь взаимодействует с представлением, то представления через диспетчер вызывают различные действия (например, добавление или обновление данных) по отношению к хранилищу, обратно от хранилища к представлению идут ответные действия, которые обновляют представление.

Представление и диспетчер во Flux в React

Рассмотрим действие архитектуры Flux на примере. Но стоит отметит, что на момент написания данной статьи (март 2022 года) самая последняя версия flux пока не поддерживала react 18, поэтому далее в проекте будет использоваться react 17.

Итак, создадим новый проект и для этого определим новый каталог fluxapp. Вначале добавим в него новый файл package.json:

{
  "name": "fluxapp",
  "description": "A React.js project using Flux",
  "version": "1.0.0",
  "author": "metanit.com",
  "scripts": {
    "dev": "webpack serve",
    "build": "webpack"
  },
  "dependencies": {
    "react": "17.0.0",
    "react-dom": "17.0.0",
    "flux": "4.0.0",
    "immutable": "4.0.0"
  },
  "devDependencies": {
    "@babel/cli": "7.17.0",
    "@babel/core": "7.17.0",
    "@babel/preset-react": "7.16.0",
    "@babel/preset-env": "7.16.0",
    "babel-loader": "8.2.0",
    "webpack": "5.70.0",
    "webpack-cli": "4.10.0",
    "webpack-dev-server": "4.7.0"
  }
}

Кроме зависимостей react и react-dom здесь добавлена зависимость flux. Кроме того, так как приложение будет разбито на отдельные части, то для их компиляции и сборки применяются пакеты babel и webpack.

Затем перейдем в командной строке/терминале к каталогу проекта и для установки пакетов выполним команду

npm install

Далее определим в проекте главную страницу index.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Flux в React</title>
</head>
<body>
    <div id="app"></div>
    <script src="public/bundle.js"></script>
</body>
</html>

То есть все файлы приложения будут компилироваться в файл public/bundle.js, который подключается на веб-странице.

Для всей логики с использованием React создадим в проекте новую папку app.

Определение действий и диспетчера

И вначале определим, какие действия будет выполнять приложение. В нашем случае будет простейшее приложение, которое будет управлять списком объектов - добавлением и удалением. Поэтому добавим в папку app новый каталог data, в котором определим новый файл ActionTypes.js:

const ActionTypes = { ADD_ITEM: "ADD_ITEM", REMOVE_ITEM: "REMOVE_ITEM"};

export default ActionTypes;

Итак, здесь определено два типа действий. И также в каталог app/data добавим новый файл PhonesDispatcher.js, который будет содержать определение диспетчера:

import {Dispatcher} from "flux";

export default new Dispatcher();

Диспетчер представляет объект класса Dispatcher из пакета flux.

И также добавим в каталог app/data новый файл Actions.js:

import ActionTypes from "./ActionTypes.js";
import PhonesDispatcher from "./PhonesDispatcher.js";

const Actions = {
  addItem(text) {
    PhonesDispatcher.dispatch({
      type: ActionTypes.ADD_ITEM,
      text,
    });
  },
  removeItem(text) {
    PhonesDispatcher.dispatch({
      type: ActionTypes.REMOVE_ITEM,
      text,
    });
  }
};

export default Actions;

Этот файл собственно определяет действия. Каждое действие определяется в виде функции, в которую могут передаваться параметры. В нашем случае список объектов будет представлять набор строк, поэтому в действия добавления и удаления элемента передается строка - добавляемый или удаляемый объект.

В самом действии вызывается метод dispatch. В качестве параметра этот метод принимает объект, в котором передаем тип действия и собственно данные. Но вообще в объекте можно определить любые данные, которые нам необходимы. При вызове действия этот объект будет передаваться в хранилище.

Определение хранилища

Далее также в каталоге app/data определим для хранилища файл PhoneStore.js:

import Immutable from "immutable";
import {ReduceStore} from "flux/utils";
import ActionTypes from "./ActionTypes.js";
import PhonesDispatcher from "./PhonesDispatcher.js";

class PhonesStore extends ReduceStore{
	constructor()
    {
        super(PhonesDispatcher);
    }
	getInitialState() {
		return Immutable.List.of("Apple iPhone 12 Pro", "Google Pixel 5");
	}

	reduce(state, action) {
		switch (action.type) {
			case ActionTypes.ADD_ITEM:
				if (action.text) {
				  return state.push(action.text);
				}
				return state;
			case ActionTypes.REMOVE_ITEM:
				let index = state.indexOf(action.text);
				if (index > -1) {
					return state.delete(index);
				}
				return state;
			default:
				return state;
		}
	}
}
export default new PhonesStore();

Хранилище представляет собой класс, унаследованный от класса ReduceStore из пакета "flux/utils". В конструкторе хранилища в конструктор базового класса передается объект диспетчера.

С помощью метода getInitialState() устанавливается состояние хранилища. В данном случае это список - объект Immutable.List. Он во многом аналогичен массиву javascript за тем исключением, что он является неизменяемым списком, а все операции с ним возвращают новый обновленный список. Подробнее про работу с такими коллекция можно посмотреть на странице immutable-js

В унаследованном методе reduce() получаем два объекта - state (текущее состояние хранилища, то, что изначально возвращается методом getInitialState) и action (тот объект, который передается в действии - то есть тип действия, добавляемый или удаляемый элемент). С помощью конструкции switch смотрим, какое действие было вызвано, и в зависимости от типа действия выполняем или добавление или удаление элемента.

Определение представления

Это была вся логика по работе с данными. Теперь определим визуальную часть. Для этого в каталог app добавим новую папку views. Далее в этой папке app/views создадим новый файл AppView.js:

import React from "react";

class AppView extends React.Component{

    constructor(props){
		super(props);
		this.state = {newItem: ""};
		
		this.onInputChange = this.onInputChange.bind(this);
		this.onClick = this.onClick.bind(this);
	}
	onInputChange(e){
		this.setState({newItem:e.target.value});
	}
	onClick(e){
		if(this.state.newItem){
			this.props.onAddItem(this.state.newItem);
			this.setState({newItem:" "});
		}
	}
	render(){
		
		let remove = this.props.onRemoveItem;
		return <div> 
				<input type="text" value={this.state.newItem} onChange={this.onInputChange} />	
				<button onClick={this.onClick}>Добавить</button>				
                <h2>Список смартфонов</h2>
                <div>
                    {
                        this.props.phones.map(function(item){
							
                            return <Phone key={item} text={item} onRemove={remove} />
                        })
                    }
                </div>
            </div>;
    }
}

class Phone extends React.Component{

    constructor(props){
		super(props);
		this.state = {text: props.text};
		this.onClick = this.onClick.bind(this);
	}
	onClick(e){
		this.props.onRemove(this.state.text);
	}
	render(){
		return <div> 
				<p>
					<b>{this.state.text}</b><br />
					<button onClick={this.onClick}>Удалить</button>	
				</p>
            </div>;
    }
}
export default AppView;

Класс AppView представляет компонент верхнего уровня, в котором выводится список. Каждый элемент списка представлен отдельным компонентом Phone. Отдельные компоненты также можно было бы поместить в отдельные файлы, но для простоты я разместил их в одном файле.

Определение контейнера

Для соединения хранилищ, действий и представлений во Flux применяются контейнеры. Поэтому добавим в каталог app новую папку containers, в которой создадим файл AppContainer.js:

import AppView from "../views/AppView.js";
import {Container} from "flux/utils";
import React from "react";
import PhoneStore from "../data/PhoneStore.js";
import Actions from "../data/Actions.js";

class AppContainer extends React.Component 
{ 
	static getStores() { 
		return [PhoneStore]; 
	} 
	static calculateState() { 
		return { 
			phones: PhoneStore.getState(),
			onAddItem: Actions.addItem,
			onRemoveItem: Actions.removeItem
		}; 
	}
	render() { 
		return <AppView phones={this.state.phones} 
						onRemoveItem={this.state.onRemoveItem}
						onAddItem={this.state.onAddItem}  />; 
	} 
} 
export default Container.create(AppContainer);

Класс контейнера AppContainer, с одной стороны, представляет компонент React. Но в то же время он реализует два необходимых метода: getStores() и calculateState().

Метод getStores() возвращает набор харнилищ, с которые используются в приложении. В нашем случае это только одно хранилище PhoneStore.

Метод calculateState() возвращает состояние контейнера. Здесь состояние контейнера включает список phones, причем этот список мы будем получать из состояния хранилища:

phones: PhoneStore.getState()

То есть, phones будет содержать объект Immutable.List.

Также в состоянии определяются два действия:

onAddItem: Actions.addItem,
onRemoveItem: Actions.removeItem

Эти действия вместе со списком phones передаются в AppView, который создается в методе render. То есть таким образом представление AppView со всеми дочерними компонентами будет связано с хранилищем и действиями и с помощью обработчиков нажатия кнопок сможем вызывать действия.

В конце файла вызывается метод Container.create(AppContainer), который создает сам контейнер.

Определение основного кода приложения

И в конце определим в папке app файл app.js, в котором будет происходить загрузка контейнера:

import AppContainer from "./containers/AppContainer.js";
import React from "react";
import ReactDOM from "react-dom";

ReactDOM.render(<AppContainer />, document.getElementById("app"));

Затем в корневой папке проекта определим файл webpack.config.js, который будет соединять все файлы в одну сборку:

const path = require("path");
  
module.exports = {
    mode: "development",
    entry: "./app/app.js", // входная точка - исходный файл
    output:{
        path: path.resolve(__dirname, "./public"),     // путь к каталогу выходных файлов - папка public
        publicPath: "/public/",
        filename: "bundle.js"       // название создаваемого файла
    },
    devServer: {
     historyApiFallback: true,
     static: {
      directory: path.join(__dirname, "/"),
    },
     port: 8081,
     open: true
   },
    module:{
        rules:[   //загрузчик для jsx
            {
                test: /\.jsx?$/, // определяем тип файлов
                exclude: /(node_modules)/,  // исключаем из обработки папку node_modules
                loader: "babel-loader",   // определяем загрузчик
                options:{
                    presets:[ "@babel/preset-env", "@babel/preset-react"]    // используемые плагины
                }
            }
        ]
    }
}

В итоге весь проект будет выглядеть следующим образом:

Структура проекта React на Flux

Для компиляции и упаковки файлов перейдем в терминале/командной строке к папке проекта и вызовем команду:

npm run build

Эта команда сгенерирует файл public/bundle.js, который будет подключаться на веб-страницу.

И в конце запустим проект на выполнение командой npm run dev:

React и Flux
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850