Это краткое руководство и обучение по фронтэнеду для бэкендера. В данном руководстве я решаю проблему быстрого построения пользовательского интерфейса к серверному приложению в виде одностраничного веб-приложения (single page app).
Основной целью моего исследования является возможность за разумное время (для одного нормального человека) получить удобный и простой в использовании интерфейс-черновик к серверному приложению. Мы (как разработчики серверной части) понимаем, что наш приоритет — серверная часть. Когда (в гипотетическом проекте) появятся во фронте профи своего дела, они все сделают красиво и "правильно".
В роли учебной задачи представлена страничка чата с каким-то умозрительным "ботом", который работает на стороне сервера и принимает сообщение только через WebSocket. Бот при этом выполняет эхо ваших сообщений (мы тут не рассматриваем серверную часть вообще).
Мне для изложения материала требуется, чтобы вы имели:
- базовое знание javascript (тут нужно поискать в интернете справочник по крайней версии js стандартов ES-2015)
- знание reactjs (на уровне обучения https://facebook.github.io/react/tutorial/tutorial.html)
- понятие о websockets (это очень просто, главное чтобы ваш сервер это умел)
- знание и умение использовать bootstrap (на уровне этого раздела http://getbootstrap.com/css/)
Что используем
Redux — официальная документация расположена по адресу http://redux.js.org. По-русски есть несколько вариантов, я лично использовал в основном https://rajdee.gitbooks.io/redux-in-russian/content/docs/introduction/index.html.
Статью exec64, она стала причиной написать этот тутриал https://exec64.co.uk/blog/websockets_with_redux/.
Готовый сервер с react и redux от https://github.com/erikras/react-redux-universal-hot-example(он нам спасает человеко-месяцы времени по настройке большой связки технологий, которые необходимы для современного js проекта)
Мотивация
Вообще я разрабатываю приложение на языке Python. Погоди-погоди уходить ...
Что мне было нужно:
- мне нужно чтобы реализация интерфейса не диктовала мне выбор технологий на стороне серверной части
- современные технологии (мне нечего было терять или быть чем-то обязанным "старым проверенным приемам")
- это должно быть одностраничное приложений (я уже сам выберу, где можно обновлять страницу целиком)
- мне нужна реакция пользовательского интерфейса в реальном времени на серверные события
- мне нужен обмен информацией сервер-клиент (а не клиент-сервер) в реальном времени
- мне нужна возможность генерировать обновления клиента на сервере
Что было испробовано:
- вариации на тему на чистом js (устарело, есть много полезных моделей велосипеда)
- JQuery (
уже не могу ТАК извратить так свой мозг, крайне сложный для быстрого старта синтаксис и… это дело профессионалов) - Angular (переход на 2 версию спугнул и не нашел за отведенное время лазейки к решению моей задачи)
- Socket.io (там все реализовано, если вы node.js программист вы уже его используете, но он слишком сильно привязывает серверную часть на node, мне нужен только клиент без третьих лиц)
Выбрано в итоге:
- React (понятно и доступно/просто + babel = делает язык вполне понятным)
- Redux (импонирует использование
единой помойкиединого хранилища) - WebSockets (очень просто и не связывает руки, а позволяет внутри себя уже применять такой формат какой позволит фантазия)
Упрощения и допущения:
- Мы не будем использовать авторизации в приложении
- Мы не будет использовать авторизации в WebSocket-ах
- Мы будем использовать самое доступное приложение Websocket Echo (https://www.websocket.org/echo.html)
Содержание
- Часть первая. Первоначальная настройка. Настройка одной страницы
- Часть вторая. Проектирование будущего приложения
- Часть третья. Изумительный Redux
- Часть четвертая. Оживляем историю
- Часть пятая. Проектируем чат
- Часть шестая. Мидлваре
Как читать
Не будете повторять — пропускайте часть 1
Знаете reactjs — пропускайте часть 2
Знаете redux — пропускайте части 3, 4 и 5
Знаете как работает middleware в redux — смело читайте часть 6 и далее в обратном порядке.
Часть первая. Первоначальная настройка. Настройка страницы.
Настройка окружения
Нам нужен node.js и npm.
Ставим node.js с сайта https://nodejs.org — а именно этот гайд написан на 6ой версии, версию 7 тестировал — все работает.
npm устанавливается вместе с node.js
Далее нужно запустить npm и обновить node.js (для windows все тоже самое без npm)
sudo npm cache clean -f
sudo npm install -g n
sudo n stable
проверяем
node -v
Настройка react-redux-universal-hot-example
Все выложено в react-redux-universal-hot-example, там же инструкция по установке.
Тут привожу последовательность действий
- Скачиваем и разархивируем архив/форкаем/что-угодно-как-вам-нравится.
- Через node.js command line или терминал переходим в эту папку
- Запускаем
npm install
npm run dev
Переходим на http://localhost:3000 и должны видеть стартовую страницу.
Если все ок — приступаем.
Создаем новый контейнер
Для настройки раздела используем предоставленную справку от команды react-redux-universal-hot-example. Оригинал статьи находится тут.
cd ./src/containers && mkdir ./SocketExample
Копируем туда hello.js как шаблон странички
cp About/About.js Hello/SocketExamplePage.js
Я использую для всего этого Atom, как действительно прекрасный редактор-чего-угодно с некоторыми плюшками.
Правим скопированный файл
Создаем заглушку под нашу страница. Вводим элемент <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.
Для этого нужно:
- Создать редюсер
- Создать экшены
- И подключить все это в общую систему
Про экшены написано В официальной документации
Про редюсеры написано Там же
Создаем файл
Создаем файл ./src/redux/modules/socketexamplemodule.js
и наполняем базовыми экшенами и редюсерами. Вот тут базовом примере есть странная нестыковка, все предлагается писать в одном файле, не разделяя на файл экшенов и редюсеров, ну допустим. Все равно — мы тут все взрослые люди (we are all adults).
Экшены 1
export const SOCKETS_CONNECTING = 'SOCKETS_CONNECTING';
export const SOCKETS_DISCONNECTING = 'SOCKETS_DISCONNECTING';
export const SOCKETS_MESSAGE_SENDING = 'SOCKETS_MESSAGE_SENDING';
export const SOCKETS_MESSAGE_RECEIVING = 'SOCKETS_MESSAGE_RECEIVING';
Все экшены мы будем запускать по нажатию кнопок, кроме события SOCKETS_MESSAGE_RECEIVING
, который мы будем синтетически вызывать вслед за отправкой сообщения. Это делается, чтобы в процессе разработки эмулировать недостающие в настоящий момент (или на конкретном этапе) функционал серверной части приложения.
Редюсер
Добавляем в тот же файл.
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case SOCKETS_CONNECTING:
return Object.assign({}, state, {
loaded: true,
message: 'Connecting...',
connected: false
});
case SOCKETS_DISCONNECTING:
return Object.assign({}, state, {
loaded: true,
message: 'Disconnecting...',
connected: true
});
case SOCKETS_MESSAGE_SENDING:
return Object.assign({}, state, {
loaded: true,
message: 'Send message',
connected: true
});
case SOCKETS_MESSAGE_RECEIVING:
return Object.assign({}, state, {
loaded: true,
message: 'Message receive',
connected: true
});
default:
return state;
}
}
Более подробно про структуру reducer и зачем Object.assign({}, state,{});
можно прочитать тут.
Вы заметили инициализацию state = initialState, которой мы не объявили (поставьте ESLint или его аналог — сильно упростит жизнь Нормального Человека). Добавим объявление до редюсера. Это будет первое состояние, которое мы будем иметь в нашем сторе на момент загрузки страницы, ну точнее страница будет загружаться уже с этим первоначальным состоянием.
const initialState = {
loaded: false,
message: 'Just created',
connected: false,
};
Экшены 2
Теперь продолжим с нашими экшенами и на этом завершим этот модуль. Мы должны описать, как они будут изменять состояние reducer'a.
Добавляем в тот же файл.
export function socketsConnecting() {
return {type: SOCKETS_CONNECTING};
}
export function socketsDisconnecting() {
return {type: SOCKETS_DISCONNECTING};
}
export function socketsMessageSending() {
return {type: SOCKETS_MESSAGE_SENDING};
}
export function socketsMessageReceiving() {
return {type: SOCKETS_MESSAGE_RECEIVING};
}
Подключаем в общий редюсер
На данный момент в приложении ничего не поменяется. Включаем наш модуль в общий конструктор reducer'ов.
В фале ./src/redux/modules/reducer.js
прописываем модуль.
import socketexample from './socketexamplemodule';
и включаем его в общую структуру результирующего редюсера
export default combineReducers({
routing: routerReducer,
reduxAsyncConnect,
auth,
form,
multireducer: multireducer({
counter1: counter,
counter2: counter,
counter3: counter
}),
info,
pagination,
widgets,
// our hero
socketexample
});
Запускаем сервер, проверяем и ура в DevTools мы видим.
Если вопросы с initialState остались, то попробуйте их поменять или добавить новую переменную в него.
Коммит: https://github.com/valentinmk/react-redux-universal-hot-example/commit/0c984e3b5bc25056aa578ee57f90895bc6baaf18
Стор
А стор у нас уже создан и редюсер в него подключен. Ничего не делаем.
Если подробнее, то вы должны помнить, как мы добавили наш редюсер в combineReducers
выше по статье. Так вот этот combineReducers
сам включается в стор, который создаётся в файле ./src/redux/create.js
.
Подключаем стор к react компонентам
Подключаем все это теперь в наши модули. В целях всесторонней демонстрации начнем с модуля истории и сделаем из него чистый компонент react (в смысле чистый от redux).
Компонент SocketConnectionLog
мы пока не трогаем, а идем сразу в контейнер SocketExamplePage
.
В данном контейнере мы будем подключать и получать данные из redux.
Подключаем библиотеку в файле ./src/containers/SocketExample/SocketExamplePage.js
.
import {connect} from 'react-redux';
Забираем экшены, чтобы потом их использовать у себя в react.
import * as socketExampleActions from 'redux/modules/socketexamplemodule';
а еще мы поменяем строку, чтобы подключить PropTypes
import React, {Component, PropTypes} from 'react';
Пишем коннектор, которым будем забирать данные из нашего редюсера.
@connect(
state => ({
loaded: state.socketexample.loaded,
message: state.socketexample.message,
connected: state.socketexample.connected}),
socketExampleActions)
Как вы видите state.socketexample.loaded
это обращение в redux, в той структуре, которую мы видим в DevTools.
Теперь подключаем проверки данных, получаемых из redux, что видится целесообразным т.к. любые проверки данных на тип есть вселенское добро.
static propTypes = {
loaded: PropTypes.bool,
message: PropTypes.string,
connected: PropTypes.bool
}
Мы получили данные теперь давайте их передавать. Внутри блока render объявляем и принимаем данные уже теперь из props.
const {loaded, message, connected} = this.props;
и спокойно и уверенно передаем их в наш модуль:
<SocketConnectionLog loaded={loaded} message={message} connected={connected} />
Мы передали новые данные (через react) в компонент. Теперь переписываем наш компонент, который уже ничего не знает про стор (redux), а только обрабатывает переданные ему данные.
В файле ./src/components/SocketExampleComponents/SocketConnectionLog.js
действуем по списку:
- проверяем полученные props
- присваиваем их внутри render
- используем в нашем компоненте
Начнем, импортируем недостающие библиотеки:
import React, {Component, PropTypes} from 'react';
добавляем проверку:
static propTypes = {
loaded: PropTypes.bool,
message: PropTypes.string,
connected: PropTypes.bool
}
объявляем и присваиваем переменные, переданные через props
const {loaded, message, connected} = this.props;
используем для вывода наши переменные
value={'index =' + 0 + ', loaded = ' + loaded + ', message = ' + message + ', connected = ' + connected}/>
{/* value="
index = 2, loaded = true, message = Connected, connected = true
index = 1, loaded = false, message = Connecting..., connected = false"/>
*/}
Проверяем и видим, initialState прилетает к нам прямо из redux->react->props->props.
Коммит: https://github.com/valentinmk/react-redux-universal-hot-example/commit/60ac05332e35dfdbc11b9415f5bf5c46cd740ba8
SocketExampleMessageLog
Теперь переходим к компоненту SocketExampleMessageLog
и сделаем его абсолютно самостоятельным, в смысле работы со стором. Мы не будем передавать в него никакие props, он будет получать все, что ему нужно из стор сам.
Открываем файл ./src/components/SocketExampleComponents/SocketMessageLog.js
в нем добавляем необходимые нам библиотеки
import React, {Component, PropTypes} from 'react';
import {connect} from 'react-redux';
import * as socketExampleActions from 'redux/modules/socketexamplemodule';
добавляем connect
, проверку типов и используем полученные данные
@connect(
state => ({
loaded: state.socketexample.loaded,
message: state.socketexample.message,
connected: state.socketexample.connected}),
socketExampleActions)
export default class SocketMessageLog extends Component {
static propTypes = {
loaded: PropTypes.bool,
message: PropTypes.string,
connected: PropTypes.bool
}
// ...
Не забываем передать значение в метод render() через props
const {loaded, message, connected} = this.props;
Мы будем использовать loaded
и connected
, чтобы определять готовность к обмену сообщения, а message
выведем просто для проверки.
<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)
aqrln
22.12.2016 03:14+2Проверяем. И получаем нашу запись в истории подключения, но почему то с запятыми. Javascript, WTF?
Ну так понятное дело.
history.map(..).reverse()
— это массив, а значениеtextarea.value
должно быть строкой. У массивов же методtoString
реализован как, грубо говоря,return this.join(',')
.valentinmk
22.12.2016 12:11А, вот оно в чем дело! Но я потратил лишний час жизни на стаке разбираясь, почему у меня возникает запятая на фактически ровном месте)
VolCh
22.12.2016 08:53+1Статья хорошая, но как по мне, прежде всего бэкендеру, для таких задач Redux — это из пушки по воробьям. MobX для таких задач показывает себя лучше как по количеству кода, так и, и это главное, по понятности для бэкендера, впитавшего ООП и прочую MVC-like императивщину, с молоком матери. Сторы там обычные объекты, хоть на верхнем уровне, хоть где угодно, экшенами могут быть методы этих или других объектов (включая методы компонента), а могут быть глобальные функции.
valentinmk
22.12.2016 12:52Да, поддерживаю и рекомендую обратить на Mobx внимание.
Есть один момент: не смог найти «расширенный» бойлерплейт, так чтобы сразу в нем был более менее полный интерфейс. Есть подсказки?VolCh
22.12.2016 13:33Если под интерфейсом имеете в виду UI, то таковых не встречалось. По простой, по-моему, причине — можно подключить любую адаптированную под React UI либу. Как-то так исторически сложилось, что я использую для «админок» Material-UI, хотя всё больше в ней разочаровываюсь (прежде всего из-за инлайн стилей) и смотрю в сторону React-Bootstrap
valentinmk
22.12.2016 13:52Да, под интерфейсом имеется ввиду набор преднастроенных технологий и стартовый шаблон с менюшками и пр., чтобы вообще не заниматься этим. Либо заниматься на уровне конфигурирования готового «скелета». У нас же «черновик» интерфейса.
В примере, который взят за основу, используется React-Bootstrap.raveclassic
22.12.2016 17:36+1Взгляните на react-toolbox. Отличное и гибкое решение темизации через склейку класснеймов из css-модулей.
VolCh
22.12.2016 20:32Я правильно понимаю, что там генерируются CSS-файлы, а не инлайн стили у элементов используются?
raveclassic
22.12.2016 20:57Еще лучше, ничего не генерируется, там используются css-модули, которые позволяют использовать обычный css (с любым процессингом) с инкапсуляцией. Расширение темы компонента происходит через добавление кастомных класснеймов к его собственным по заданным в интерфейсе ключам.
За это отвечает react-css-themr, и, собственно, его можно использовать и безreact-toolbox
.
LiguidCool
22.12.2016 13:58+1Пожалуй без знания основ было бы сложно понять «что тут происходит», но зная основу, практика заходит «на ура». Тут есть неплохой скринкаст (там и по Реакту есть).
valentinmk
22.12.2016 14:02Отличное дополнение, для более глубокого погружения и понимания, то что нужно
raveclassic
Приятно читать такие статьи, все по делу, без нытья, что фронт это сложно, и js, как всегда, убог.
Рад, что у вас все получилось, так держать!
valentinmk
Спасибо. Говоря честно, все «нытье» и размышления о js при втором проходе по статье я категорически убрал. js с сахаром и таким набором фреймворков становится почти приятным)