带有Play Framework和Scala的实时图表:Web JVM上的极高生产力

作为一名顽固的后端开发人员,每当我考虑在JVM平台上使用某些UI构建Web应用程序时,我都会感到害怕。 这是有原因的:拥有JSFLiferayGrails等等的经验,...我不再想走这条路了。 但是,如果有需要,真的有选择的余地吗? 我发现了一个我认为很棒的游戏Play Framework

Play Framework建立在JVM之上,可以毫不费力地使用JavaScala创建Web应用程序。 它提供的宝贵和与众不同的区别:静态编译(甚至用于页面模板),易于入门和简洁( 此处有更多信息)。

为了演示Play Framework的出色之处,我想分享一下我开发简单Web应用程序的经验。 假设我们有几台主机,并且希望实时(在图表上)观察每台主机上的CPU使用情况。 当听到“实时”消息时,这可能意味着不同的意思,但是对于我们的应用程序而言,这意味着:使用WebSockets将数据从服务器推送到客户端。 尽管Play Framework支持纯Java API,但我将使用一些Scala代替,因为它使代码非常紧凑和清晰。

让我们开始吧! 下载Play Framework (撰写本文时,最新版本为2.1.1)后,让我们通过输入以下内容来创建应用

play new play-websockets-example

并选择Scala作为主要语言。 难怪在这里:如今这是一种非常标准的方法,对吗?

准备好我们的应用程序后,下一步就是创建一些起始网页。 Play Framework使用基于Scala的自己的类型安全模板引擎,它具有几个非常简单的规则,并且非常容易上手。 这是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 src="@routes.Assets.at("javascripts/highcharts.js")" type="text/javascript">
  </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").load( "/host/" + hostid,
    function( response, status, xhr ) {
      if (status == "error") {
        $("#content").html( "Sorry but there was an error:" + xhr.status + " " + xhr.statusText);
      }
    }
  )
}
</script>

除了有趣的结构(这是很好的描述了轿跑车在这里 ),它看起来非常像一个JavaScript代码的常规HTML。 该网页的结果是浏览器中主机的简单列表。 每当用户单击特定主机时,都会从服务器获取另一个视图(使用旧伙伴AJAX ),并在主机的右侧显示。 这是第二个(也是最后一个)模板views / host.scala.html

@(host: Host)( implicit request: RequestHeader )

<div id="content">
  <div id="chart">
  <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">
var socket = new WebSocket("@routes.Application.stats( host.id ).webSocketURL()")
socket.onmessage = 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 );
}
</script>

它看起来像是一个片段,而不是一个完整HTML页面,该页面只有一个图表,并通过侦听器打开WebSockets连接。 在HighchartsjQuery的巨大帮助下,JavaScript编程对于后端开发人员而言从未像现在这样容易。 此时,UI部分已完成。 让我们继续到后端。

首先,让我们定义路由表,该表仅包含三个URL,默认情况下位于conf / routes

GET     /                           controllers.Application.index
GET     /host/:id                   controllers.Application.host( id: String )
GET     /stats/:id                  controllers.Application.stats( id: String )

定义好视图和路线后,就该填充最后一个最有趣的部分了,这些控制器将所有部分粘合在一起(实际上,只有一个控制器, controllers / Application.scala )。 这是一个将索引操作映射到由views / dashboard.scala.html模板化的视图的代码段,就这么简单:

def index = Action {
  Ok( views.html.dashboard( "Dashboard", Hosts.hosts() ) )
}

对此操作的解释听起来像是这样:返回成功的响应代码,并使用两个参数titlehosts渲染模板views / dashboard.scala.html作为响应主体。 处理/ host /:id的动作看起来几乎相同:

def host( id: String ) = Action { implicit request =>
  Hosts.hosts.find( _.id == id ) match {
    case Some( host ) => Ok( views.html.host( host ) )
    case None => NoContent
  }    
}

这是在models / Hosts.scala中定义的Hosts对象。 为了简单起见,主机列表是硬编码的:

package models

case class Host( id: String, name: String )

object Hosts {  
  def hosts(): List[ Host ] = {
    return List( new Host( "h1", "Host 1" ), new Host( "h2", "Host 2" ) )
  } 
}

无聊的部分结束了,让我们继续最后但并非最不重要的实现:使用WebSockets服务器推送主机的CPU统计信息。 如您所见, / stats /:id URL已经映射到控制器操作,因此让我们看一下它的实现:

def stats( id: String ) = WebSocket.async[JsValue] { request =>
  Hosts.hosts.find( _.id == id ) match {
    case Some( host ) => Statistics.attach( host )
    case None => {
      val enumerator = Enumerator
        .generateM[JsValue]( Promise.timeout( None, 1.second ) )
        .andThen( Enumerator.eof )
      Promise.pure( ( Iteratee.ignore[JsValue], enumerator ) )
    }
  }
}

