![](https://habrastorage.org/webt/ty/pi/vz/typivzoqbkyrfawb47veqd-mcis.jpeg)
В этой статье я покажу, как создать веб-приложение с использованием Node.js, которое позволяет отслеживать результаты матчей NHL в реальном времени. Показатели обновляются в соответствии с изменениями счета по ходу игр.
Мне очень понравилось писать эту статью, поскольку работа над ней включала две любимые мною вещи: разработку ПО и спорт.
В ходе работы мы будем использовать следующие инструменты:
- Node.js;
- Socket.io;
- MySportsFeed.com.
Если у вас нет установленного Node.js, посетите страницу загрузки и установите, а потом мы продолжим.
Что такое Socket.io?
Это технология, которая соединяет клиента с сервером. В текущем примере клиент — веб-браузер, а сервер — приложение Node.js. Сервер может работать одновременно с несколькими клиентами в любое время.
Как только соединение установлено, сервер может отправлять сообщения всем клиентам или же всего одному из них. Тот, в свою очередь, может отправлять сообщениея на сервер, обеспечивая связь в двух направлениях.
До Socket.io веб-приложения обычно работали на AJAX. Его использование предусматривало необходимость опроса клиентом сервера и наоборот в поисках новых событий. К примеру, такие опросы могли выполняться каждые 10 секунд для проверки наличия новых сообщений.
Это давало дополнительную нагрузку, поскольку поиск новых сообщений велся даже тогда, когда их не было вовсе.
При использовании Socket.io сообщения получаются в режиме реального времени без необходимости постоянно проверять их наличие, что снижает нагрузку.
Пример приложения Socket.io
Прежде чем мы начнем собирать данные соревнований в реальном времени, давайте сделаем приложение-пример для демонстрации принципа работы Socket.io.
Вначале я собираюсь создать приложение Node.js. В окне консоли нужно перейти к каталогу C:\GitHub\NodeJS, создать новую папку для приложения и в ней — новое приложение:
cd \GitHub\NodeJS
mkdir SocketExample
cd SocketExample
npm init
Я оставил настройки по умолчанию, вы можете поступить так же.
Поскольку мы создаем веб-приложение, для упрощения установки я буду использовать пакет NPM, который называется Express. В командной строке выполняем следующие команды: npm install express --save.
Конечно, мы должны установить и пакет Socket.io package: npm install socket.io --save
Теперь нужно запустить веб-сервер. Для этого создаем новый файл index.js и размещаем следующий участок кода:
var app = require('express')();
var http = require('http').Server(app);
app.get('/', function(req, res){
res.sendFile(__dirname + '/index.html');
});
http.listen(3000, function(){
console.log('HTTP server started on port 3000');
});
Если Express вам не знаком, то пример кода выше включает библиотеку Express, здесь идет создание нового HTTP-сервера. В примере HTTP-сервер слушает порт 3000, т.е. localhost:3000. Путь идет к корню, “/”. Результат возвращается в виде HTML-файла index.html.
Прежде чем создать этот файл, давайте завершим запуск сервера, настроив Socket.io. Для создания сервера Socket выполняем следующие команды:
var io = require('socket.io')(http);
io.on('connection', function(socket){
console.log('Client connection received');
});
Как и в случае с Express, код начинается с импорта библиотеки Socket.io. Это указывается переменной io. Далее, используя эту переменную, создаем обработчик события с функцией on. Это событие вызывается каждый раз, когда клиент подключается к серверу.
Теперь давайте создадим простенький клиент. Для этого нужно сделать файл index.html и разместить внутри такой код:
<!doctype html>
<html>
<head>
<title>Socket.IO Example</title>
</head>
<body>
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io();
</script>
</body>
</html>
HTML выше загружает клиент Socket.io JavaScript и инициализирует подключение к серверу. Для того, чтобы увидеть пример, запускаем Node: node index.js.
Далее в браузере вводим localhost:3000. Страница останется пустой, но если вы посмотрите на консоль во время выполнения Node, то увидите два сообщения:
HTTP server started on port 3000
Client connection received
Сейчас, когда мы успешно соединились, давайте продолжим работу. Например, отправим сообщение в клиент с сервера. Затем, когда клиент получит его, отправится ответное сообщение:
io.on('connection', function(socket){
console.log('Client connection received');
socket.emit('sendToClient', { hello: 'world' });
socket.on('receivedFromClient', function (data) {
console.log(data);
});
});
Предыдущая функция io.on обновлена включением нескольких новых строк кода. Первая, socket.emit, отправляет сообщение клиенту. sendToClient — имя события. Именуя события, вы получаете возможность отправлять разные типы сообщений, так что клиент сможет интерпретировать их по-разному. Еще одно обновление — socket.on, где тоже есть название события: receivedFromClient. Все это создает функцию, которая принимает данные от клиента. В этом случае они также записываются в окне консоли.
Выполненные действия завершают подготовку сервера. Теперь он может принимать и отправлять данные от любого подключенного клиента.
Давайте завершим этот пример, обновив клиент путем получения ивента sendToClient. Когда событие получено, дается ответ receivedFromClient.
На этом завершаем JavaScript-часть HTML. В index.html добавляем:
var socket = io();
socket.on('sendToClient', function (data) {
console.log(data);
socket.emit('receivedFromClient', { my: 'data' });
});
Используя встроенную переменную сокета, получаем похожую логику на сервере с функцией socket.on. Клиент прослушивает событие sendToClient. Как только клиент подключен, сервер отправляет это сообщение. Клиент, получая его, записывает событие в консоли браузера. После этого клиент использует тот же socket.emit, что ранее сервер для отправки оригинального события. В этом случае клиент отправляет полученное событие FromClient на сервер. Когда тот получает сообщение, это логируется в окне консоли.
Попробуйте сами. Сначала, в консоли, запустите ваше Node-приложение: node index.js. Затем загрузите в браузере localhost:3000.
Проверьте консоль браузера, и вы увидите в логах JSON следующее: {hello: «world»}
Затем, пока выполняется приложение Node, вы увидите следующее:
HTTP server started on port 3000
Client connection received
{ my: 'data' }
Как клиент, так и сервер могут использовать JSON-данные для выполнения специфических задач. Давайте посмотрим, как можно работать с данными соревнований в реальном времени.
Информация с соревнований
После того как мы поняли принципы отправки и получения данных клиентом и сервером, стоит попробовать обеспечить выполнение обновлений в реальном времени. Я использовал данные соревнований, хотя то же самое можно проделывать не только с информацией спортивного характера. Но раз уж работаем с ней, то нужно найти источник. Им послужит MySportsFeeds. Сервис платный — от $1 в месяц, имейте в виду.
Как только учетная запись настроена, вы можете начать работу с их API. Для этого можно использовать пакет NPM: npm install mysportsfeeds-node --save.
После установки пакета подключаем API:
var MySportsFeeds = require("mysportsfeeds-node");
var msf = new MySportsFeeds("1.2", true);
msf.authenticate("********", "*********");
var today = new Date();
msf.getData('nhl', '2017-2018-regular', 'scoreboard', 'json', {
fordate: today.getFullYear() +
('0' + parseInt(today.getMonth() + 1)).slice(-2) +
('0' + today.getDate()).slice(-2),
force: true
});
В примере выше замените мои данные своими.
Код выполняет вызов API для получения сегодняшних результатов соревнований NHL. Переменная fordate — то, что определяет дату. Также я использовал force и true для того, чтобы получать в ответ данные даже в том случае, если результаты прежние.
При текущей настройке результаты вызова API записываются в текстовый файл. В последнем примере мы это поменяем; для демонстрационных же целей файл результатов можно просмотреть в текстовом редакторе, чтобы понять содержимое ответа. В нашем результате мы видим объект таблицы результатов. Этот объект содержит массив, называемый gameScore. Он сохраняет результат каждой игры. Каждый объект в свою очередь содержит дочерний объект, называемый игрой. Этот объект предоставляет информацию о том, кто играет.
За пределами игрового объекта имеется несколько переменных, которые обеспечивают отображение текущего состояния игры. Данные меняются в зависимости от ее результатов. Когда игра еще не началась, используются переменные, которые позволяют получить информацию о том, когда это случится. Когда игра началась, предоставляются дополнительные данные о результатах, включая информацию о том, какой сейчас период и сколько времени осталось. Для того чтобы лучше понять, о чем идет речь, давайте перейдем к следующему разделу.
Обновления в режиме реального времени
У нас есть все части пазла, поэтому давайте его собирать! К сожалению, у MySportsFeeds ограниченная поддержка выдачи данных, поэтому придется постоянно запрашивать информацию. Здесь есть положительный момент: мы знаем, что данные меняются только раз в 10 минут, поэтому слишком часто опрашивать сервис не нужно. Получаемые данные можно отправлять с сервера на все подключенные клиенты.
Для получения нужных данных я буду использовать функцию setInterval JavaScript, которая позволяет обращаться к API (в моем случае) каждые 10 минут для поиска обновлений. Когда данные приходят, событие отправляется всем подключенным клиентам. Далее результаты обновляются посредством JavaScript в веб-браузере.
К MySportsFeeds также идет обращение, когда приложение Node запускается первым. Полученные результаты будут использоваться для любых клиентов, которые подключаются до первого 10-минутного интервала. Информация об этом сохраняется в глобальной переменной. Она, в свою очередь, обновляется как часть интервального опроса. Это дает гарантию того, что у каждого из клиентов будут актуальные результаты.
Для того, чтобы в основном файле index.js все было хорошо, я создал новый файл с именем data.js. Он содержит функцию, экспортирующуюся из index.js, которая выполняет предыдущий вызов API MySportsFeeds. Вот полное содержание этого файла:
var MySportsFeeds = require("mysportsfeeds-node");
var msf = new MySportsFeeds("1.2", true, null);
msf.authenticate("*******", "******");
var today = new Date();
exports.getData = function() {
return msf.getData('nhl', '2017-2018-regular', 'scoreboard', 'json', {
fordate: today.getFullYear() +
('0' + parseInt(today.getMonth() + 1)).slice(-2) +
('0' + today.getDate()).slice(-2),
force: true
});
};
Функция getData экспортируется и возвращает результаты вызова. Давайте посмотрим на то, что у нас содержится в файле index.js.
var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http);
var data = require('./data.js');
// Global variable to store the latest NHL results
var latestData;
// Load the NHL data for when client's first connect
// This will be updated every 10 minutes
data.getData().then((result) => {
latestData = result;
});
app.get('/', function(req, res){
res.sendFile(__dirname + '/index.html');
});
http.listen(3000, function(){
console.log('HTTP server started on port 3000');
});
io.on('connection', function(socket){
// when clients connect, send the latest data
socket.emit('data', latestData);
});
// refresh data
setInterval(function() {
data.getData().then((result) => {
// Update latest results for when new client's connect
latestData = result;
// send it to all connected clients
io.emit('data', result);
console.log('Last updated: ' + new Date());
});
}, 300000);
Первые семь строк кода выше обеспечивают инициализацию требуемых библиотек и вызов глобальной переменной latestData. Последний список используемых библиотек таков: Express, Http Server, созданный с помощью Express, Socket.io плюс только что созданный файл data.js.
С учетом потребностей приложение заполняет последние данные (latestData) для клиентов, которые подключатся при первом запуске сервера:
// Global variable to store the latest NHL results
var latestData;
// Load the NHL data for when client's first connect
// This will be updated every 10 minutes
data.getData().then((result) => {
latestData = result;
});
Следующие несколько строк устанавливают путь для главной страницы сайта (в нашем случае localhost:3000/) и дают команду HTTP-серверу слушать порт 3000.
Затем Socket.io настраивается для поиска новых подключений. Когда они обнаруживаются, сервер отправляет данные событий с содержимым переменной latestData.
И, наконец, последний фрагмент кода создает необходимый интервал опроса. Когда он обнаружен, переменная latestData обновляется с результатами вызова API. Эти данные затем передают одно и то же событие всем клиентам.
// refresh data
setInterval(function() {
data.getData().then((result) => {
// Update latest results for when new client's connect
latestData = result;
// send it to all connected clients
io.emit('data', result);
console.log('Last updated: ' + new Date());
});
}, 300000);
Как мы видим, когда клиент подключается и определяется событие, оно выдается с переменной сокета. Это позволяет отправить событие определенному подключившемуся клиенту. Внутри интервала глобальная io используется для отправки события. Она отправляет данные всем клиентам. Настройка сервера завершена.
Как это будет выглядеть
Теперь поработаем над фронтендом клиента. В раннем примере я создал базовый файл index.html, который устанавливает связь с клиентом, чтобы записывать события сервера и отправлять их. Теперь я расширю возможности файла.
Поскольку сервер отправляет нам объект JSON, я буду использовать jQuery и расширение jQuery, которое называется JsRender. Это шаблонная библиотека. Она позволит мне создать шаблон c HTML, который будет использоваться для отображения содержимого каждой игры NHL в удобной форме. Сейчас вы сможете убедиться в широте ее возможностей. Код содержит более 40 строк, поэтому я разделю его на несколько меньших участков, а в конце покажу все содержимое HTML.
Вот что используется для отображения игровых данных:
<script id="gameTemplate" type="text/x-jsrender">
<div class="game">
<div>
{{:game.awayTeam.City}} {{:game.awayTeam.Name}} at {{:game.homeTeam.City}} {{:game.homeTeam.Name}}
</div>
<div>
{{if isUnplayed == "true" }}
Game starts at {{:game.time}}
{{else isCompleted == "false"}}
<div>Current Score: {{:awayScore}} - {{:homeScore}}</div>
<div>
{{if currentIntermission}}
{{:~ordinal_suffix_of(currentIntermission)}} Intermission
{{else currentPeriod}}
{{:~ordinal_suffix_of(currentPeriod)}}<br/>
{{:~time_left(currentPeriodSecondsRemaining)}}
{{else}}
1st
{{/if}}
</div>
{{else}}
Final Score: {{:awayScore}} - {{:homeScore}}
{{/if}}
</div>
</div>
</script>
Шаблон определяется с помощью тега script. Он содержит идентификатор шаблона и специальный тип сценария, называемый text / x-jsrender. Шаблон определяет контейнер div для каждой игры, который содержит класс игры для применения определенного базового стиля. Внутри этого div и содержится начало шаблона.
В следующем div отображаются гостевая команда и команда-хозяйка. Это реализовано объединением названия города и имени команды вместе с игровым объектом из данных MySportsFeeds.
{{: game.awayTeam.City}} — это то, как я определяю объект, который будет заменен физическим значением при визуализации шаблона. Этот синтаксис определяется библиотекой JsRender.
Когда игра unPlayed, появится строка, в которой игра начнется с {{: game.time}}.
Пока игра не завершена, отображается текущий счет: {{: awayScore}} — {{: homeScore}}. И, наконец, небольшой прием, который позволит определить, какой сейчас период, и уточнить, не перерыв ли сейчас.
Если переменная currentIntermission появляется в результатах, то я использую функцию I, определяемую ordinal_suffix_of, которая преобразует номер периода в следующий текст: 1-й (2-й, 3-й и т. д.) Перерыв.
Когда перерыва нет, я ищу значение currentPeriod. Здесь также используется ordinal_suffix_of, чтобы показать, что игра находится в 1-м (2-м, 3-м и т. д.) периодах.
Кроме того, другая функция, которую я определил как time_left, используется для преобразования количества секунд, оставшихся до конца периода. Например: 10:12.
Последняя часть кода отображает финальный результат, когда игра завершена.
Вот пример того, как это выглядит, когда есть смешанный список завершенных игр, игр, которые еще не завершены, и и игр, которые еще не начались (я не очень хороший дизайнер, поэтому результат и выглядит так, как должен, когда разработчик создает пользовательский интерфейс для приложения своими руками):
![image](https://habrastorage.org/getpro/habr/post_images/bb9/736/7c3/bb97367c3d8d9b7cbf74e0e69ea1ed6b.png)
Далее -—фрагмент JavaScript, который создает сокет, вспомогательные функции ordinal_suffix_of и time_left и переменную, которая ссылается на созданный шаблон jQuery.
<script>
var socket = io();
var tmpl = $.templates("#gameTemplate");
var helpers = {
ordinal_suffix_of: function(i) {
var j = i % 10,
k = i % 100;
if (j == 1 && k != 11) {
return i + "st";
}
if (j == 2 && k != 12) {
return i + "nd";
}
if (j == 3 && k != 13) {
return i + "rd";
}
return i + "th";
},
time_left: function(time) {
var minutes = Math.floor(time / 60);
var seconds = time - minutes * 60;
return minutes + ':' + ('0' + seconds).slice(-2);
}
};
</script>
Последним фрагментом является код для приема события сокета и рендера шаблона:
socket.on('data', function (data) {
console.log(data);
$('#data').html(tmpl.render(data.scoreboard.gameScore, helpers));
});
У меня есть разделитель с идентификатором данных. Результат рендера шаблона (tmpl.render) записывает HTML в этот контейнер. Что действительно круто — библиотека JsRender может принимать массив данных, в данном случае data.scoreboard.gameScore, который выполняет итерацию через каждый элемент в массиве и создает одну игру на элемент.
Вот обещанный выше окончательный вариант, где HTML и JavaScript собраны вместе:
<!doctype html>
<html>
<head>
<title>Socket.IO Example</title>
</head>
<body>
<div id="data">
</div>
<script id="gameTemplate" type="text/x-jsrender">
<div class="game">
<div>
{{:game.awayTeam.City}} {{:game.awayTeam.Name}} at {{:game.homeTeam.City}} {{:game.homeTeam.Name}}
</div>
<div>
{{if isUnplayed == "true" }}
Game starts at {{:game.time}}
{{else isCompleted == "false"}}
<div>Current Score: {{:awayScore}} - {{:homeScore}}</div>
<div>
{{if currentIntermission}}
{{:~ordinal_suffix_of(currentIntermission)}} Intermission
{{else currentPeriod}}
{{:~ordinal_suffix_of(currentPeriod)}}<br/>
{{:~time_left(currentPeriodSecondsRemaining)}}
{{else}}
1st
{{/if}}
</div>
{{else}}
Final Score: {{:awayScore}} - {{:homeScore}}
{{/if}}
</div>
</div>
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsrender/0.9.90/jsrender.min.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io();
var helpers = {
ordinal_suffix_of: function(i) {
var j = i % 10,
k = i % 100;
if (j == 1 && k != 11) {
return i + "st";
}
if (j == 2 && k != 12) {
return i + "nd";
}
if (j == 3 && k != 13) {
return i + "rd";
}
return i + "th";
},
time_left: function(time) {
var minutes = Math.floor(time / 60);
var seconds = time - minutes * 60;
return minutes + ':' + ('0' + seconds).slice(-2);
}
};
var tmpl = $.templates("#gameTemplate");
socket.on('data', function (data) {
console.log(data);
$('#data').html(tmpl.render(data.scoreboard.gameScore, helpers));
});
</script>
<style>
.game {
border: 1px solid #000;
float: left;
margin: 1%;
padding: 1em;
width: 25%;
}
</style>
</body>
</html>
Теперь самое время запустить приложение Node и открыть localhost:3000 для того, чтобы увидеть результат!
Через каждые X минут сервер отправляет событие клиенту. Клиент, в свою очередь, будет перерисовывать элементы игры с обновленными данными. Поэтому, когда вы оставляете сайт открытым, результаты игр будут постоянно обновляться.
Вывод
Конечный продукт использует Socket.io для создания сервера, к которому подключаются клиенты. Сервер извлекает данные и отправляет их клиенту. Когда клиент получает данные, он может обновлять результаты постепенно. Это уменьшает нагрузку на сервер, поскольку клиент действует, только когда получает событие от сервера.
Сервер может отправлять сообщения клиенту, а клиент, в свою очередь, — серверу. Когда сервер получает сообщение, он выполняет обработку данных.
Чат-приложения работают примерно так же. Сервер получит сообщение от клиента, а затем передаст все данные подключенным клиентам, чтобы показать, что кто-то отправил новое сообщение.
Надеюсь, вам понравилась эта статья, так как я при разработке этого спортивного приложения, работающего в реальном времени, получил просто гору удовольствия, которым мне и хотелось поделиться вместе со знаниями. Ведь хоккей — один из моих любимых видов спорта!
![](https://habrastorage.org/webt/a-/lc/ia/a-lciamy-fqodfq0pwnkdqlgpgy.jpeg)