Статьи

Как создать спортивное приложение в реальном времени, используя Node.js

Конечный продукт
Что вы будете создавать

В сегодняшней статье я собираюсь продемонстрировать, как создать веб-приложение, которое будет отображать результаты матчей НХЛ в режиме реального времени. Результаты будут обновляться автоматически по мере продвижения игры.

Это очень интересная статья для меня, так как она дает мне возможность объединить два моих любимых увлечения: развитие и спорт.

Технологии, которые будут использоваться для создания приложения:

  1. Node.js
  2. Socket.io
  3. MySportsFeed.com

Если у вас не установлен Node.js, посетите страницу загрузки и настройте его, прежде чем продолжить.

Socket.io — это технология, которая соединяет клиента с сервером. В этом примере клиент является веб-браузером, а сервер — приложением Node.js. К серверу может быть подключено несколько клиентов в любой момент времени.

Как только соединение установлено, сервер может отправлять сообщения всем клиентам или отдельным клиентам. В свою очередь, клиент может отправить сообщение на сервер, что позволяет осуществлять двустороннюю связь в реальном времени.

До Socket.io веб-приложения обычно использовали AJAX, и клиент, и сервер опрашивали друг друга в поисках событий. Например, каждые 10 секунд будет происходить вызов AJAX, чтобы увидеть, есть ли какие-либо сообщения для обработки.

Опрос для сообщений вызвал значительную нагрузку как на клиенте, так и на сервере, так как он будет постоянно искать сообщения, когда их не было.

С Socket.io сообщения принимаются мгновенно, без необходимости искать сообщения, что снижает накладные расходы.

Прежде чем использовать спортивные данные в реальном времени, давайте создадим пример приложения, чтобы продемонстрировать, как работает Socket.io.

Для начала я собираюсь создать новое приложение Node.js. В окне консоли я собираюсь перейти к C: \ GitHub \ NodeJS, создать новую папку для моего приложения и создать новое приложение:

1
2
3
4
cd \GitHub\NodeJS
mkdir SocketExample
cd SocketExample
npm init

Я использовал все настройки по умолчанию.

Поскольку мы делаем веб-приложение, я собираюсь использовать пакет NPM под названием Express, чтобы упростить настройку. В командной строке установите его следующим образом: npm install express --save

И, конечно, нам нужно будет установить пакет Socket.io: npm install socket.io --save

Давайте начнем с создания веб-сервера. Создайте новый файл index.js и поместите в него следующий код для создания веб-сервера с помощью Express:

01
02
03
04
05
06
07
08
09
10
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, например, http: // localhost: 3000 . Маршрут создается в корне сайта «/». Результат маршрута возвращает файл HTML: index.html.

Прежде чем мы создадим файл index.html, давайте завершим работу сервера, настроив Socket.io. Добавьте следующее в файл index.js, чтобы создать сервер Socket:

1
2
3
4
5
var io = require(‘socket.io’)(http);
 
io.on(‘connection’, function(socket){
    console.log(‘Client connection received’);
});

Как и в Express, код начинается с импорта библиотеки Socket.io. Это хранится в переменной с именем io . Затем, используя переменную io , создается обработчик событий с функцией on . Прослушиваемое событие — это соединение. Это событие вызывается каждый раз, когда клиент подключается к серверу.

Давайте теперь создадим нашего самого основного клиента. Создайте новый файл с именем index.html и поместите в него следующий код:

01
02
03
04
05
06
07
08
09
10
11
12
<!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

Затем в браузере перейдите по адресу http: // localhost: 3000 . Ничего не появится на странице; однако, если вы посмотрите на консоль, где запущено приложение Node, в журнале регистрируются два сообщения:

  1. HTTP-сервер запущен на порту 3000
  2. Клиентское соединение получено

Теперь, когда у нас есть успешное сокетное соединение, давайте его использовать. Начнем с отправки сообщения с сервера клиенту. Затем, когда клиент получает сообщение, он может отправить ответ обратно на сервер.

Давайте посмотрим на сокращенный файл index.js:

1
2
3
4
5
6
7
8
9
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 , который также содержит имя события: socket.on . Это создает функцию, которая принимает данные от клиента. В этом случае данные регистрируются в окне консоли.

Это завершает поправки на стороне сервера; теперь он может отправлять и получать данные от любых подключенных клиентов.

Давайте sendToClient этот пример, обновив клиент для получения события sendToClient . Когда он получает событие, он может ответить на событие receivedFromClient обратно на сервер.

Это выполняется в части HTML в JavaScript, поэтому в файле index.html я обновил JavaScript следующим образом:

1
2
3
4
5
6
7
var socket = io();
 
socket.on(‘sendToClient’, function (data) {
    console.log(data);
     
    socket.emit(‘receivedFromClient’, { my: ‘data’ });
});

