[Из песочницы] Быстрый курс Redux + websockets для бэкендера
Это краткое руководство и обучение по фронтэнеду для бэкендера. В данном руководстве я решаю проблему быстрого построения пользовательского интерфейса к серверному приложению в виде одностраничного веб-приложения (single page app).
Основной целью моего исследования является возможность за разумное время (для одного нормального человека) получить удобный и простой в использовании интерфейс-черновик к серверному приложению. Мы (как разработчики серверной части) понимаем, что наш приоритет — серверная часть. Когда (в гипотетическом проекте) появятся во фронте профи своего дела, они все сделают красиво и «правильно».
В роли учебной задачи представлена страничка чата с каким-то умозрительным «ботом», который работает на стороне сервера и принимает сообщение только через WebSocket. Бот при этом выполняет эхо ваших сообщений (мы тут не рассматриваем серверную часть вообще).
Мне для изложения материала требуется, чтобы вы имели:
- базовое знание javascript (тут нужно поискать в интернете справочник по крайней версии js стандартов ES-2015)
- знание reactjs (на уровне обучения https://facebook.github.io/react/tutorial/tutorial.html)
- понятие о websockets (это очень просто, главное чтобы ваш сервер это умел)
- знание и умение использовать bootstrap (на уровне этого раздела http://getbootstrap.com/css/)
Что используем
Redux — официальная документация расположена по адресу http://redux.js.org. По-русски есть несколько вариантов, я лично использовал в основном https://rajdee.gitbooks.io/redux-in-russian/content/docs/introduction/index.html.
Статью exec64, она стала причиной написать этот тутриал https://exec64.co.uk/blog/websockets_with_redux/.
Готовый сервер с react и redux от https://github.com/erikras/react-redux-universal-hot-example (он нам спасает человеко-месяцы времени по настройке большой связки технологий, которые необходимы для современного js проекта)
Мотивация
Вообще я разрабатываю приложение на языке Python. Погоди-погоди уходить …
Что мне было нужно:
- мне нужно чтобы реализация интерфейса не диктовала мне выбор технологий на стороне серверной части
- современные технологии (мне нечего было терять или быть чем-то обязанным «старым проверенным приемам»)
- это должно быть одностраничное приложений (я уже сам выберу, где можно обновлять страницу целиком)
- мне нужна реакция пользовательского интерфейса в реальном времени на серверные события
- мне нужен обмен информацией сервер-клиент (а не клиент-сервер) в реальном времени
- мне нужна возможность генерировать обновления клиента на сервере
Что было испробовано:
- вариации на тему на чистом js (устарело, есть много полезных моделей велосипеда)
- JQuery (уже не могу ТАК извратить так свой мозг, крайне сложный для быстрого старта синтаксис и… это дело профессионалов)
- Angular (переход на 2 версию спугнул и не нашел за отведенное время лазейки к решению моей задачи)
- Socket.io (там все реализовано, если вы node.js программист вы уже его используете, но он слишком сильно привязывает серверную часть на node, мне нужен только клиент без третьих лиц)
Выбрано в итоге:
- React (понятно и доступно/просто + babel = делает язык вполне понятным)
- Redux (импонирует использование единой помойки единого хранилища)
- WebSockets (очень просто и не связывает руки, а позволяет внутри себя уже применять такой формат какой позволит фантазия)
Упрощения и допущения:
- Мы не будем использовать авторизации в приложении
- Мы не будет использовать авторизации в WebSocket-ах
- Мы будем использовать самое доступное приложение Websocket Echo (https://www.websocket.org/echo.html)
Содержание
- Часть первая. Первоначальная настройка. Настройка одной страницы
- Часть вторая. Проектирование будущего приложения
- Часть третья. Изумительный Redux
- Часть четвертая. Оживляем историю
- Часть пятая. Проектируем чат
- Часть шестая. Мидлваре
Как читать
Не будете повторять — пропускайте часть 1
Знаете reactjs — пропускайте часть 2
Знаете redux — пропускайте части 3, 4 и 5
Знаете как работает middleware в redux — смело читайте часть 6 и далее в обратном порядке.
Часть первая. Первоначальная настройка. Настройка страницы.
Настройка окружения
Нам нужен node.js и npm.
Ставим node.js с сайта https://nodejs.org —, а именно этот гайд написан на 6ой версии, версию 7 тестировал — все работает.
npm устанавливается вместе с node.js
Далее нужно запустить npm и обновить node.js (для windows все тоже самое без npm)
sudo npm cache clean -f
sudo npm install -g n
sudo n stableпроверяем
node -vНастройка react-redux-universal-hot-example
Все выложено в react-redux-universal-hot-example, там же инструкция по установке.
Тут привожу последовательность действий
- Скачиваем и разархивируем архив/форкаем/что-угодно-как-вам-нравится.
- Через node.js command line или терминал переходим в эту папку
- Запускаем
npm install
npm run devПереходим на http://localhost:3000 и должны видеть стартовую страницу.
Если все ок — приступаем.
Создаем новый контейнер
Для настройки раздела используем предоставленную справку от команды react-redux-universal-hot-example. Оригинал статьи находится тут.
cd ./src/containers && mkdir ./SocketExampleКопируем туда hello.js как шаблон странички
cp About/About.js Hello/SocketExamplePage.jsЯ использую для всего этого Atom, как действительно прекрасный редактор-чего-угодно с некоторыми плюшками.
Правим скопированный файл
Создаем заглушку под нашу страница. Вводим элемент . Позже будем выводить статус соединения в этот элемент.
import React, {Component} from 'react';
import Helmet from 'react-helmet';
export default class SocketExamplePage extends Component {
render() {
return (
Socket Exapmle Page
Sockets not connected
);
}
}Подключаем созданную страницу
Добавляем в ./src/containers/index.js новый компонент React
export SocketExamplePage from './SocketExample/SocketExamplePage';Добавляем в ./src/routes.js, чтобы связать переход по пунти /socketexamplepage в карту ссылок
...
import {
App,
Chat,
Home,
Widgets,
About,
Login,
LoginSuccess,
Survey,
NotFound,
SocketExamplePage
} from 'containers';
...
{ /* Routes */ }
... Добавляем в ./src/containers/App/App.js, чтобы добавить пункт в меню
Socket Example Page
Проверяем
npm run devКоммит: https://github.com/valentinmk/react-redux-universal-hot-example/commit/69935996671fc5dd64062143526d1a00b49afcbd
На данный момент мы имеем:
- Раздел веб приложения
- Страничка на React для нашего приложения
- Заготовка, чтобы идти дальше
Прежде чем начнем. Я все разрабатывал в обратном порядке — сначала крутил мидлваре, потом прокидывал экшены и только потом уже прикручивал адекватный интерфейс в reactjs. Мы в руководстве будем делать все в правильном порядке, потому что так действительно быстрее и проще. Минус моего подхода в том, что я использовал в разы больше отладки и «костылей», чем нужно на самом деле. Будем рациональными.
Часть вторая. Проектирование будущего приложения
Сначала мы проектируем интерфейс пользователя. Для этого мы примерно представляем, как должен выглядеть скелет нашего интерфейса и какие действия будут происходить в нем.
В руководстве для начинающих React представлен подход по проектированию динамических приложений на React, от которого мы не будем отклоняться, а прямо будем следовать по нему.
Дэн Абрамов писал в своей документации много про то, что и как нужно разделять в приложении и как организовывать структуру приложения. Мы будем следовать его примеру.
Итак начнем.
Прежде всего хочу сказать, что для наглядности и отладки прямо при написании приложения мы будем добавлять элементы уберем с формы после окончания работы.
Пользовательский интерфейс «Вариант 1»
Мы добавляем два новых раздела на нашу страницу.
В логе подключения сокетов будем кратко выводить текущие события, связанные с подключением отключением. Изменяем файл ./src/containers/SocketExample/SocketExamplePage.js.
// inside render () { return (...) }
Socket connection log
index — порядковый номер записи логаloaded — признак загружен ли элемент на странице пользователя
message — переменна-сообщение для отладки и наглядности кода
connected — признак подключены ли мы сейчас к серверу
Конечно мы забыли про кнопки и поля ввода, добавляем:
- подключиться к websocket
- отключиться от websocket
В логе сообщений будем отображать отправленные -> и полученные сообщения <-.
// inside render () { return (...) }
Message log
-
Socket string
-
[ECHO] Socket string
Кнопка и ввод для отправить сообщение
Не нажимайте кнопку Send
Проверяем и закомитимся для получения полного кода.
Коммит: https://github.com/valentinmk/react-redux-universal-hot-example/commit/510a59f732a9bf42e070e7f57e970a2307661739
Пользовательский интерфейс Вариант 2. Компоненты.
Давайте разделим все на компоненты. Ничего сложного.
Создаем новую папку в директории ./src/components назовем ее SocketExampleComponents.
Добавление компонента происходит в три шага:
1 — создаем файл с компонентом в нашей папке SocketConnectionLog.js
мы оборачиваем в содержимое компонента в div так как от нас этого ожидает React
import React, {Component} from 'react';
export default class SocketConnectionLog extends Component {
render() {
return (
Socket connection log
);
}
}2 — прописываем наш новый компонент в файле components/index.js
export SocketConnectionLog from './SocketExampleComponents/SocketConnectionLog';3 — правим нашу страницу ./src/components/SocketExamplePage.js и вместо скопированного нами кода вставляем только один элемент
import {SocketConnectionLog} from 'components';
// ...
Добавляем другой новый компонент в ту же папку ./src/components/SocketExampleComponents.
Добавляем в три шага
1 — создаем файл с компонентом в нашей папке SocketMessageLog.js
import React, {Component} from 'react';
export default class SocketMessageLog extends Component {
render() {
return (
Message log
-
Socket string
-
[ECHO] Socket string
);
}
}2 — прописываем наш новый компонент в файле ./src/components/index.js
export SocketMessageLog from './SocketExampleComponents/SocketMessageLog';3 — правим нашу страницу и вместо скопированного нами кода вставляем только один элемент
// ...
import {SocketMessageLog} from 'components';
// ...
Проверяем. Ничего не изменилось и это успех.
Коммит:
https://github.com/valentinmk/react-redux-universal-hot-example/commit/97a6526020a549f2ddf91370ac70dbc0737f167b
Заканчиваем 2 часть.
Часть третья. Изумительный Redux
Переходим сразу к Redux.
Для этого нужно:
- Создать редюсер
- Создать экшены
- И подключить все это в общую систему
Про экшены написано В официальной документации
Про редюсеры написано Там же
Создаем файл
Создаем файл ./src/redux/modules/socketexamplemodule.js и наполняем базовыми экшенами и редюсерами. Вот тут базовом примере есть странная нестыковка, все предлагается писать в одном файле, не разделяя на файл экшенов и редюсеров, ну допустим. Все равно — мы тут все взрослые люди (we are all adults).
Экшены 1
export const SOCKETS_CONNECTING = 'SOCKETS_CONNECTING';
export const SOCKETS_DISCONNECTING = 'SOCKETS_DISCONNECTING';
export const SOCKETS_MESSAGE_SENDING = 'SOCKETS_MESSAGE_SENDING';
export const SOCKETS_MESSAGE_RECEIVING = 'SOCKETS_MESSAGE_RECEIVING';Все экшены мы будем запускать по нажатию кнопок, кроме события SOCKETS_MESSAGE_RECEIVING, который мы будем синтетически вызывать вслед за отправкой сообщения. Это делается, чтобы в процессе разработки эмулировать недостающие в настоящий момент (или на конкретном этапе) функционал серверной части приложения.
Редюсер
Добавляем в тот же файл.
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case SOCKETS_CONNECTING:
return Object.assign({}, state, {
loaded: true,
message: 'Connecting...',
connected: false
});
case SOCKETS_DISCONNECTING:
return Object.assign({}, state, {
loaded: true,
message: 'Disconnecting...',
connected: true
});
case SOCKETS_MESSAGE_SENDING:
return Object.assign({}, state, {
loaded: true,
message: 'Send message',
connected: true
});
case SOCKETS_MESSAGE_RECEIVING:
return Object.assign({}, state, {
loaded: true,
message: 'Message receive',
connected: true
});
default:
return state;
}
}Более подробно про структуру reducer и зачем Object.assign({}, state,{}); можно прочитать тут.
Вы заметили инициализацию state = initialState, которой мы не объявили (поставьте ESLint или его аналог — сильно упростит жизнь Нормального Человека). Добавим объявление до редюсера. Это будет первое состояние, которое мы будем иметь в нашем сторе на момент загрузки страницы, ну точнее страница будет загружаться уже с этим первоначальным состоянием.
const initialState = {
loaded: false,
message: 'Just created',
connected: false,
};Экшены 2
Теперь продолжим с нашими экшенами и на этом завершим этот модуль. Мы должны описать, как они будут изменять состояние reducer’a.
Добавляем в тот же файл.
export function socketsConnecting() {
return {type: SOCKETS_CONNECTING};
}
export function socketsDisconnecting() {
return {type: SOCKETS_DISCONNECTING};
}
export function socketsMessageSending() {
return {type: SOCKETS_MESSAGE_SENDING};
}
export function socketsMessageReceiving() {
return {type: SOCKETS_MESSAGE_RECEIVING};
}Подключаем в общий редюсер
На данный момент в приложении ничего не поменяется. Включаем наш модуль в общий конструктор reducer’ов.
В фале ./src/redux/modules/reducer.js прописываем модуль.
import socketexample from './socketexamplemodule';и включаем его в общую структуру результирующего редюсера
export default combineReducers({
routing: routerReducer,
reduxAsyncConnect,
auth,
form,
multireducer: multireducer({
counter1: counter,
counter2: counter,
counter3: counter
}),
info,
pagination,
widgets,
// our hero
socketexample
});Запускаем сервер, проверяем и ура в DevTools мы видим.

Если вопросы с initialState остались, то попробуйте их поменять или добавить новую переменную в него.
Коммит: https://github.com/valentinmk/react-redux-universal-hot-example/commit/0c984e3b5bc25056aa578ee57f90895bc6baaf18
Стор
А стор у нас уже создан и редюсер в него подключен. Ничего не делаем.
Если подробнее, то вы должны помнить, как мы добавили наш редюсер в combineReducers выше по статье. Так вот этот combineReducers сам включается в стор, который создаётся в файле ./src/redux/create.js.
Подключаем стор к react компонентам
Подключаем все это теперь в наши модули. В целях всесторонней демонстрации начнем с модуля истории и сделаем из него чистый компонент react (в смысле чистый от redux).
Компонент SocketConnectionLog мы пока не трогаем, а идем сразу в контейнер SocketExamplePage.
В данном контейнере мы будем подключать и получать данные из redux.
Подключаем библиотеку в файле ./src/containers/SocketExample/SocketExamplePage.js.
import {connect} from 'react-redux';Забираем экшены, чтобы потом их использовать у себя в react.
import * as socketExampleActions from 'redux/modules/socketexamplemodule';а еще мы поменяем строку, чтобы подключить PropTypes
import React, {Component, PropTypes} from 'react';Пишем коннектор, которым будем забирать данные из нашего редюсера.
@connect(
state => ({
loaded: state.socketexample.loaded,
message: state.socketexample.message,
connected: state.socketexample.connected}),
socketExampleActions)Как вы видите state.socketexample.loaded это обращение в redux, в той структуре, которую мы видим в DevTools.
Теперь подключаем проверки данных, получаемых из redux, что видится целесообразным т.к. любые проверки данных на тип есть вселенское добро.
static propTypes = {
loaded: PropTypes.bool,
message: PropTypes.string,
connected: PropTypes.bool
}Мы получили данные теперь давайте их передавать. Внутри блока render объявляем и принимаем данные уже теперь из props.
const {loaded, message, connected} = this.props;и спокойно и уверенно передаем их в наш модуль:
Мы передали новые данные (через react) в компонент. Теперь переписываем наш компонент, который уже ничего не знает про стор (redux), а только обрабатывает переданные ему данные.
В файле ./src/components/SocketExampleComponents/SocketConnectionLog.js действуем по списку:
- проверяем полученные props
- присваиваем их внутри render
- используем в нашем компоненте
Начнем, импортируем недостающие библиотеки:
import React, {Component, PropTypes} from 'react';добавляем проверку:
static propTypes = {
loaded: PropTypes.bool,
message: PropTypes.string,
connected: PropTypes.bool
}объявляем и присваиваем переменные, переданные через props
const {loaded, message, connected} = this.props;используем для вывода наши переменные
value={'index =' + 0 + ', loaded = ' + loaded + ', message = ' + message + ', connected = ' + connected}/>
{/* value="
index = 2, loaded = true, message = Connected, connected = true
index = 1, loaded = false, message = Connecting..., connected = false"/>
*/}Проверяем и видим, initialState прилетает к нам прямо из redux→react→props→props.
Коммит: https://github.com/valentinmk/react-redux-universal-hot-example/commit/60ac05332e35dfdbc11b9415f5bf5c46cd740ba8
SocketExampleMessageLog
Теперь переходим к компоненту SocketExampleMessageLog и сделаем его абсолютно самостоятельным, в смысле работы со стором. Мы не будем передавать в него никакие props, он будет получать все, что ему нужно из стор сам.
Открываем файл ./src/components/SocketExampleComponents/SocketMessageLog.js
в нем добавляем необходимые нам библиотеки
import React, {Component, PropTypes} from 'react';
import {connect} from 'react-redux';
import * as socketExampleActions from 'redux/modules/socketexamplemodule';добавляем connect, проверку типов и используем полученные данные
@connect(
state => ({
loaded: state.socketexample.loaded,
message: state.socketexample.message,
connected: state.socketexample.connected}),
socketExampleActions)
export default class SocketMessageLog extends Component {
static propTypes = {
loaded: PropTypes.bool,
message: PropTypes.string,
connected: PropTypes.bool
}
// ...Не забываем передать значение в метод render () через props
const {loaded, message, connected} = this.props;Мы будем использовать loaded и connected, чтобы определять готовность к обмену сообщения, а message выведем просто для проверки.
-
{message}
-
[ECHO] {message}
Я буду проверять переменные loaded и connected явно, чтобы быть более прозрачным для (возможных) потомков.
Полпути пройдено.
Коммит: https://github.com/valentinmk/react-redux-universal-hot-example/commit/a473d6a86262f2d2b52c590974e77df9454de5a1.
Часть четвертая. Оживляем историю
В предыдущих частя мы все подготовили к тому, чтобы начать использовать стор.
В этой части мы будем связывать события в react и состояния в стор. Начнем.
Оживим историю подключений в нашем компоненте ./src/components/SocketExampleComponents/SocketConnectionLog.js.
Но как мы помним, он ничего про стор не знает. Это означает, что он ничего не знает про экшены и поэтому ему их нужно передать через контейнер ./src/containers/SocketExample/SocketExamplePage.js. Просто передаем компоненту их как будто это простые props.
Вообще все функции экшенов мы подключили через connect. Стоп. Подробней. Вспомним.
//....
import * as socketExampleActions from 'redux/modules/socketexamplemodule';
//....
@connect(
state => ({
loaded: state.socketexample.loaded,
message: state.socketexample.message,
connected: state.socketexample.connected}),
socketExampleActions)Поэтому просто включаем их в проверку в файле ./src/containers/SocketExample/SocketExamplePage.js:
static propTypes = {
loaded: PropTypes.bool,
message: PropTypes.string,
connected: PropTypes.bool,
socketsConnecting: PropTypes.func,
socketsDisconnecting: PropTypes.func
}и передаем в наш компонент
render() {
const {loaded, message, connected, socketsConnecting, socketsDisconnecting} = this.props;
return (
Socket Exapmle Page
);
}Теперь давайте обеспечим прием преданных в компонент экшенов в файле ./src/components/SocketExampleComponents/SocketConnectionLog.js.
Мы будем добавлять их (экшены) в проверку и использовать в наших обработчиках действий на форме. Обработчиков сделаем два: по клику кнопки «Connect» и «Disconnect».
static propTypes = {
loaded: PropTypes.bool,
message: PropTypes.string,
connected: PropTypes.bool,
connectAction: PropTypes.func,
disconnectAction: PropTypes.func
}
handleConnectButton = (event) => {
event.preventDefault();
this.props.connectAction();
}
handleDisconnectButton = (event) => {
event.preventDefault();
this.props.disconnectAction();
}Прописываем вызов обработчиков функций по нажатию соответствующих кнопок.
render() {
const {loaded, message, connected} = this.props;
return (
Socket connection log
