在Node.js上创建实时体育应用程序的示例



在本文中,我将展示如何使用Node.js创建Web应用程序,该应用程序允许您实时跟踪NHL匹配的结果。 指示器根据比赛中得分的变化而更新。

我真的很喜欢写这篇文章,因为从事其中的工作包括我喜欢的两件事:软件开发和体育。

在工作过程中,我们将使用以下工具:

  • Node.js;
  • Socket.io;
  • MySportsFeed.com。

如果您尚未安装Node.js,请访问下载页面并安装,然后我们将继续。

什么是socket.io?


这是将客户端连接到服务器的技术。 在当前示例中,客户端是Web浏览器,服务器是Node.js。 该服务器可以随时与多个客户端同时工作。

建立连接后,服务器可以将消息发送到所有客户端或仅其中一个客户端。 依次可以将消息发送到服务器,从而提供两个方向的通信。

在Socket.io之前,Web应用程序通常在AJAX上运行。 它的使用要求客户端轮询服务器,反之亦然,以查找新事件。 例如,可以每10秒执行一次此类轮询以检查是否有新消息。

这增加了额外的负担,因为即使在根本不搜索新消息的情况下也要进行搜索。

使用Socket.io时,可以实时接收消息,而无需不断检查消息是否存在,从而减少了负载。

Socket.io示例应用程序


在开始收集实时比赛数据之前,让我们创建一个示例应用程序来演示Socket.io的工作方式。

首先,我将创建一个Node.js应用程序。 在控制台窗口中,您需要转到C:\ GitHub \ NodeJS目录,为该应用程序创建一个新文件夹,并在其中创建一个新应用程序:

cd \GitHub\NodeJS mkdir SocketExample cd SocketExample npm init 

我保留了默认设置,您可以执行相同的操作。

由于我们正在构建Web应用程序,因此我将使用名为Express的NPM软件包来简化安装。 在命令提示符下,运行以下命令:npm install express --save。

当然,我们也必须安装Socket.io软件包:npm install socket.io --save

现在您需要启动Web服务器。 为此,请创建一个新的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,即 本地主机 :3000。 路径转到根目录“ /”。 结果以HTML index.html文件的形式返回。

在创建此文件之前,让我们通过配置Socket.io完成服务器启动。 要创建套接字服务器,请运行以下命令:

 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服务器在端口3000上启动
客户端连接已收到

现在我们已经成功连接,让我们继续工作。 例如,从服务器向客户端发送消息。 然后,当客户端收到它时,将发送响应消息:

 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事件更新客户端来结束本示例。 收到事件后,将收到来自客户端的响应。

到此结束HTML的JavaScript部分。 在index.html中添加:

 var socket = io(); socket.on('sendToClient', function (data) { console.log(data); socket.emit('receivedFromClient', { my: 'data' }); }); 

使用内置的socket变量,我们可以通过socket.on函数在服务器上获得类似的逻辑。 客户端侦听sendToClient事件。 一旦客户端连接,服务器就会发送此消息。 接收到该消息的客户端将事件记录在浏览器控制台中。 之后,客户端使用与用于发送原始事件的服务器相同的socket.emit。 在这种情况下,客户端会将接收到的FromClient事件发送到服务器。 当他收到消息时,该消息将记录在控制台窗口中。

自己尝试。 首先,在控制台中,启动您的Node应用程序:node index.js。 然后在浏览器中加载localhost :3000。

检查您的浏览器控制台,您将在JSON日志中看到以下内容:{hello:“ world”}

然后,在Node应用程序运行时,您将看到以下内容:

HTTP服务器在端口3000上启动
客户端连接已收到
{我:“数据”}

客户端和服务器都可以使用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函数,该函数使您每10分钟访问一次API(以我为例)以查找更新。 数据到达时,事件将发送到所有连接的客户端。 然后,通过Web浏览器中的JavaScript更新结果。

当首先启动Node应用程序时,也会调用MySportsFeeds。 结果将用于前10分钟间隔之前连接的所有客户端。 有关此的信息存储在全局变量中。 反过来,它会作为间隔调查的一部分进行更新。 这样可以确保每个客户都有相关的结果。

为了使index.js主文件中的内容一切正常,我创建了一个名为data.js的新文件。 它包含从index.js导出的函数,该函数先前调用了MySportsFeeds API。 这是此文件的完整内容:

 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); 

上面的前七行代码初始化所需的库,并调用lastData全局变量。 使用的库的最新列表是: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变量将使用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和称为JsRender的jQuery扩展。 这是一个模板库。 这将允许我创建一个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库定义。

取消播放游戏后,会出现一行,其中游戏以{{:game.time}}开始。

在游戏结束之前,将显示当前得分:{{:awayScore}}-{{:homeScore}}。 最后,这是一个小技巧,它将确定现在的时期,并阐明现在是否有休息时间。

如果currentIntermission变量出现在结果中,则我使用由ordinal_suffix_of定义的函数I,该函数将周期号转换为以下文本:1st(第二,第三等)中断。

没有中断时,我会寻找currentPeriod的值。 这里还使用Ordinal_suffix_of来显示游戏处于第1(第2,第3等)时期。

另外,我定义为time_left的另一个函数用于转换到周期结束之前剩余的秒数。 例如:10:12。

游戏结束时,代码的最后部分显示最终结果。

这是一个示例,说明存在混合完成的游戏,尚未完成的游戏和尚未开始的游戏(我不是一个很好的设计师,所以结果看起来像开发人员创建自定义时应该是这样)的混合列表自己动手的应用程序界面):

图片

接下来是一个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)); }); 

我有一个带数据ID的定界符。 模板渲染(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创建客户端连接到的服务器。 服务器检索数据并将其发送到客户端。 客户收到数据后,可以逐渐更新结果。 这减少了服务器上的负载,因为客户端仅在其从服务器接收事件时才采取行动。

服务器可以将消息发送到客户端,然后客户端也可以发送到服务器。 服务器收到消息后,将执行数据处理。

聊天应用程序的工作方式几乎相同。 服务器将从客户端接收消息,然后将所有数据传输到连接的客户端,以表明有人发送了新消息。

我希望您喜欢这篇文章,因为当我开发此实时体育应用程序时,我只是想分享自己的知识而感到非常高兴。 毕竟,曲棍球是我最喜欢的运动之一!

Source: https://habr.com/ru/post/zh-CN413957/


All Articles