这里没有太多代码,但是如果您对Play Framework中的 WebSockets感到好奇,请单击此链接 。 这两行乍看起来可能有些怪异,但是一旦您阅读了文档并了解了Play Framework的基本设计原理,它就会看起来更加熟悉和友好。 Statistics对象是真正完成任务的对象,让我们看一下代码:

package models

import scala.concurrent.Future
import scala.concurrent.duration.DurationInt

import akka.actor.ActorRef
import akka.actor.Props
import akka.pattern.ask
import akka.util.Timeout
import play.api.Play.current
import play.api.libs.concurrent.Akka
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.api.libs.iteratee.Enumerator
import play.api.libs.iteratee.Iteratee
import play.api.libs.json.JsValue

case class Refresh()
case class Connect( host: Host )
case class Connected( enumerator: Enumerator[ JsValue ] )

object Statistics {
  implicit val timeout = Timeout( 5 second )
  var actors: Map[ String, ActorRef ] = Map()

  def actor( id: String ) = actors.synchronized {
    actors.find( _._1 == id ).map( _._2 ) match {
      case Some( actor ) => actor      
      case None => {
        val actor = Akka.system.actorOf( Props( new StatisticsActor(id) ), name = s"host-$id" )   
        Akka.system.scheduler.schedule( 0.seconds, 3.second, actor, Refresh )
        actors += ( id -> actor )
        actor
      }
    }
  }

  def attach( host: Host ): Future[ ( Iteratee[ JsValue, _ ], Enumerator[ JsValue ] ) ] = {
    ( actor( host.id ) ? Connect( host ) ).map {      
      case Connected( enumerator ) => ( Iteratee.ignore[JsValue], enumerator )
    }
  }
}

与往常一样,由于Scala的简洁性,没有太多的代码,但是发生了很多事情。 由于我们可能有数百个主机,因此合理地为每个主机分配自己的工作线程(而不是线程),或者更确切地说,为自己的参与者 。 为此,我们将使用另一个令人称奇的库Akka 。 上面的代码片段只是为主机创建一个actor ,或者使用已经创建的actor的注册表中的现有actor 。 请注意,该实现已相当简化,并省略了重要细节。 正确方向的想法是使用主管和其他高级概念来代替同步阻止。 还值得一提的是,我们希望将actor安排为预定任务:我们要求actor系统每3秒向actor发送一条“ 刷新”消息。 这意味着图表也将每三秒钟更新一次新值。

因此,当创建主机的actor时,我们向他发送一条消息Connect,通知正在建立新的连接。 当收到响应消息Connected时 ,我们从该方法返回,并且此时将通过WebSockets建立连接。 请注意,我们有意使用Iteratee.ignore [JsValue]忽略了来自客户端的任何输入。

这是StatisticsActor实现:

package models

import java.util.Date

import scala.util.Random

import akka.actor.Actor
import play.api.libs.iteratee.Concurrent
import play.api.libs.json.JsNumber
import play.api.libs.json.JsObject
import play.api.libs.json.JsString
import play.api.libs.json.JsValue

class StatisticsActor( hostid: String ) extends Actor {
  val ( enumerator, channel ) = Concurrent.broadcast[JsValue]

  def receive = {
    case Connect( host ) => sender ! Connected( enumerator )       
    case Refresh => broadcast( new Date().getTime(), hostid )
  }

  def broadcast( timestamp: Long, id: String ) {
    val msg = JsObject( 
      Seq( 
        "id" -> JsString( id ),
        "cpu" -> JsObject( 
          Seq( 
            ( "timestamp" -> JsNumber( timestamp ) ), 
            ( "load" -> JsNumber( Random.nextInt( 100 ) ) ) 
          ) 
        )
      )
    )

    channel.push( msg )
  }
}

CPU统计信息是随机生成的, 参与者仅每3秒将其作为简单的JSON对象广播一次。 在客户端,JavaScript代码解析此JSON并更新图表。 这是Mozilla Firefox中两个主机( 主机1主机2)的外观:

最后,我个人迄今为止对Play Framework所做的工作感到非常兴奋。 仅花了几个小时即可上手,又花了几个小时使事情按预期进行。 来自运行应用程序的错误报告和反馈周期绝对很棒,这要归功于Play Framework团队及其周围的社区。 我还有很多东西要学,但是值得去做。 请在GitHub上找到完整的源代码。


翻译自: https://www.javacodegeeks.com/2013/05/real-time-charts-with-play-framework-and-scala-extreme-productivity-on-jvm-for-web.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值