Быстрый курс Redux + websockets для бэкендера

Всем привет из 2018! Оригинальный react-redux-universal-hot-example прекратил развитие в 2017 году, но его можно собрать на версии 6.14.2, на 8 и выше версии будут ошибки. Но есть его форк
https://github.com/bertho-zero/react-redux-universal-hot-example, где продолжается разработка и поддерживаются более свежие версии Nodejs.

Это краткое руководство и обучение по фронтэнеду для бэкендера. В данном руководстве я решаю проблему быстрого построения пользовательского интерфейса к серверному приложению в виде одностраничного веб-приложения (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, там же инструкция по установке.
Тут привожу последовательность действий


  1. Скачиваем и разархивируем архив/форкаем/что-угодно-как-вам-нравится.
  2. Через node.js command line или терминал переходим в эту папку
  3. Запускаем

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, как действительно прекрасный редактор-чего-угодно с некоторыми плюшками.


Правим скопированный файл


Создаем заглушку под нашу страница. Вводим элемент <p>. Позже будем выводить статус соединения в этот элемент.


import React, {Component} from 'react';
import Helmet from 'react-helmet';

export default class SocketExamplePage extends Component {
  render() {
    return (
      <div className="container">
        <h1>Socket Exapmle Page</h1>
        <Helmet title="Socket Exapmle Page"/>
        <p>Sockets not connected</p>
      </div>
    );
  }
}

Подключаем созданную страницу


Добавляем в ./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 */ }
      <Route path="about" component={About}/>
      <Route path="login" component={Login}/>
      <Route path="survey" component={Survey}/>
      <Route path="widgets" component={Widgets}/>
      <Route path="socketexamplepage" component={SocketExamplePage}/>
...

Добавляем в ./src/containers/App/App.js, чтобы добавить пункт в меню


              <LinkContainer to="/socketexamplepage">
                <NavItem eventKey={99}>Socket Example Page</NavItem>
              </LinkContainer>

Проверяем


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 (...) }
          <h3>Socket connection log</h3>
          <textarea
            className="form-control"
            rows="1"
            readOnly
            placeholder="Waiting ..."
            value="
              index = 2, loaded = true, message = Connected, connected = true
              index = 1, loaded = false, message = Connecting..., connected = false"/>

index — порядковый номер записи лога

loaded — признак загружен ли элемент на странице пользователя

message — переменна-сообщение для отладки и наглядности кода

connected — признак подключены ли мы сейчас к серверу

Конечно мы забыли про кнопки и поля ввода, добавляем:


  • подключиться к websocket
  • отключиться от websocket

          <button className="btn btn-primary btn-sm">
            <i className="fa fa-sign-in"/> Connect
          </button>
          <button className="btn btn-danger btn-sm">
            <i className="fa fa-sign-out"/> Disconnect
          </button>

В логе сообщений будем отображать отправленные -> и полученные сообщения <-.


// inside render () { return (...) }
      <h3>Message log</h3>
      <ul>
          <li key="1" className="unstyled">
            <span className="glyphicon glyphicon-arrow-right"></span>
            Socket string
          </li>
          <li key="2" className="unstyled">
            <span className="glyphicon glyphicon-arrow-left"></span>
            [ECHO] Socket string
          </li>
      </ul>

Кнопка и ввод для отправить сообщение


      <form className="form-inline">
        <p></p>
        <div className="form-group">
          <input
          className="form-control input-sm"
          type="text"
          ref="message_text"></input>
        </div>
        <button className="btn btn-primary btn-sm">
          <i className="fa fa-sign-in"/> Send
        </button>
      </form>

Не нажимайте кнопку 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 (
      <div>
        <h3>Socket connection log</h3>
        <textarea
          className="form-control"
          rows="1"
          readOnly
          placeholder="Waiting ..."
          value="
            index = 2, loaded = true, message = Connected, connected = true
            index = 1, loaded = false, message = Connecting..., connected = false"/>
        <button className="btn btn-primary btn-sm">
          <i className="fa fa-sign-in"/> Connect
        </button>
        <button className="btn btn-danger btn-sm">
          <i className="fa fa-sign-out"/> Disconnect
        </button>
      </div>
    );
  }
}

2 — прописываем наш новый компонент в файле components/index.js


export SocketConnectionLog from './SocketExampleComponents/SocketConnectionLog';

3 — правим нашу страницу ./src/components/SocketExamplePage.js и вместо скопированного нами кода вставляем только один элемент


import {SocketConnectionLog} from 'components';
// ...
        <SocketConnectionLog />

Добавляем другой новый компонент в ту же папку ./src/components/SocketExampleComponents.


Добавляем в три шага


1 — создаем файл с компонентом в нашей папке SocketMessageLog.js


import React, {Component} from 'react';

export default class SocketMessageLog extends Component {
  render() {
    return (
      <div>
        <h3>Message log</h3>
        <ul>
          <li key="1" className="unstyled">
            <span className="glyphicon glyphicon-arrow-right"></span>
            Socket string
          </li>
          <li key="2" className="unstyled">
            <span className="glyphicon glyphicon-arrow-left"></span>
            [ECHO] Socket string
          </li>
        </ul>
        <form className="form-inline">
          <p></p>
          <div className="form-group">
            <input
            className="form-control input-sm"
            type="text"
            ref="message_text"></input>
          </div>
          <button className="btn btn-primary btn-sm">
            <i className="fa fa-sign-in"/> Send
          </button>
        </form>
      </div>
    );
  }
}

