我将继续使用很棒的Play框架和Scala语言进行一次伟大的旅程,我想分享实时图表的另一种有趣的实现:这次使用轻量级服务器端事件,而不是本文前面介绍的全双工WebSockets技术。 确实,如果您不需要双向通信,而仅需要服务器推送,则服务器端事件看起来是很自然的选择。 而且,如果您使用的是Play Framework ,那么做起来也很容易。
让我们尝试覆盖相同的用例,以便比较这两种实现是公平的:我们有几个主机,并且我们希望实时(在图表上)观察每个主机上的CPU使用情况。 让我们从创建一个简单的Play Framework应用程序开始(选择Scala作为主要语言):
play new play-sse-example
现在,当我们的应用程序的布局准备就绪时,下一步就是创建一些起始网页(使用Play Framework的类型安全模板引擎)并将其命名为views / dashboard.scala.html 。 看起来是这样的:
@(title: String, hosts: List[Host])
<!DOCTYPE html>
<html>
<head>
<title>@title</title>
<link rel="stylesheet" media="screen" href="@routes.Assets.at("stylesheets/main.css")">
<link rel="shortcut icon" type="image/png" href="@routes.Assets.at("images/favicon.png")">
<script src="@routes.Assets.at("javascripts/jquery-1.9.0.min.js")" type="text/javascript"></script>
<script src="@routes.Assets.at("javascripts/highcharts.js")" type="text/javascript"></script>
</head>
<body>
<div id="hosts">
<ul class="hosts">
@hosts.map { host =>
<li>
<a href="#" onclick="javascript:show( '@host.id' )"><b>@host.name</b></a>
</li>
}
</ul>
</div>
<div id="content">
</div>
</body>
</html>
<script type="text/javascript">
function show( hostid ) {
$('#content').trigger('unload');
$("#content").load( "/host/" + hostid,
function( response, status, xhr ) {
if (status == "error") {
$("#content").html( "Sorry but there was an error:" + xhr.status + " " + xhr.statusText);
}
}
)
}
</script>
该模板看起来与WebSockets示例中的模板完全相同,只不过一行相同,稍后将对其进行解释。
$('#content').trigger('unload');
该网页的结果是一个简单的主机列表。 每当用户单击主机链接时,就会从服务器(使用AJAX )获取特定于主机的视图并显示该视图。 下一个模板是最有趣的模板,即views / host.scala.html ,其中包含许多重要的细节:
@(host: Host)( implicit request: RequestHeader )
<div id="content">
<div id="chart"></div>
<script type="text/javascript">
var charts = []
charts[ '@host.id' ] = new Highcharts.Chart({
chart: {
renderTo: 'chart',
defaultSeriesType: 'spline'
},
xAxis: {
type: 'datetime'
},
series: [{
name: "CPU",
data: []
}
]
});
</script>
</div>
<script type="text/javascript">
if( !!window.EventSource ) {
var event = new EventSource("@routes.Application.stats( host.id )");
event.addEventListener('message', function( event ) {
var datapoint = jQuery.parseJSON( event.data );
var chart = charts[ '@host.id' ];
chart.series[ 0 ].addPoint({
x: datapoint.cpu.timestamp,
y: datapoint.cpu.load
}, true, chart.series[ 0 ].data.length >= 50 );
} );
$('#content').bind('unload',function() {
event.close();
});
}
</script>
核心的UI组件是使用Highcharts库构建的简单图表。 底部的脚本块尝试创建EventSource对象,该对象是浏览器端服务器端事件的实现。 如果浏览器支持服务器端事件 ,则将创建到服务器端端点的相应连接,并且将从服务器收到的每个消息( “消息”侦听器)更新图表。 现在是解释此构造的目的的好时机(与上述$('#content')。trigger('unload')相对 ):
$('#content').bind('unload',function() {
event.close();
});
每当用户单击其他主机时,都应关闭前一个事件流并创建一个新事件流。 不这样做会导致创建越来越多的事件流,并用越来越多的事件侦听器充斥浏览器。 为了克服这个问题,我们将unload方法绑定到具有id 内容的div元素,并在用户单击主机时一直调用它。 这样,我们就一直关闭事件流,然后再打开新事件流。 足够的UI,让我们继续到后端。
路由表和几乎所有代码都保持相同,只是对统计信息.attach和Application.stats进行了两个小的更改。 让我们看一下如何在控制器端实现使用服务器端事件的服务器对主机CPU统计信息的推送(并映射到/ stats /:id URL):
def stats( id: String ) = Action { request =>
Hosts.hosts.find( _.id == id ) match {
case Some( host ) =>
Async {
Statistics.attach( host ).map { enumerator =>
Ok.stream( enumerator &> EventSource() ).as( "text/event-stream")
}
}
case None => NoContent
}
}
很短的一段代码,可以完成很多事情。 通过其ID找到相应的主机后,我们通过接收Enumerator实例“附加”到该主机:CPU统计数据的连续流。 Ok.stream(枚举数&> EventSource()) . as(“文本/事件流”)会将连续的统计数据流转换为客户端可以使用服务器端事件使用的事件流 。
为了完成服务器端的更改,让我们看一下“附加”到主机的统计信息流的样子:
def attach( host: Host ): Future[ Enumerator[ JsValue ] ] = {
( actor( host.id ) ? Connect( host ) ).map {
case Connected( enumerator ) => enumerator
}
}
就像返回Enumerator一样简单,并且由于我们使用的是Akka actor,因此使用Future和异步调用变得有些棘手。 而且,就是这样!
实际上,我们的简单应用程序如下所示(使用Mozilla Firefox ),仅以Host 1和Host 2为例:
非常简单,非常好,再次感谢Play Framework的开发人员和社区。 完整的源代码可在GitHub上获得 。