Сегодня поговорим о том, как наладить взаимодействие React-приложения с сервером, используя Socket.io, добившись при этом высокой скорости отклика приложения на события, которые генерирует сервер. Примеры кода рассчитаны на React или React Native. При этом концепции, изложенные здесь, универсальны, их можно применить и при разработке с использованием других фронтенд-фреймворков, таких, как Vue или Angular.
Итак, нам нужно, чтобы клиентское приложение реагировало на события, генерируемые на сервере. Обычно в подобных случаях речь идёт о приложениях реального времени. В таком сценарии сервер передаёт клиенту свежие данные по мере их появления. После того, как между клиентом и сервером будет установлено соединение, сервер, не полагаясь на запросы клиента, самостоятельно инициирует передачу данных.
Подобная модель подходит для множества проектов. Среди них — чаты, игры, торговые системы, и так далее. Часто предложенный в этом материале подход используют для того, чтобы повысить скорость отклика системы, при том, что сама по себе такая система может функционировать и без него. Мотивы разработчика в данном случае не важны. Полагаем, допустимо предположить, что вы это читаете, так как вам хочется узнать о том, как использовать сокеты при создании 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-разработки к приложениям реального времени, смогут это сделать.
Уважаемые читатели! Планируете ли вы применить описанную здесь методику клиент-серверного взаимодействия в своих проектах?
Автор: RUVDS.com