2 — прописываем наш новый компонент в файле ./src/components/index.js


export SocketMessageLog from './SocketExampleComponents/SocketMessageLog';

3 — правим нашу страницу и вместо скопированного нами кода вставляем только один элемент


// ...
import {SocketMessageLog} from 'components';
// ...
        <SocketMessageLog/>

Проверяем. Ничего не изменилось и это успех.


Коммит:
https://github.com/valentinmk/react-redux-universal-hot-example/commit/97a6526020a549f2ddf91370ac70dbc0737f167b


Заканчиваем 2 часть.


Часть третья. Изумительный Redux


Переходим сразу к Redux.


Для этого нужно:


  1. Создать редюсер
  2. Создать экшены
  3. И подключить все это в общую систему

Про экшены написано В официальной документации
Про редюсеры написано Там же

Создаем файл


Создаем файл ./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 мы видим.


image


Если вопросы с 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;

и спокойно и уверенно передаем их в наш модуль:


<SocketConnectionLog loaded={loaded} message={message} connected={connected} />

Мы передали новые данные (через react) в компонент. Теперь переписываем наш компонент, который уже ничего не знает про стор (redux), а только обрабатывает переданные ему данные.


В файле ./src/components/SocketExampleComponents/SocketConnectionLog.js действуем по списку:


  1. проверяем полученные props
  2. присваиваем их внутри render
  3. используем в нашем компоненте

Начнем, импортируем недостающие библиотеки:


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 выведем просто для проверки.


        <ul>
          <li key="1" className="unstyled">
            <span className="glyphicon glyphicon-arrow-right"> </span>
            {message}
          </li>
          <li key="2" className="unstyled">
            <span className="glyphicon glyphicon-arrow-left"> </span>
            [ECHO] {message}
          </li>
        </ul>

Я буду проверять переменные loaded и connected явно, чтобы быть более прозрачным для (возможных) потомков.


        <form className="form-inline">
          <p></p>
          <div className="form-group">
            <input
              className="form-control input-sm"
              type="text"
              ref="message_text" readOnly = {(loaded === true) ? false : true}></input>
          </div>
          <button
            className="btn btn-primary btn-sm"
            disabled = {(connected === true) ? false : true}>
            <i className="fa fa-sign-in"/> Send
          </button>
        </form>

Полпути пройдено.


Коммит: 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 (
      <div className="container">
        <h1>Socket Exapmle Page</h1>
        <Helmet title="Socket Exapmle Page"/>
        <SocketConnectionLog loaded={loaded} message={message} connected={connected} connectAction={socketsConnecting} disconnectAction={socketsDisconnecting}/>
        <SocketMessageLog/>
      </div>
    );
  }

Теперь давайте обеспечим прием преданных в компонент экшенов в файле ./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 (
      <div>
        <h3>Socket connection log</h3>
        <textarea
          className="form-control"
          rows="1"
          readOnly
          placeholder="Waiting ..."
          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"/>
          */}
        <button
          className="btn btn-primary btn-sm"
          onClick={this.handleDisconnectButton}>
          <i className="fa fa-sign-in"/> Connect
        </button>
        <button
          className="btn btn-danger btn-sm"
          onClick={this.handleConnectButton}>
          <i className="fa fa-sign-out"/> Disconnect
        </button>
      </div>
    );

Запускаем. Проверяем. Ура, оно живо! Можно посмотреть в DevTools, что события создаются в сторе.


Если внимательно проследить как меняются состояния, то можно заметить, что компонент истории сообщений работает как-то не так (хотя он написан правильно). Дело в том, что при нажатии кнопки подключения у нас состояние connected = false, а при разрыве подключения у нас состояние connected = true. Давай-те поправим.


Для этого в файле ./src/redux/modules/socketexamplemodule.js правим странные строчки


 case SOCKETS_CONNECTING:
  return Object.assign({}, state, {
   loaded: true,
   message: 'Connecting...',
   connected: true
  });
 case SOCKETS_DISCONNECTING:
  return Object.assign({}, state, {
   loaded: true,
   message: 'Disconnecting...',
   connected: false
  });

Ну теперь все работает правильно.


НО далее мы поменяем эти значения на исходные, это важный момент. Событие попытки подключения не тождественно состоянию подключено (да я кэп).

Реализуем историю подключения. Главное ограничение принцип работы самого стора. Мы нее можем изменять само состояние, но мы можем его целиком пересоздавать и присваивать. Поэтому чтобы накапливать историю мы будем ее копировать, прибавлять к копии текущее состояние и присваивать это значение оригиналу (с которого сняли копию).


    case SOCKETS_CONNECTING:
      return Object.assign({}, state, {
        loaded: true,
        message: 'Connecting...',
        connected: true,
        history: [
          ...state.history,
          {
            loaded: true,
            message: 'Connecting...',
            connected: true
          }
        ]
      });
    case SOCKETS_DISCONNECTING:
      return Object.assign({}, state, {
        loaded: true,
        message: 'Disconnecting...',
        connected: false,
        history: [
          ...state.history,
          {
            loaded: true,
            message: 'Disconnecting...',
            connected: false
          }
        ]
      });

Делаем отображение в том же элементе. Прежде всего передаем переменную истории через props в файле ./src/containers/SocketExample/SocketExamplePage.js. Далее в файле ./src/components/SocketExampleComponents/SocketConnectionLog.js принимает переданную переменную.