Используя экземплярную переменную сокета, мы имеем очень похожую логику на сервере с функцией socket.on . Для клиента это прослушивание события sendToClient . Как только клиент подключен, сервер отправляет это сообщение. Когда клиент получает его, он регистрируется на консоли в браузере. Затем клиент использует тот же socket.emit который сервер использовал для отправки исходного события. В этом случае клиент отправляет обратно сообщение receiveFromClient на сервер. Когда сервер получает сообщение, оно регистрируется в окне консоли.

Попробуй сам. Сначала в консоли запустите приложение Node: node index.js . Затем загрузите http: // localhost: 3000 в ваш браузер.

Проверьте консоль веб-браузера, и вы должны увидеть в журнале следующие данные JSON: {hello: "world"}

Затем в командной строке, где запущено приложение Node, вы должны увидеть следующее:

1
2
3
HTTP server started on port 3000
Client connection received
{ my: ‘data’ }

И клиент, и сервер могут использовать полученные данные JSON для выполнения конкретных задач. Мы узнаем больше об этом, когда подключимся к спортивным данным в реальном времени.

Теперь, когда мы освоили, как отправлять и получать данные на клиент и сервер, их можно использовать для обновления в режиме реального времени. Я решил использовать спортивные данные, хотя та же теория не ограничивается спортом. Прежде чем я начал этот проект, я исследовал различные спортивные данные. Я остановился на MySportsFeeds , потому что они предлагают бесплатные аккаунты для разработчиков (я никак не связан с ними). Чтобы получить доступ к данным в реальном времени, я зарегистрировал аккаунт и сделал небольшое пожертвование. Пожертвования начинаются с 1 доллара, чтобы обновлять данные каждые 10 минут. Это будет хорошо для примера.

Как только ваша учетная запись настроена, вы можете приступить к настройке доступа к их API. Чтобы помочь с этим, я собираюсь использовать их пакет NPM: npm install mysportsfeeds-node --save

После установки пакета вызовы API можно выполнить следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
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
});

В приведенном выше примере обязательно замените вызов функции authenticate на ваше имя пользователя и пароль.

Следующий код выполняет вызов API для получения таблицы результатов НХЛ на сегодня. Переменная fordate — это то, что определяет сегодня. Я также установил для true значение true чтобы ответ всегда возвращался, даже если данные не изменились.

При текущей настройке результаты вызова API записываются в текстовый файл. В последнем примере это будет изменено; однако для демонстрации файл результатов можно просмотреть в текстовом редакторе, чтобы понять содержание ответа. Результаты содержат объект табло. Этот объект содержит массив с именем gameScore . Этот объект хранит результат каждой игры. Каждый объект содержит дочерний объект с именем game . Этот объект предоставляет информацию о том, кто играет.

За пределами игрового объекта есть несколько переменных, которые обеспечивают текущее состояние игры. Данные изменяются в зависимости от состояния игры. Например, когда игра еще не началась, есть только несколько переменных, которые говорят нам, что игра не выполняется и не началась.

Когда игра продолжается, предоставляются дополнительные данные о счете, в каком периоде игра и сколько времени осталось. Мы увидим это в действии, когда перейдем к HTML, чтобы показать игру в следующем разделе.

У нас есть все кусочки головоломки, поэтому пришло время собрать головоломку вместе, чтобы показать окончательную картину. В настоящее время MySportsFeeds имеет ограниченную поддержку для передачи данных нам, поэтому нам придется опросить данные из них. К счастью, мы знаем, что данные меняются только раз в 10 минут, поэтому нам не нужно добавлять накладные расходы, слишком часто опрашивая изменения. После того, как мы опросим данные из них, мы можем отправить эти обновления с сервера на всех подключенных клиентов.

Чтобы выполнить опрос, я буду использовать функцию JavaScript setInterval для вызова API (в моем случае) каждые 10 минут для поиска обновлений. Когда данные получены, событие отправляется всем подключенным клиентам. Когда клиенты получат событие, игровые результаты будут обновлены с помощью JavaScript в веб-браузере.

MySportsFeeds также будет вызываться при первом запуске приложения Node. Эти данные будут использоваться для любых клиентов, которые подключаются до первого 10-минутного интервала. Это хранится в глобальной переменной. Эта же глобальная переменная обновляется как часть опроса интервала. Это гарантирует, что при подключении новых клиентов после опроса они будут иметь самые последние данные.

Чтобы помочь с некоторой чистотой кода в основном файле index.js, я создал новый файл с именем data.js. Этот файл будет содержать экспортируемую функцию (доступную в файле index.js), которая выполняет предыдущий вызов API MySportsFeeds. Вот полное содержание этого файла:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
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:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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 экземпляры необходимых библиотек и глобальной переменной latestData . Окончательный список используемых библиотек: Express, Http-сервер, созданный с помощью Express, Socket.io, и только что созданный вышеупомянутый файл data.js.

Принимая во внимание все необходимое, приложение заполняет latestData для клиентов, которые будут подключаться при первом запуске сервера:

