Итак, нам нужно, чтобы клиентское приложение реагировало на события, генерируемые на сервере. Обычно в подобных случаях речь идёт о приложениях реального времени. В таком сценарии сервер передаёт клиенту свежие данные по мере их появления. После того, как между клиентом и сервером будет установлено соединение, сервер, не полагаясь на запросы клиента, самостоятельно инициирует передачу данных.
Подобная модель подходит для множества проектов. Среди них — чаты, игры, торговые системы, и так далее. Часто предложенный в этом материале подход используют для того, чтобы повысить скорость отклика системы, при том, что сама по себе такая система может функционировать и без него. Мотивы разработчика в данном случае не важны. Полагаем, допустимо предположить, что вы это читаете, так как вам хочется узнать о том, как использовать сокеты при создании React-приложений и приблизить вашу разработку к приложениям реального времени.
Здесь мы покажем очень простой пример. А именно — создадим сервер на Node.js, который может принимать подключения от клиентов, написанных на React, и отправлять в сокет, с заданной периодичностью, сведения о текущем времени. Клиент, получая свежие данные, будет выводить их на странице приложения. И клиент и сервер используют библиотеку Socket.io.
Настройка рабочей среды
Предполагается, что у вас установлены базовые инструменты, такие, как Node.js и NPM. Кроме того, вам понадобится NPM-пакет
create-react-app
, поэтому, если его у вас ещё нет, установите его глобально такой командой:npm --global i create-react-app
Теперь можно создать React-приложение
socket-timer
, с которым мы будем экспериментировать, выполнив такую команду:create-react-app socket-timer
Теперь приготовьте ваш любимый текстовый редактор и найдите папку, в которой расположены файлы приложения
socket-timer
. Для того, чтобы его запустить, достаточно выполнить, с помощью терминала, команду npm start
.В нашем примере серверный и клиентский код будут расположены в одной папке, но подобное не стоит делать в рабочих проектах.
Socket.io на сервере
Создадим сервер, поддерживающий вебсокеты. Для того, чтобы это сделать, перейдите в терминал, переключитесь на папку приложения и установите Socket.io:
npm i --save socket.io
Теперь создайте файл
server.js
в корне папки. В этом файле, для начала, импортируйте библиотеку и создайте сокет:const io = require('socket.io')();
Теперь можно использовать переменную
io
для работы с сокетами. Вебсокеты — это долгоживущие двусторонние каналы связи между клиентом и сервером. На сервере надо принять запрос на соединение от клиента и поддерживать подключение. Используя это соединение, сервер сможет публиковать (генерировать) события, которые будет получать клиент.Сделаем следующее:
io.on('connection', (client) => {
// тут можно генерировать события для клиента
});
Далее, нужно сообщить Socket.io о том, на каком порту требуется ожидать подключения клиента.
const port = 8000;
io.listen(port);
console.log('listening on port ', port);
На данном этапе можно перейти в терминал и запустить сервер, выполнив команду
node server
. Если всё сделано правильно, вы увидите сообщение об его успешном запуске: listening on port 8000
.Сейчас сервер бездействует. Доступ к каналу связи с клиентом имеется, но канал пока простаивает. Канал связи двусторонний, поэтому сервер может не только передавать клиенту данные, но и реагировать на события, которые генерирует клиент. Этот механизм можно рассматривать как серверный обработчик событий, привязанный к конкретному событию конкретного клиента.
Для того, чтобы завершить работу над серверной частью приложения, надо запустить таймер. Необходимо, чтобы сервис запускал новый таймер для каждого подключившегося к нему клиента, при этом нужно, чтобы клиент мог передать серверу сведения о том, с каким интервалом он хочет получать данные. Это важный момент, демонстрирующий возможности двусторонней связи клиента и сервера.
Отредактируйте код в
server.js
следующим образом:io.on('connection', (client) => {
client.on('subscribeToTimer', (interval) => {
console.log('client is subscribing to timer with interval ', interval);
});
});
Теперь у нас есть базовая конструкция для организации подключения клиента и для обработки события запуска таймера, приходящего с клиента. Сейчас можно запустить таймер и начать транслировать события, содержащие сведения о текущем времени, клиенту. Снова отредактируйте серверный код, приведя его к такому виду:
io.on('connection', (client) => {
client.on('subscribeToTimer', (interval) => {
console.log('client is subscribing to timer with interval ', interval);
setInterval(() => {
client.emit('timer', new Date());
}, interval);
});
});
Тут мы открываем сокет и начинаем ожидать подключения клиентов. Когда клиент подключается, мы оказываемся в замыкании, где можно обрабатывать события от конкретного клиента. В частности, речь идёт о событии
subscribeToTimer
, которое было сгенерировано на клиенте. Сервер, при его получении, запускает таймер с заданным клиентом интервалом. При срабатывании таймера событие timer
передаётся клиенту.В данный момент код в файле
server.js
должен выглядеть так:const io = require('socket.io')();
io.on('connection', (client) => {
client.on('subscribeToTimer', (interval) => {
console.log('client is subscribing to timer with interval ', interval);
setInterval(() => {
client.emit('timer', new Date());
}, interval);
});
});
const port = 8000;
io.listen(port);
console.log('listening on port ', port);
Серверная часть проекта готова. Прежде чем переходить к клиенту, проверим, запускается ли, после всех правок, код сервера, выполнив в терминале команду
node server
. Если, пока вы редактировали server.js
, сервер был запущен, перезапустите его для проверки работоспособности последних изменений.Socket.io на клиенте
React-приложение мы уже запускали, выполнив в терминале команду
npm start
. Если оно всё ещё запущено, открыто в браузере, значит вы сможете внести изменения в код и браузер тут же перезагрузит изменённое приложение.Сначала надо написать клиентский код для работы с сокетами, который будет взаимодействовать с серверным сокетом и запускать таймер, генерируя событие
subscribeToTimer
и потребляя события timer
, которые публикует сервер.Для того, чтобы это сделать, создайте файл
api.js
в папке src
. В этом файле мы создадим функцию, которую можно будет вызвать для отправки серверу события subscribeToTimer
и передачи данных события timer
, генерируемого сервером, коду, который занимается визуализацией.Начнём с создания функции и её экспорта из модуля:
function subscribeToTimer(interval, cb) {
}
export { subscribeToTimer }
Тут мы используем функции в стиле Node.js, где тот, кто вызывает функцию, может передать интервал таймера в первом параметре, а функцию обратного вызова — во втором.
Теперь нужно установить клиентскую версию библиотеки Socket.io. Сделать это можно из терминала:
npm i --save socket.io-client
Далее — импортируем библиотеку. Тут мы можем использовать синтаксис модулей ES6, так как выполняемый клиентский код транспилируется с помощью Webpack и Babel. Создать сокет можно, вызвав главную экспортируемую функцию из модуля
socket.io-client
и передав в неё данные о сервере:import openSocket from 'socket.io-client';
const socket = openSocket('http://localhost:8000');
Итак, на сервере мы ждём подключения клиента и события
subscribeToTimer
, после получения которого запустим таймер, и, при каждом его срабатывании, будем генерировать события timer
, передаваемые клиенту.Теперь осталось лишь подписаться на событие
timer
, приходящее с сервера, и сгенерировать событие subscribeToTimer
. Каждый раз, получая событие timer
с сервера, будем выполнять функцию обратного вызова с данными события. В результате полный код api.js
будет выглядеть так:import openSocket from 'socket.io-client';
const socket = openSocket('http://localhost:8000');
function subscribeToTimer(cb) {
socket.on('timer', timestamp => cb(null, timestamp));
socket.emit('subscribeToTimer', 1000);
}
export { subscribeToTimer };
Обратите внимание на то, что мы подписываемся на событие
timer
сокета до того, как генерируем событие subscribeToTimer
. Делается это на тот случай, если мы столкнёмся с состоянием гонок, когда сервер уже начнёт выдавать события timer
, а клиент на них ещё не подписан, что приведёт к потере данных, передаваемых в событиях.Использование данных, полученных с сервера, в компоненте React
Итак, файл
api.js
готов, он экспортирует функцию, которую можно вызвать для подписки на события, генерируемые сервером. Теперь поговорим о том, как использовать эту функцию в компоненте React для вывода, в реальном времени, данных, полученных с сервера через сокет.При создании React-приложения с помощью
create-react-app
был сгенерирован файл App.js
(в папке src
). В верхней части кода этого файла добавим импорт ранее созданного API:import { subscribeToTimer } from './api';
Теперь можно добавить в тот же файл конструктор компонента, внутри которого вызвать функцию
subscribeToTimer
из api.js
. Каждый раз, получая событие с сервера, просто запишем значение timestamp
в состояние компонента, используя данные, пришедшие с сервера.constructor(props) {
super(props);
subscribeToTimer((err, timestamp) => this.setState({
timestamp
}));
}
Так как мы собираемся использовать значение
timestamp
в состоянии компонента, имеет смысл установить для него значение по умолчанию. Для этого добавьте следующий фрагмент кода ниже конструктора:state = {
timestamp: 'no timestamp yet'
};
На данном этапе можно отредактировать код функции
render
таким образом, чтобы она выводила значение timestamp
:render() {
return (
<div className="App">
<p className="App-intro">
This is the timer value: {this.state.timestamp}
</p>
</div>
);
}
Теперь, если всё сделано правильно, на странице приложения каждую секунду будут выводиться текущие дата и время, полученные с сервера.
Итоги
Я часто использую описанный здесь шаблон взаимодействия серверного и клиентского кода. Вы, без особых сложностей, можете расширить его для применения в собственных сценариях. Хочется надеяться, что теперь все, кто хотел приблизить свои React-разработки к приложениям реального времени, смогут это сделать.
Уважаемые читатели! Планируете ли вы применить описанную здесь методику клиент-серверного взаимодействия в своих проектах?
Комментарии (7)
friday
18.07.2017 22:34Взаимодействие с сервером прямо из компонента — гарантированный способ выстрелить себе в ногу. Для пробрасывания данных в реакт давно придумали redux/mobx/etc.
Akuma
18.07.2017 22:53+3Да ну, глупости.
Не в конструкторе конечно подписываться, а в componentDidMount + незабывать отписываться.
В остальном redux и пр. нужны только если у вас большое приложение и данные используются где-то еще. Если они используются в пределах одного компонента — вполне можно манипулировать ими в самом компоненте: если руки на месте, ничего страшного не случится.
comerc
19.07.2017 20:25Знаете, какой рутовый компонент приложения при использовании redux? Provider. Представьте, что в его локальном стейте хранятся все данные. Вот и весь наш redux.
comerc
19.07.2017 20:27Можно взять FeathersJS и вообще забыть о транспорте между клиентом и сервером.
Akuma
Краское содержание статьи:
— Огрызок мануала create-react-app
— Огрызок мануала socket.io
— Погрызаный огрызок мануала react
Не обижайтесь, но такой громкий заголовок заставляет ожидать чего-то большего, чем просто копипаста доков из пары бибилитек.
«Методика клиент-серверного взаимодействия» — это просто socket.io, который кстати не только вебсокеты.
А React приплетен вообще непонятно зачем.
UPD: А, это перевод.