Приступим в файле ./src/containers/SocketExample/SocketExamplePage.js забираем из стора:


@connect(
 state => ({
  loaded: state.socketexample.loaded,
  message: state.socketexample.message,
  connected: state.socketexample.connected,
  history: state.socketexample.history
 }),
 socketExampleActions)

проверяем на тип


  static propTypes = {
    loaded: PropTypes.bool,
    message: PropTypes.string,
    connected: PropTypes.bool,
    history: PropTypes.array,
    socketsConnecting: PropTypes.func,
    socketsDisconnecting: PropTypes.func    
  }

присваиваем и передаем


  render() {
    const {loaded, message, connected, socketsConnecting, socketsDisconnecting, history} = this.props;
    return (
      <div className="container">
        <h1>Socket Exapmle Page</h1>
        <Helmet title="Socket Exapmle Page"/>
        <SocketConnectionLog loaded={loaded} message={message} connected={connected} connectAction={socketsConnecting} disconnectAction={socketsDisconnecting} history={history}/>
        <SocketMessageLog/>
      </div>
    );

Принимаем уже в файле ./src/components/SocketExampleComponents/SocketConnectionLog.js.


 static propTypes = {
   loaded: PropTypes.bool,
   message: PropTypes.string,
   connected: PropTypes.bool,
   history: PropTypes.array,
   connectAction: PropTypes.func,
   disconnectAction: PropTypes.func
 }

Для вывода истории в лог нам уже на самом деле не требуются текущие значения loaded, message, connected.


Давайте выведем в историю в обратной хронологии, так чтобы актуально состояние всегда было сверху.


  render() {
    const {history} = this.props;
    return (
      <div>
        <h3>Socket connection log</h3>
        <textarea
          className="form-control"
          rows="1"
          readOnly
          placeholder="Waiting ..."
          value={
            history.map((historyElement, index) =>
              'index = ' + index +
              ' loaded = ' + historyElement.loaded.toString() +
              ' message = ' + historyElement.message.toString() +
              ' connected = ' + historyElement.connected.toString() + ' \n').reverse()
            }/>
        <button
          className="btn btn-primary btn-sm"
          onClick={this.handleConnectButton}>
          <i className="fa fa-sign-in"/> Connect
        </button>
        <button
          className="btn btn-danger btn-sm"
          onClick={this.handleDisconnectButton}>
          <i className="fa fa-sign-out"/> Disconnect
          </button>
      </div>
    );

Главное, что нужно не забыть это добавить history при инициализации редюсера, иначе наши проверки не будут срабатывать.


В файле ./src/redux/modules/socketexamplemodule.js.


const initialState = {
  loaded: false,
  message: 'Just created',
  connected: false,
  history: []
};

Проверяем. И получаем нашу запись в истории подключения, но почему то с запятыми. Javascript, WTF? Ну да ладно, если мы добавим после мапа и реверса .join(''), то это все решает.


".join('') все решает.", Карл!

Какой у нас результат? Читаем и пишем в стор! Можно себя похвалить! Но этого явно мало, ведь мы делаем это только внутри своей же собственной странички и никак не общаемся с внешним миром.


Коммит: https://github.com/valentinmk/react-redux-universal-hot-example/commit/24144226ea4c08ec1af5db3a5e9b37461be2dbdd


Часть пятая. Проектируем чат


У нас есть заготовка для подключения/отключения к сокету. Теперь мы должны сделать оболочку для чата, она станет нашим рабочем моделью (прототип у нас уже есть).


С чатом мы выполним такие же действия, как и с логом (историей) подключений — добавим историю чата и научим ее выводить.


Полный цикл будет выглядеть так:


  • В редаксе нужно:


    • объявить новую переменную и инициализировать,
    • описать для нее экшены,
    • описать как данная переменна будет изменяться.

  • В компоненте нужно:
    • принять эту переменную,
    • включить ее в отображение,
    • связать кнопку на интерфейсе и экшены.

Настройка редюсера


Начнем с файле ./src/redux/modules/socketexamplemodule.js нам нужно
добавить новую переменную.


const initialState = {
  loaded: false,
  message: 'Just created',
  connected: false,
  history: [],
  message_history: []
};

У нас уже есть экшены SOCKETS_MESSAGE_SENDING и SOCKETS_MESSAGE_RECEIVING. Дополнительных экшенов создавать не будет.


Приступаем к описанию, как будет себя вести нам нужно просто описать как будет работать редюсер.


case SOCKETS_MESSAGE_SENDING:
      return Object.assign({}, state, {
        loaded: true,
        message: 'Send message',
        connected: true,
        message_history: [
          ...state.message_history,
          {
            direction: '->',
            message: action.message_send
          }
        ]
      });
    case SOCKETS_MESSAGE_RECEIVING:
      return Object.assign({}, state, {
        loaded: true,
        message: 'Message receive',
        connected: true,
        message_history: [
          ...state.message_history,
          {
            direction: '<-',
            message: action.message_receive
          }
        ]
      });

Обратите внимание на переменные переменный action.message_receive и action.message_send. С помощью них мы изменяем состояние нашего стора. Переменные будут передаваться внутри экшенов.


Реализуем передачу переменных в стор из экшенов.


export function socketsMessageSending(sendMessage) {
  return {type: SOCKETS_MESSAGE_SENDING, message_send: sendMessage};
}
export function socketsMessageReceiving(sendMessage) {
  return {type: SOCKETS_MESSAGE_RECEIVING, message_receive: sendMessage};
}

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


Мы выполнили работы со стороны редюсера и переходим к настройке отображения и управления из компонента.


Настройка интерфейса


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


Подключаем новую переменную, которую мы получаем из стора в файле ./src/components/SocketExampleComponents/SocketMessageLog.js.


@connect(
  state => ({
    loaded: state.socketexample.loaded,
    message: state.socketexample.message,
    connected: state.socketexample.connected,
    message_history: state.socketexample.message_history
  }),
  socketExampleActions)
export default class SocketMessageLog extends Component {
  static propTypes = {
    loaded: PropTypes.bool,
    message: PropTypes.string,
    connected: PropTypes.bool,
    message_history: PropTypes.array,
    socketsMessageSending: PropTypes.func
  }

Теперь нам нужны функции, которые будут обрабатывать нажатия кнопок на форме.


handleSendButton = (event) => {
    event.preventDefault();
    this.props.socketsMessageSending(this.refs.message_text.value);
    this.refs.message_text.value = '';
  }

Подробнее, забираем по ссылке из поля message_text. Передаем message_text в наш экшен оправки сообщения. Стираем значение в этом поле для ввода нового.


Добавляем переменную в props.


const {loaded, connected, message_history} = this.props;

Выводим лог сообщений, по аналогии с подключением


        <ul>
          {
            message_history.map((messageHistoryElement, index) =>
            <li key={index} className={'unstyled'}>
              <span className={(messageHistoryElement.direction === '->') ? 'glyphicon glyphicon-arrow-right' : 'glyphicon glyphicon-arrow-left'}></span>
              {messageHistoryElement.message}
            </li>
          )}
        </ul>

Не пытайтесь использовать более вложенные ветвления — это у вас не получится. Т.е. не пытайтесь использовать вложенные ' '?' ':' '. Вас будут от этого защищать. Причина — здесь не место вычислений данных. Здесь вообще про интерфейс.

Обновляем форму и кнопки


        <form
          className="form-inline"
          onSubmit={this.handleSendButton}>
          <p></p>
          <div className="form-group">
            <input
              className="form-control input-sm"
              type="text"
              ref="message_text" readOnly = {(loaded && connected === true) ? false : true}>
            </input>
          </div>
          <button
            className="btn btn-primary btn-sm"
            onClick={this.handleSendButton}
            disabled = {(connected === true) ? false : true}>
            <i className="fa fa-sign-in"/> Send
          </button>
        </form>

Тестируем и видим отправленные сообщения.


Давайте имитировать получение сообщения. Будем делать это в лоб.


handleSendButton = (event) => {
    event.preventDefault();
    this.props.socketsMessageSending(this.refs.message_text.value);
    this.props.socketsMessageReceiving(this.refs.message_text.value);
    this.refs.message_text.value = '';
  }

Подробнее, в дополнение к предыдущей версии мы вызывает экшен получения сообщения и передаем в него наше сообщение this.refs.message_text.value.


Не забываем добавить новые элементы в проверку!


static propTypes = {
    loaded: PropTypes.bool,
    message: PropTypes.string,
    connected: PropTypes.bool,
    message_history: PropTypes.array,
    socketsMessageSending: PropTypes.func,
    socketsMessageReceiving: PropTypes.func
  }

Отлично, скучная кропотливая часть закончилась!


Коммит: https://github.com/valentinmk/react-redux-universal-hot-example/commit/5158391cdd53545408637fd732c981f17852e84b.


Мидлваре


Более подробно о мидлваре в редаксе можно почитать на официальном сайте http://redux.js.org/docs/advanced/Middleware.html.


А еще вот та самая статья https://exec64.co.uk/blog/websockets_with_redux/.


Давайте создадим наш собственный мидлваре, в котором будем реализовывать интерфейс к сервису, построенному на websockets.


Первый проход


Создаем новый файл ./src/redux/middleware/socketExampleMiddleware.js


В этот файл нам нужно добавить экшены, которыми мы будем манипулировать. По своему принципу мидлваре напоминает структуру редюсера, но этому будет проиллюстрировано ниже.


Для начала просто проверяем, что концепция работает и делаем тестовый прототип, который будет подтверждением подхода.


import * as socketExampleActions from 'redux/modules/socketexamplemodule';

export default function createSocketExampleMiddleware() {
  let socketExample = null;
  socketExample = true;
  socketExampleActions();

  return store => next => action => {
    switch (action.type) {
      default:
        console.log(store, socketExample, action);
        return next(action);
    }
  };
}

Подробнее. Вообще мидлваре управляет самим стором и как он обрабатывает события и состояния внутри себя. Использую конструкцию return store => next => action => мы вмешиваемся в каждый экшен происходящий в сторе и по полю switch (action.type) выполняем те или иные действия.


У нас сейчас действительно простой пример и логирование в консоль самый просто способ посмотреть, что у нас прилетает в переменных store, socketExample, action. (socketExampleActions(); оставили просто, чтобы не ругался валидатор, вообще они нам понадобятся в будущем).


Не проверяем, у нас ничего не работает, потому что мы не подключили наш класс в мидлваре. Исправляем.


В файле ./src/redux/create.js меняем пару строк.


import createSocketExampleMiddleware from './middleware/socketExampleMiddleware';
//...
  const middleware = [
    createMiddleware(client),
    reduxRouterMiddleware,
    thunk,
    createSocketExampleMiddleware()
  ];

Запускаем проверяем. Теперь в консоли полный беспорядок и это означает, что наш концепт работает!


Коммит: https://github.com/valentinmk/react-redux-universal-hot-example/commit/7833a405be3445e58e8e672e9db03f8cfbfde022


Второй проход. Делаем лог историю.


Мы проверили концепцию и готовы делать нашу боевую модель. Теперь будем подключаться к websockets.


Здесь и далее даются варианты написания кода, которые иллюстрируют ход разработки. Эти примеры содержат преднамеренные ошибки, которые показывают основные технические проблемы и особенности, с которыми я столкнулся в рамках подготовительных работ.

Добавляем в файл ./src/redux/middleware/socketExampleMiddleware.js функции, которыми будем обрабатывать события подключения и отключения.


  const onOpen = (token) => evt => {
    console.log('WS is onOpen');
    console.log('token ' + token);
    console.log('evt ' + evt.data);
  };
  const onClose = () => evt => {
    console.log('WS is onClose');
    console.log('evt ' + evt.data);
  };

убираем лишние объявления (нужно удалить)


  socketExample = true;
  socketExampleActions();

добавляем наши редюсеры и убираем лишнее логирование.


      case 'SOCKETS_CONNECT':
        if (socketExample !== null) {
          console.log('SOCKETS_DISCONNECTING');
          store.dispatch(socketExampleActions.socketsDisconnecting());
          socket.close();
        }
        console.log('SOCKETS_CONNECTING');
        socketExample = new WebSocket('ws://echo.websocket.org/');
        store.dispatch(socketExampleActions.socketsConnecting());
        socketExample.onclose = onClose();
        socketExample.onopen = onOpen(action.token);
        break;
      default:
        return next(action);

Подробнее. Начинаем разбираться. Мы ловим событие SOCKETS_CONNECT, проверяем подключены ли мы, если нет то запускаем принудительное закрытие подключения, создаем новый веб сокет и добавляем ему методы onClose() и onOpen(action.token). Понимает, что сейчас ничего не работает. Мы ловим экшен SOCKETS_CONNECT, которого у нас пока нет. Но у нас есть другой экшен SOCKETS_CONNECTING, почему бы не использовать его — меняем скрипт.


      case 'SOCKETS_CONNECTING':
        if (socketExample !== null) {
          console.log('SOCKETS_DISCONNECTING');
          store.dispatch(SocketExampleActions.socketsDisconnecting());
          socket.close();
        }
        console.log('SOCKETS_CONNECTING');
        socketExample = new WebSocket('ws://echo.websocket.org/');
        store.dispatch(SocketExampleActions.socketsConnecting());
        socketExample.onclose = onClose();
        socketExample.onopen = onOpen(action.token);
        break;
      default:
        return next(action);

!!! Внимание после этого скрипт будет находиться в бесконечном цикле — сохраните все или не нажимайте кнопку подключиться на этом этапе.

Проверяем и видим, что все пошло не так. В консоли постоянные SOCKETS_CONNECTING и SOCKETS_DISCONNECTING. Закрываем вкладку или браузер.


Подробнее. Мидлваре "слушает" стор на предмет экшенов store => next => action => и включается в обработку, когда находит свой экшен SOCKETS_CONNECTING. Далее по коду идет вызов экшена store.dispatch(SocketExampleActions.socketsConnecting());, который в свою очередь вызывает экшен SOCKETS_CONNECTING, который ловит мидлваре и т.д.


Вывод простой — экшены для мидлеваре должны быть всегда отдельными от экшенов, которые происходят на стороне клиентов.


Как быть дальше.


Наш вариант (я думаю он не один) будет таким:


  • пользователь будет вызывать нажатием кнопки экшены мидлвара,
  • который будет вызывать уже "интерфейсные" экшены.

Что на практике будет означать


  • SOCKETS_CONNECT вызывается пользователем
  • при его обработке будет вызываться SOCKETS_CONNECTING,
  • который будет уже обновлять стор и соответствующим образом представлять действие на стороне клиента.

Давайте исправим все это.


Во-первых, нам не хватает экшенов.


Дополняем наши 2 экшена новыми в файле src\redux\modules\socketexamplemodule.js.


export const SOCKETS_CONNECTING = 'SOCKETS_CONNECTING';
export const SOCKETS_CONNECT = 'SOCKETS_CONNECT';
export const SOCKETS_DISCONNECTING = 'SOCKETS_DISCONNECTING';
export const SOCKETS_DISCONNECT = 'SOCKETS_DISCONNECT';

И объявим функции они нам пригодятся.


export function socketsConnecting() {
  return {type: SOCKETS_CONNECTING};
}
export function socketsConnect() {
  return {type: SOCKETS_CONNECT};
}
export function socketsDisconnecting() {
  return {type: SOCKETS_DISCONNECTING};
}
export function socketsDisconnect() {
  return {type: SOCKETS_DISCONNECT};
}

Теперь нужно дать возможность пользователю запускать данные действия. По идеи нужно лезть в ./src/components/SocketExampleComponents/SocketConnectionLog.jsр, но на самом деле управляющие функции ему передают через компонент react. Поэтому правим сначала ./src/containers/SocketExample/SocketExamplePage.js.


static propTypes = {
    loaded: PropTypes.bool,
    message: PropTypes.string,
    connected: PropTypes.bool,
    history: PropTypes.array,
    socketsConnecting: PropTypes.func,
    socketsDisconnecting: PropTypes.func,
//HERE
    socketsConnect: PropTypes.func,
    socketsDisconnect: PropTypes.func
  }
  render() {
//HERE
    const {loaded, message, connected, socketsConnecting, socketsDisconnecting, history, socketsConnect, socketsDisconnect} = this.props;
    return (
      <div className="container">
        <h1>Socket Exapmle Page</h1>
        <Helmet title="Socket Exapmle Page"/>
        <SocketConnectionLog
          loaded={loaded}
          message={message}
          connected={connected}
          connectAction={socketsConnecting}
          disconnectAction={socketsDisconnecting}
          history={history}
//HERE
          connectAction={socketsConnect}
          disconnectAction={socketsDisconnect}
          />
        <SocketMessageLog/>
      </div>
    );
  }

Возвращаемся к ./src/redux/middleware/SocketExampleMiddleware.js и наводим порядок.


Изменяем один кейс


      case 'SOCKETS_CONNECT':

Добавляем кейс на обработку отключения:


      case 'SOCKETS_DISCONNECT':
        if (socketExample !== null) {
          console.log('SOCKETS_DISCONNECTING');
          store.dispatch(socketExampleActions.socketsDisconnecting());
          socketExample.close();
        }
        socketExample = null;
        break;

Для того, чтобы иметь возможность запускать пользовательские экшены при событии disconnect передаем в системное событие также сам стор.


        socketExample.onclose = onClose(store);

и изменяем сам обработчик


  const onClose = (store) => evt => {
    console.log('WS is onClose');
    console.log('evt ' + evt.data);
    store.dispatch(socketExampleActions.socketsDisconnect());
  };

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


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


        socketExample = new WebSocket('ws://echo.websocket.org123/');

Подробнее. Эта проверка связана с тем, что обработка событий у нас идет в асинхронном режиме. Мы не знаем в каком порядке от сокета нам будут прилетать события — последовательно, в обратном порядке или парами. Наш код должен быть способным корректно обрабатывать любые варианты.


Попробуйте самостоятельно переместить store.dispatch(socketExampleActions.socketsDisconnect()); из метода onClose в кейс редюсера и посмотреть что же изменится.


Коммит: https://github.com/valentinmk/react-redux-universal-hot-example/commit/7569536048df83f7e720b000243ed9798308df20


Проход второй. Делаем сообщения


Все аналогично первой части второго прохода.
Добавляем экшены в ./src/redux/modules/socketexamplemodule.js


export const SOCKETS_MESSAGE_SENDING = 'SOCKETS_MESSAGE_SENDING';
export const SOCKETS_MESSAGE_SEND = 'SOCKETS_MESSAGE_SEND';
export const SOCKETS_MESSAGE_RECEIVING = 'SOCKETS_MESSAGE_RECEIVING';
export const SOCKETS_MESSAGE_RECEIVE = 'SOCKETS_MESSAGE_RECEIVE';

Добавляем обработчики


export function socketsMessageSending(sendMessage) {
  return {type: SOCKETS_MESSAGE_SENDING, message_send: sendMessage};
}
export function socketsMessageSend(sendMessage) {
  return {type: SOCKETS_MESSAGE_SEND, message_send: sendMessage};
}
export function socketsMessageReceiving(receiveMessage) {
  return {type: SOCKETS_MESSAGE_RECEIVING, message_receive: receiveMessage};
}

Стоп. Почему не 4 обработчика? Подробнее. Нам, на самом деле, нам не нужна обработка socketsMessageReceive, потому что пользователю не нужно вмешиваться в процесс получения сообщения. Хотя на будущее этим событием мы можем отмечать факт отображения сообщения у пользователя в его интерфейсе, т.е. тот самый признак "прочитано" (но это за пределами этой статьи).


Прием сообщения


Переходим к описанию обработки событий от сокета в файле ./src/redux/middleware/socketExampleMiddleware.js.


В нашем обработчике получаем событие от сокета, извлекаем из него сообщение и передаем в стор через экшен.


  const onMessage = (ws, store) => evt => {
    // Parse the JSON message received on the websocket
    const msg = evt.data;
    store.dispatch(SocketExampleActions.socketsMessageReceiving(msg));
  };

      case 'SOCKETS_CONNECT':
        if (socketExample !== null) {
          console.log('SOCKETS_DISCONNECTING');
          store.dispatch(SocketExampleActions.socketsDisconnecting());
          socket.close();
        }
        console.log('SOCKETS_CONNECTING');
        socketExample = new WebSocket('wss://echo.websocket.org/');
        store.dispatch(SocketExampleActions.socketsConnecting());
        socketExample.onmessage = onMessage(socketExample, store);
        socketExample.onclose = onClose(store);
        socketExample.onopen = onOpen(action.token);
        break;

Отправка сообщения


В самом мидлваре пишем редюсер.


      case 'SOCKETS_MESSAGE_SEND':
        socketExample.send(action.message_send);
        store.dispatch(SocketExampleActions.socketsMessageSending(action.message_send));
        break;

Подробнее. action.message_send — это о чем? Все, что мы кладем в стор появляется в процессе обработки store => next => action => в этих переменных. Когда мы запускаем экшен, то в этой переменной передается все с чем мы этот экшен запустили.


Давайте реализуем как в экшене появится сообщение.


Правим файл ./src/components/SocketExampleComponents/SocketMessageLog.js, чтобы получить возможность запускать экшен от пользователя.


  static propTypes = {
    loaded: PropTypes.bool,
    message: PropTypes.string,
    connected: PropTypes.bool,
    message_history: PropTypes.array,
    socketsMessageSend: PropTypes.func
  }

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


  handleSendButton = (event) => {
    event.preventDefault();
    this.props.socketsMessageSend(this.refs.message_text.value);
    this.refs.message_text.value = '';
  }

Подробнее. Мы получим новые сообщения сразу их стора по факту в переменной message_history и react на сразу отрисует их. Для того, чтобы отправить сообщение мы вызываем экщен мидлваре this.props.socketsMessageSend(this.refs.message_text.value), тем самым в action мы передаем наше сообщение, которое обрабывается редюсером мидлваре SOCKETS_MESSAGE_SEND, который в свою очередь вызывает событие SOCKETS_MESSAGE_SENDING, которое обрабатывается и отрисовывается интефейсным редюсером.


Запускаем. Проверяем.


Финиш!


[Заметки на полях] Оглянитесь, вспомните себя в начале этой статьи. Сейчас вы сможете развернуть и быстро создать интерфейс к вашему бэкэнду с получением и обработкой данных в реальном времени. Если у вас появились интересные задумки, не откладывайте — делайте.

[Заметки на полях] А вдруг я это все не зря.

Коммит: https://github.com/valentinmk/react-redux-universal-hot-example/commit/40fd0b38f7a4b4acad141997e1ad9e7d978aa3b3


PS


Рабочая копия данного материала размещена тут.

Поделиться публикацией

Комментарии 32

    0

    Приятно читать такие статьи, все по делу, без нытья, что фронт это сложно, и js, как всегда, убог.
    Рад, что у вас все получилось, так держать!

      0
      Спасибо. Говоря честно, все «нытье» и размышления о js при втором проходе по статье я категорически убрал. js с сахаром и таким набором фреймворков становится почти приятным)
      0
      Одна из лучших статей за последнее время. Спасибо автору!
        0
        Благодарю! Это мой лучший мотиватор)
        +2
        Проверяем. И получаем нашу запись в истории подключения, но почему то с запятыми. Javascript, WTF?

        Ну так понятное дело. history.map(..).reverse() — это массив, а значение textarea.value должно быть строкой. У массивов же метод toString реализован как, грубо говоря, return this.join(',').

          0
          А, вот оно в чем дело! Но я потратил лишний час жизни на стаке разбираясь, почему у меня возникает запятая на фактически ровном месте)
          +1
          Статья хорошая, но как по мне, прежде всего бэкендеру, для таких задач Redux — это из пушки по воробьям. MobX для таких задач показывает себя лучше как по количеству кода, так и, и это главное, по понятности для бэкендера, впитавшего ООП и прочую MVC-like императивщину, с молоком матери. Сторы там обычные объекты, хоть на верхнем уровне, хоть где угодно, экшенами могут быть методы этих или других объектов (включая методы компонента), а могут быть глобальные функции.
            0
            Да, поддерживаю и рекомендую обратить на Mobx внимание.
            Есть один момент: не смог найти «расширенный» бойлерплейт, так чтобы сразу в нем был более менее полный интерфейс. Есть подсказки?
              0
              Если под интерфейсом имеете в виду UI, то таковых не встречалось. По простой, по-моему, причине — можно подключить любую адаптированную под React UI либу. Как-то так исторически сложилось, что я использую для «админок» Material-UI, хотя всё больше в ней разочаровываюсь (прежде всего из-за инлайн стилей) и смотрю в сторону React-Bootstrap
                0
                Да, под интерфейсом имеется ввиду набор преднастроенных технологий и стартовый шаблон с менюшками и пр., чтобы вообще не заниматься этим. Либо заниматься на уровне конфигурирования готового «скелета». У нас же «черновик» интерфейса.
                В примере, который взят за основу, используется React-Bootstrap.
                  +1
                  Взгляните на react-toolbox. Отличное и гибкое решение темизации через склейку класснеймов из css-модулей.
                    0
                    Я правильно понимаю, что там генерируются CSS-файлы, а не инлайн стили у элементов используются?
                      0
                      Еще лучше, ничего не генерируется, там используются css-модули, которые позволяют использовать обычный css (с любым процессингом) с инкапсуляцией. Расширение темы компонента происходит через добавление кастомных класснеймов к его собственным по заданным в интерфейсе ключам.
                      За это отвечает react-css-themr, и, собственно, его можно использовать и без react-toolbox.
            0
            Давно таких познавательных статей не было.
              0
              Спасибо!
              +1
              Пожалуй без знания основ было бы сложно понять «что тут происходит», но зная основу, практика заходит «на ура». Тут есть неплохой скринкаст (там и по Реакту есть).
                0
                Отличное дополнение, для более глубокого погружения и понимания, то что нужно
                  +1

                  Рекомендую ещё такое — шедеврально!

                +1
                базовое знание javascript (тут нужно поискать в интернете справочник по крайней версии js стандартов ES-2015)

                Лучшее, что я видел — Обзор базовых возможностей ES6

                  0
                  Занес в TODO
                  +1
                  Вот тут базовом примере есть странная нестыковка, все предлагается писать в одном файле, не разделяя на файл экшенов и редюсеров, ну допустим.

                  Это фича

                    0
                    Это особенно ценно, занес в TODO.
                    Кстати читал же про уток, но как-то не придал значения.
                      0
                      Action Types всегда должны лежать в отдельном файле, так как, если у вас связанные сущности, например Tab -> Widget, и по редьюсеру на каждую сущность, файлы с редьюсерами будут циклично референсить друг друга.
                      Например, при экшене TAB_DELETE нужно зачистить табу и все виджеты, к ней привязанные.
                        0
                        Тема со структурой проекта как оказалось очень важная.
                        Я правильно понимаю, что под отдельным файлом подразумевается отдельный общий файл со всеми экшенами проекта?

                        Плюс, а можно ли проиллюстрировать суть замечания каким либо примером. Я просто не могу понять, почему
                        Action Types всегда должны лежать в отдельном файле

                        Хочется от «частного к общему» разобраться.
                          +1
                          отдельный общий файл со всеми экшенами проекта
                          Именно. Самое важное, чтобы в нем лежали только константы экшенов (ну или на худой конец сами action creators, если их немного).
                          Далее, действительно логично делить редьюсеры по сущностям и выделять под каждую по файлу. Таким образом, если в двух разных редьюсерах нужно реагировать на экшены для соседнего, то, имея все константы в одном внешнем файле (ну или хотя бы в директории, главное не в файлах с редьюсерами), можно их без проблем зареференсить.

                          Пример:
                          //actions/types.js
                          export const TAB_DELETE = 'TAB_DELETE';
                          
                          //actions/tab.js
                          import {TAB_DELETE} from './types';
                          export const tabDelete = id => ({
                            type: TAB_DELETE,
                            payload: {
                              id
                            }
                          });
                          
                          //reducers/tab.js
                          import {TAB_DELETE} from '../actions/types';  //no crossreference between tab and widget
                          export const tab = (state = {}, action) => {
                            const {type, payload} = action;
                            switch (type) {
                              case TAB_DELETE: {
                                //delete tab with id === payload.id
                                return state;
                              }
                              default: {
                                return state;
                              }
                            }
                          };
                          
                          //recuders/widget.js
                          import {TAB_DELETE} from '../actions/types'; //no crossreference between widget and tab
                          export const widget = (state = {}, action) => {
                            const {type, payload} = action;
                            switch (type) {
                              case TAB_DELETE: {
                                //delete all widgets with tabId === payload.id
                                return state;
                              }
                              default: {
                                return state;
                              }
                            }
                          };
                          
                            0

                            Отлично! Спасибо большое. В TODO к статье занес поправить этот кусок.


                            Действительно у Эрика получается этого в явном виде не выделяется, хотя он пишет в своем походе https://github.com/erikras/ducks-modular-redux:


                            A module…
                            1. MUST export default a function called reducer()
                            2. MUST export its action creators as functions
                            3. MUST have action types in the form npm-module-or-app/reducer/ACTION_TYPE
                            4. MAY export its action types as UPPER_SNAKE_CASE, if an external reducer needs to listen for them, or if it is a published reusable library


                            These same guidelines are recommended for {actionType, action, reducer} bundles that are shared as reusable Redux libraries.

                            Вот что он понимает под "такие же правила рекомендованы для actionType, action, reducer" я не понимаю.


                            С другой стороны в его подходе реально не учитывается вложенность, а это значит что не может быть связанного Tab и Widget, но тогда будет 3 разных объекта Tab, Widget и TabWidget.

                              0
                              С другой стороны в его подходе реально не учитывается вложенность, а это значит что не может быть связанного Tab и Widget, но тогда будет 3 разных объекта Tab, Widget и TabWidget.
                              Ну это как-то странно совсем. Получается эта методология не помогает решать проблемы, а создает новые. Сущности-то — это табы и виджеты, ладно б у них связь многие ко многим была, я б еще понял, но простую связку хранить в трех модулях только для разрешения циркурлярных зависимостей — мне кажется, это перебор.

                              Опять же, иметь перед глазами один файл с вообще всеми возможными экшенами в приложении — удобно. Еще Абрамов в самом начале про это писал. Но это, конечно, вкусовщина.

                              Про дефолтный экспорт согласен, но это опять вкусовщина.
                                0

                                Поправка, если (и только если следовать обозначенным принципам), то у нас получается один уровень вложенности для модулей, а это приведет нас к тому что будет либо Tab, либо Widget, либо монструозный TabWidget. Короче для всех разновидностей мы будем делать отдельные модули. (Ну это допустим следуя поговорке "Если нельзя, но очень хочется, то можно").


                                Опять же, иметь перед глазами один файл с вообще всеми возможными экшенами в приложении — удобно. Еще Абрамов в самом начале про это писал. Но это, конечно, вкусовщина.

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

                                  0
                                  Ну, большинство приложений — все же CRUD-мордашки к бэкенду, а там без связности/вложенности никуда.

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

                                  А если разные виджеты можно как-то настраивать и сохранять эти настройки в пресеты? А если, а если… Плоская модульная структура начинает трещать по швам.
                              0

                              Тут ключевой вопрос: зачем редюсер виджета отрабатывает экшен таба?


                              "Кто такой Студебеккер? Это ваш родственник Студебеккер? Папа ваш Студебеккер?" :)

                                0
                                Ну а как вы иначе удалите виджеты при удалении табы? В редьюсере табы полезете в глобальный стейт?
                                Примеров уйма, на самом деле. Вот лежат у пользователя в настройках айдишники его созданных табов. Жмет он на удаление табы, экшен должен попасть в оба редьюсера. Если, конечно, у вас данные нормализованы (с этого надо было начать, наверное).
                        0
                        del (промахнулся)

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

                        Самое читаемое