在今天的文章中,我将演示如何制作一个Web应用程序,以显示NHL的实时比赛得分。 分数将随着游戏的进行而自动更新。
对于我来说,这是一篇非常令人兴奋的文章,因为它使我有机会将我最喜欢的两种激情集中在一起:发展与体育。
用于创建应用程序的技术是:
- Node.js
- 套接字
- MySportsFeed.com
如果您尚未安装Node.js,请立即访问其下载页面并进行设置,然后再继续。
什么是Socket.io?
Socket.io是一种将客户端连接到服务器的技术。 在此示例中,客户端是Web浏览器,服务器是Node.js应用程序。 该服务器可以在任何给定时间连接到多个客户端。
建立连接后,服务器可以将消息发送到所有客户端或单个客户端。 作为回报,客户端可以将消息发送到服务器,从而实现双向实时通信。
在Socket.io之前,Web应用程序通常会使用AJAX,并且客户端和服务器都将互相轮询以查找事件。 例如,每10秒钟就会发生一次AJAX调用,以查看是否有任何消息要处理。
轮询消息会导致客户端和服务器上的大量开销,因为当没有消息时,它将不断寻找消息。
使用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的新文件,并将以下代码放入其中,以使用Express创建Web服务器:
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服务器:
var io = require('socket.io')(http);
io.on('connection', function(socket){
console.log('Client connection received');
});
与Express相似,代码从导入Socket.io库开始。 这存储在名为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
然后,在浏览器中,导航到http:// localhost:3000 。 什么都不会出现在页面上; 但是,如果您查看运行Node应用程序的控制台,则会记录两条消息:
- HTTP服务器在端口3000上启动
- 客户端连接已收到
现在我们已经成功建立了套接字连接,让我们使用它。 让我们从服务器向客户端发送一条消息开始。 然后,当客户端收到消息时,它可以将响应发送回服务器。
让我们看一下缩写的index.js文件:
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
事件响应将其返回给服务器。
这是在HTMLJavaScript部分中完成的,因此在index.html文件中,我对JavaScript进行了如下更新:
var socket = io();
socket.on('sendToClient', function (data) {
console.log(data);
socket.emit('receivedFromClient', { my: 'data' });
});
使用实例化的套接字变量,我们在服务器上具有一个socket.on
函数的逻辑非常相似。 对于客户端,它正在侦听sendToClient
事件。 一旦客户端连接,服务器就会发送此消息。 客户端收到该消息后,会将其记录到浏览器的控制台中。 然后,客户端使用与服务器用于发送原始事件的socket.emit
相同的socket.emit
。 在这种情况下,客户端将receivedFromClient
事件发送回服务器。 服务器收到消息后,将记录到控制台窗口。
自己试试吧。 首先,在控制台中,运行您的Node应用程序: node index.js
。 然后在浏览器中加载http:// localhost:3000 。
检查Web浏览器控制台,您应该看到记录了以下JSON数据: {hello: "world"}
然后,在运行Node应用程序的命令提示符下,应该看到以下内容:
HTTP server started on port 3000
Client connection received
{ my: 'data' }
客户端和服务器都可以使用接收到的JSON数据执行特定任务。 连接到实时体育数据后,我们将了解更多有关此的信息。
体育数据
既然我们已经掌握了如何与客户端和服务器之间收发数据,可以利用它来提供实时更新。 我选择使用体育数据,尽管相同的理论不仅限于体育。 在开始这个项目之前,我研究了不同的体育数据。 我选择的一个是MySportsFeeds ,因为它们提供免费的开发人员帐户(我不以任何方式与他们联系)。 为了访问实时数据,我注册了一个帐户,然后进行了少量捐赠。 捐款起价为1美元,每10分钟更新一次数据。 这对于示例是有益的。
设置帐户后,您可以继续设置对其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
});
在上面的示例中,请确保使用您的用户名和密码替换对authenticate函数的调用。
以下代码执行对今天的NHL记分牌的API调用。 fordate
变量是今天指定的变量。 我还将force
设置为true
以便即使数据没有更改,也总是返回响应。
在当前设置下,API调用的结果将写入文本文件。 在最后一个示例中,将对此进行更改; 但是,出于演示目的,可以在文本编辑器中查看结果文件以了解响应的内容。 结果包含一个计分板对象。 该对象包含一个名为gameScore
的数组。 该对象存储每个游戏的结果。 每个对象都包含一个称为game
的子对象。 该对象提供有关谁在玩的信息。
在游戏对象之外,有一些变量可以提供游戏的当前状态。 数据根据游戏状态而变化。 例如,当游戏尚未开始时,只有很少的变量告诉我们游戏尚未进行且尚未开始。
进行游戏时,会提供有关得分,游戏进行时间以及剩余时间的其他数据。 当我们进入HTML在下一节中显示游戏时,我们将看到它的作用。
实时更新
我们已经将拼图中的所有内容都准备好了,因此现在是时候将拼图放在一起以揭示最终图片了。 当前,MySportsFeeds对将数据推送给我们的支持有限,因此我们将不得不从中轮询数据。 幸运的是,我们知道数据每10分钟仅更改一次,因此我们不需要通过过于频繁地轮询更改来增加开销。 一旦从它们中轮询数据,就可以将这些更新从服务器推送到所有连接的客户端。
为了执行轮询,我将使用JavaScript setInterval
函数每10分钟调用一次API(以我为例)以查找更新。 接收到数据后,事件将发送到所有连接的客户端。 当客户收到事件时,游戏分数将在网络浏览器中使用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
函数并返回调用结果,在这种情况下,该结果是Promise,它将在主应用程序中解决。
现在让我们看一下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,使用Express创建的Http Server,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;
});
接下来的几行为网站的根页面( http:// localhost:3000 / )设置了一条路由,并启动HTTP服务器以侦听端口3000。
接下来,设置Socket.io以查找连接。 接收到新的连接时,服务器将发出一个事件,该事件称为data,其内容为latestData
变量。
最后,最后的代码块创建了轮询间隔。 发生间隔时,将使用API调用的结果更新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);
您可能会注意到,当客户端连接并发出事件时,它会使用套接字变量来发出事件。 这种方法只会将事件发送到该连接的客户端。 在时间间隔内,全局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>
模板是使用脚本标签定义的。 它包含模板的ID和称为text/x-jsrender
的特殊脚本类型。 模板为每个游戏定义了一个容器div,其中包含一个应用某些基本样式的类游戏。 在这个div内,模板开始。
在下一个div中,将显示客队和主队。 这是通过将MySportsFeed数据中的游戏对象中的城市和球队名称连接在一起来完成的。
{{:game.awayTeam.City}}
是我定义一个对象的方法,该对象将在渲染模板时替换为物理值。 此语法由JsRender库定义。
显示团队之后,下一部分代码将执行一些条件逻辑。 unPlayed
播放游戏unPlayed
,将输出一个字符串,表明游戏将从{{:game.time}}
。
游戏未完成时,将显示Current Score: {{:awayScore}} - {{:homeScore}}
。 最后,一些棘手的小逻辑可以识别曲棍球比赛处于哪个时段或是否处于间歇状态。
如果结果中提供了变量currentIntermission
,那么我将使用定义的函数ordinal_suffix_of
,它将周期号转换为:1st(第2、3rd等)。
当它不间歇时,我会寻找currentPeriod
值。 这也使用ordinal_suffix_of
来显示游戏处于第一(第二,第三等)时期。
在此之下,我定义的另一个名为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));
});
我有一个占位符div,其中包含数据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应用程序,然后浏览至http:// localhost:3000亲自查看结果!
每隔X分钟,服务器将向客户端发送一个事件。 客户端将使用更新的数据重画游戏元素。 因此,当您让站点保持打开状态并定期查看它时,您将看到当前正在进行游戏时刷新游戏数据。
结论
最终产品使用Socket.io创建客户端连接到的服务器。 服务器获取数据并将其发送到客户端。 客户端收到数据后,可以无缝更新显示。 因为客户端仅在接收到来自服务器的事件时才执行工作,所以这减少了服务器的负载。
插座不限于一个方向; 客户端还可以将消息发送到服务器。 服务器收到消息后,可以执行一些处理。
聊天应用程序通常以这种方式工作。 服务器将从客户端接收消息,然后广播到所有连接的客户端,以表明有人发送了新消息。
希望您喜欢这篇文章,因为我为我最喜欢的运动之一创建了此实时运动应用程序而大发雷霆!
翻译自: https://code.tutsplus.com/tutorials/real-time-sports-application-using-nodejs--cms-30594