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


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

Поделиться с друзьями
-->

Комментарии (17)


  1. raveclassic
    21.12.2016 22:15

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


    1. valentinmk
      22.12.2016 12:08

      Спасибо. Говоря честно, все «нытье» и размышления о js при втором проходе по статье я категорически убрал. js с сахаром и таким набором фреймворков становится почти приятным)


  1. nightvich
    21.12.2016 23:24

    Одна из лучших статей за последнее время. Спасибо автору!


    1. valentinmk
      22.12.2016 12:08

      Благодарю! Это мой лучший мотиватор)


  1. aqrln
    22.12.2016 03:14
    +2

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

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


    1. valentinmk
      22.12.2016 12:11

      А, вот оно в чем дело! Но я потратил лишний час жизни на стаке разбираясь, почему у меня возникает запятая на фактически ровном месте)


  1. VolCh
    22.12.2016 08:53
    +1

    Статья хорошая, но как по мне, прежде всего бэкендеру, для таких задач Redux — это из пушки по воробьям. MobX для таких задач показывает себя лучше как по количеству кода, так и, и это главное, по понятности для бэкендера, впитавшего ООП и прочую MVC-like императивщину, с молоком матери. Сторы там обычные объекты, хоть на верхнем уровне, хоть где угодно, экшенами могут быть методы этих или других объектов (включая методы компонента), а могут быть глобальные функции.


    1. valentinmk
      22.12.2016 12:52

      Да, поддерживаю и рекомендую обратить на Mobx внимание.
      Есть один момент: не смог найти «расширенный» бойлерплейт, так чтобы сразу в нем был более менее полный интерфейс. Есть подсказки?


      1. VolCh
        22.12.2016 13:33

        Если под интерфейсом имеете в виду UI, то таковых не встречалось. По простой, по-моему, причине — можно подключить любую адаптированную под React UI либу. Как-то так исторически сложилось, что я использую для «админок» Material-UI, хотя всё больше в ней разочаровываюсь (прежде всего из-за инлайн стилей) и смотрю в сторону React-Bootstrap


        1. valentinmk
          22.12.2016 13:52

          Да, под интерфейсом имеется ввиду набор преднастроенных технологий и стартовый шаблон с менюшками и пр., чтобы вообще не заниматься этим. Либо заниматься на уровне конфигурирования готового «скелета». У нас же «черновик» интерфейса.
          В примере, который взят за основу, используется React-Bootstrap.


          1. raveclassic
            22.12.2016 17:36
            +1

            Взгляните на react-toolbox. Отличное и гибкое решение темизации через склейку класснеймов из css-модулей.


            1. VolCh
              22.12.2016 20:32

              Я правильно понимаю, что там генерируются CSS-файлы, а не инлайн стили у элементов используются?


              1. raveclassic
                22.12.2016 20:57

                Еще лучше, ничего не генерируется, там используются css-модули, которые позволяют использовать обычный css (с любым процессингом) с инкапсуляцией. Расширение темы компонента происходит через добавление кастомных класснеймов к его собственным по заданным в интерфейсе ключам.
                За это отвечает react-css-themr, и, собственно, его можно использовать и без react-toolbox.


  1. lfo
    22.12.2016 12:11

    Давно таких познавательных статей не было.


    1. valentinmk
      22.12.2016 12:11

      Спасибо!


  1. LiguidCool
    22.12.2016 13:58
    +1

    Пожалуй без знания основ было бы сложно понять «что тут происходит», но зная основу, практика заходит «на ура». Тут есть неплохой скринкаст (там и по Реакту есть).


    1. valentinmk
      22.12.2016 14:02

      Отличное дополнение, для более глубокого погружения и понимания, то что нужно