1
2
3
4
5
6
7
8
// 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;
});

Следующие несколько строк устанавливают маршрут для корневой страницы веб-сайта ( http: // localhost: 3000 / ) и запускают HTTP-сервер для прослушивания порта 3000.

Далее, Socket.io настроен для поиска соединений. При получении нового соединения сервер генерирует событие, называемое data, с содержимым переменной latestData .

И, наконец, последний фрагмент кода создает интервал опроса. Когда происходит интервал, переменная latestData обновляется с результатами вызова API. Затем эти данные генерируют одно и то же событие данных для всех клиентов.

01
02
03
04
05
06
07
08
09
10
11
12
// 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 . Это библиотека шаблонов. Это позволит мне создать шаблон с HTML, который будет использоваться для отображения содержимого каждой игры НХЛ простым и понятным способом. Через мгновение вы увидите мощь этой библиотеки. Конечный код содержит более 40 строк кода, поэтому я собираюсь разбить его на более мелкие куски, а затем в конце отобразить полный HTML-код.

Эта первая часть создает шаблон, который будет использоваться для отображения данных игры:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<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 начинается шаблонирование.

В следующем разделе отображаются команда гостей и гостей. Это делается путем объединения города и названия команды из игрового объекта из данных MySportsFeed.

{{:game.awayTeam.City}} — это то, как я определяю объект, который будет заменен физическим значением при визуализации шаблона. Этот синтаксис определяется библиотекой JsRender.

После отображения команд следующий фрагмент кода выполняет некоторую условную логику. Когда игра не unPlayed , выводится строка, которая начинается в {{:game.time}} .

Когда игра не завершена, отображается текущий счет: Current Score: {{:awayScore}} - {{:homeScore}} . И, наконец, немного хитрой логики, чтобы определить, в каком периоде игра в хоккей или в перерыве.

Если в результатах предусмотрена переменная currentIntermission , то я использую определенную мной функцию под названием ordinal_suffix_of , которая преобразует номер периода в следующее: 1-й (2-й, 3-й и т. Д.) Intermission.

Когда это не в перерыве, я ищу значение currentPeriod . Здесь также используется ordinal_suffix_of чтобы показать, что игра находится в 1-м (2-м, 3-м и т. Д.) ordinal_suffix_of .

Кроме того, другая функция, которую я определил, называется time_left , используется для преобразования количества секунд, оставшихся в количестве минут и секунд, оставшихся в периоде. Например: 10:12.

Последняя часть кода отображает итоговый счет, потому что мы знаем, что игра завершена.

Вот пример того, как это выглядит, когда есть смесь готовых игр, готовых игр и игр, которые еще не запускались (я не очень хороший дизайнер, поэтому все выглядит так, как вы ожидаете, когда разработчик делает их собственный пользовательский интерфейс).

Пример готовых игр

Далее следует фрагмент JavaScript, который создает сокет, вспомогательные функции ordinal_suffix_of и time_left и переменная, которая ссылается на созданный шаблон jQuery.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<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>

Последний кусок кода — это код для получения события сокета и визуализации шаблона:

1
2
3
4
5
socket.on(‘data’, function (data) {
    console.log(data);
     
    $(‘#data’).html(tmpl.render(data.scoreboard.gameScore, helpers));
});

У меня есть заполнитель div с идентификатором данных. Результат рендеринга шаблона ( tmpl.render ) записывает HTML- tmpl.render в этот контейнер. Что действительно важно, так это то, что библиотека JsRender может принимать массив данных, в данном случае data.scoreboard.gameScore , который выполняет data.scoreboard.gameScore по каждому элементу в массиве и создает одну игру на каждый элемент.

Вот окончательный HTML и JavaScript все вместе:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
<!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 и перейдите по адресу http: // localhost: 3000, чтобы увидеть результаты самостоятельно!

Каждые X минут сервер будет отправлять событие клиенту. Клиент будет перерисовывать игровые элементы с обновленными данными. Поэтому, когда вы оставляете сайт открытым и периодически просматриваете его, вы увидите, что данные игры обновляются, когда игры находятся в процессе.

Конечный продукт использует Socket.io для создания сервера, к которому подключаются клиенты. Сервер выбирает данные и отправляет их клиенту. Когда клиент получает данные, он может плавно обновить отображение. Это снижает нагрузку на сервер, потому что клиент выполняет работу только тогда, когда он получает событие от сервера.

Розетки не ограничены одним направлением; клиент также может отправлять сообщения на сервер. Когда сервер получает сообщение, он может выполнить некоторую обработку.

Приложения чата обычно работают таким образом. Сервер получит сообщение от клиента, а затем рассылает его всем подключенным клиентам, чтобы показать, что кто-то отправил новое сообщение.

Надеюсь, вам понравилась эта статья, так как я с удовольствием создавал это спортивное приложение в реальном времени для одного из моих любимых видов спорта!