Scala编程:打造你的Web和API服务器

发表时间: 2019-10-07 12:21

【CSDN 编者按】大家都知道Web和API服务器在互联网中的重要性,在计算机网络方面提供了最基本的界面。本文主要介绍了怎样利用Scala实现实时聊天网站和API服务器,通过本篇文章,你定将受益匪浅

作者 | Haoyi

译者 | 弯月,责编 | 刘静

出品 | CSDN(ID:CSDNnews)

以下为译文:

Web和API服务器是互联网系统的骨干,它们为计算机通过网络交互提供了基本的界面,特别是在不同公司和组织之间。这篇指南将向你介绍如何利用Scala简单的HTTP服务器,来提供Web内容和API。本文还会介绍一个完整的例子,告诉你如何构建简单的实时聊天网站,同时支持HTML网页和JSON API端点。

这篇文及章的目的是介绍怎样用Scala实现简单的HTTP服务器,从而提供网页服务,以响应API请求。我们会建立一个简单的聊天网站,可以让用户发表聊天信息,其他访问网站的用户都可以看见这些信息。为简单起见,我们将忽略认证、性能、用户挂历、数据库持久存储等问题。但是,这篇文章应该足够你开始用Scala构建网站和API服务器了,并为你学习并构建更多产品级项目打下基础。

我们将使用Cask web框架:

http://www.lihaoyi.com/cask/

Cask是一个Scala的HTTP为框架,可以用来架设简单的网站并迅速运行。

开始

要开始使用Cask,只需下载并解压示例程序:

$ curl -L https://github.com/lihaoyi/cask/releases/download/0.3.0/minimalApplication-0.3.0.zip > cask.zip

$ unzip cask.zip

$ cd minimalApplication-0.3.0

运行find来看看有哪些文件:

$ find . -type f

./build.sc

./app/test/src/ExampleTests.scala

./app/src/MinimalApplication.scala

./mill

我们感兴趣的大部分代码都位于
app/src/MinimalApplication.scala中。

package app

object MinimalApplication extends cask.MainRoutes{

@cask.get("/")

def hello= {

"Hello World!"

}

@cask.post("/do-thing")

def doThing(request: cask.Request)= {

new String(request.readAllBytes).reverse

}

initialize

}

用build.sc进行构建:

import mill._, scalalib._

object app extends ScalaModule{

def scalaVersion = "2.13.0"

def ivyDeps = Agg(

ivy"com.lihaoyi::cask:0.3.0"

)

object test extends Tests{

def testFrameworks = Seq("utest.runner.Framework")

def ivyDeps = Agg(

ivy"com.lihaoyi::utest::0.7.1",

ivy"com.lihaoyi::requests::0.2.0",

)

}

}

如果你使用Intellij,那么可以运行如下命令来设置Intellij项目配置:

$ ./mill mill.scalalib.GenIdea/idea

现在你可以在Intellij中打开minimalApplication-0.3.0/目录,查看项目的目录,也可以进行编辑。

可以利用Mill构建工具运行该程序,只需执行./mill:

$ ./mill -w app.runBackground

该命令将在后台运行Cask Web服务器,同时监视文件系统,如果文件发生了变化,则重启服务器。然后我们可以使用浏览器浏览服务器,默认网址是localhost:8080:

在/do-thing上还有个POST端点,可以在另一个终端上使用curl来访问:

$ curl -X POST --data hello http://localhost:8080/do-thingolleh

可见,它接受数据hello,然后将反转的字符串返回给客户端。

然后可以运行
app/test/src/ExampleTests.scala中的自动化测试:

$ ./mill clean app.runBackground # stop the webserver running in the background

$ ./mill app.test

[50/56] app.test.compile

[info] Compiling 1 Scala source to /Users/lihaoyi/test/minimalApplication-0.3.0/out/app/test/compile/dest/classes ...

[info] Done compiling.

[56/56] app.test.test

-------------------------------- Running Tests --------------------------------

+ app.ExampleTests.MinimalApplication 629ms

现在基本的东西已经运行起来了,我们来重新运行Web服务器:

$ ./mill -w app.runBackground

然后开始实现我们的聊天网站!

提供HTML服务

第一件事就是将纯文本的"Hello, World!"转换成HTML网页。最简单的方式就是利用Scalatags这个HTML生成库。要在项目中使用Scalatags,只需将其作为依赖项加入到build.sc文件即可:

def ivyDeps = Agg(

+ ivy"com.lihaoyi::scalatags:0.7.0",

ivy"com.lihaoyi::cask:0.3.0"

)

如果使用Intellij,那么还需要重新运行./mill
mill.scalalib.GenIdea/idea命令,来发现依赖项的变动,然后重新运行./mill -w app.runBackground让Web服务器重新监听改动。

然后,我们可以在MinimalApplication.scala中导入Scalatags:

package app

+import scalatags.Text.all._

object MinimalApplication extends cask.MainRoutes{

然后用一段最简单的Scalatags HTML模板替换"Hello, World!"。

def hello = {

- "Hello World!"

+ html(

+ head,

+ body(

+ h1("Hello!"),

+ p("World")

+ )

+ ).render

}

我们应该可以看到./mill -w app.runBackground命令重新编译了代码并重启了服务器。然后刷新网页额,就会看到纯文本已经被替换成HTML页面了。

Bootstrap

为了让页面更好看一些,我们使用Bootstrap这个CSS框架。只需按照它的指南,使用link标签引入bootstrap:

head(

+ link(

+ rel := "stylesheet",

+ href := "https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"

+ )

),

body(

- h1("Hello!"),

- p("World")

+ div(cls := "container")(

+ h1("Hello!"),

+ p("World")

+ )

)

现在字体不太一样了:

虽然还不是最漂亮的网站,但现在已经足够了。

在本节的末尾,我们修改一下Scalatags的HTML模板,加上硬编码的聊天文本和假的输入框,让它看起来更像一个聊天应用程序。

body(

div(cls := "container")(

- h1("Hello!"),

- p("World")

+ h1("Scala Chat!"),

+ hr,

+ div(

+ p(b("alice"), " ", "Hello World!"),

+ p(b("bob"), " ", "I am cow, hear me moo"),

+ p(b("charlie"), " ", "I weigh twice as much as you")

+ ),

+ hr,

+ div(

+ input(`type` := "text", placeholder := "User name", width := "20%"),

+ input(`type` := "text", placeholder := "Please write a message!", width := "80%")

+ )

)

)

现在我们有了一个简单的静态网站,其利用Cask web框架和Scalatags HTML库提供HTML网页服务。现在的服务器代码如下所示:

package app

import scalatags.Text.all._

object MinimalApplication extends cask.MainRoutes{

@cask.get("/")

def hello = {

html(

head(

link(

rel := "stylesheet",

href := "https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"

)

),

body(

div(cls := "container")(

h1("Scala Chat!"),

hr,

div(

p(b("alice"), " ", "Hello World!"),

p(b("bob"), " ", "I am cow, hear me moo"),

p(b("charlie"), " ", "I weigh twice as much as you")

),

hr,

div(

input(`type` := "text", placeholder := "User name", width := "20%"),

input(`type` := "text", placeholder := "Please write a message!", width := "80%")

)

)

)

).render

}

initialize

}

接下来,我们来看看怎样让它支持交互!

表单和数据

为网站添加交互的第一次尝试是使用HTML表单。首先我们要删掉硬编码的消息列表,转而根据数据来输出HTML网页:

object MinimalApplication extends cask.MainRoutes{

+ var messages = Vector(

+ ("alice", "Hello World!"),

+ ("bob", "I am cow, hear me moo"),

+ ("charlie", "I weigh twice as much as you"),

+ )

@cask.get("/")

div(

- p(b("alice"), " ", "Hello World!"),

- p(b("bob"), " ", "I am cow, hear me moo"),

- p(b("charlie"), " ", "I weight twice as much as you")

+ for((name, msg) <- messages)

+ yield p(b(name), " ", msg)

),

这里我们简单地使用了内存上的mssages存储。关于如何将消息持久存储到数据库中,我将在以后的文章中介绍。

接下来,我们需要让页面底部的两个input支持交互。为实现这一点,我们需要将它们包裹在form元素中:

hr,

- div(

- input(`type` := "text", placeholder := "User name", width := "20%"),

- input(`type` := "text", placeholder := "Please write a message!", width := "80%")

+ form(action := "/", method := "post")(

+ input(`type` := "text", name := "name", placeholder := "User name", width := "20%"),

+ input(`type` := "text", name := "msg", placeholder := "Please write a message!", width := "60%"),

+ input(`type` := "submit", width := "20%")

)

这样我们就有了一个可以交互的表单,外观跟之前的差不多。但是,提交表单会导致Error 404: Not Found错误。这是因为我们还没有将表单与服务器连接起来,来处理表单提交并获取新的聊天信息。我们可以这样做:

- )

+

+ @cask.postForm("/")

+ def postHello(name: String, msg: String)= {

+ messages = messages :+ (name -> msg)

+ hello

+ }

+

@cask.get("/")

@cast.postForm定义为根URL(即 / )添加了另一个处理函数,但该处理函数处理POST请求,而不处理GET请求。Cask文档(http://www.lihaoyi.com/cask/)中还有关于@cask.*注释的其他例子,你可以利用它们来定义处理函数。

验证

现在,用户能够以任何名字提交任何评论。但是,并非所有的评论和名字都是有效的:最低限度,我们希望保证评论和名字字段非空,同时我们还需要限制最大长度。

实现这一点很简单:

@cask.postForm("/")

def postHello(name: String, msg: String)= {

- messages = messages :+ (name -> msg)

+ if (name != "" && name.length < 10 && msg != "" && msg.length < 160){

+ messages = messages :+ (name -> msg)

+ }

hello

}

这样就可以阻止用户输入非法的name和msg,但出现了另一个问题:用户输入了非法的名字或信息并提交,那么这些信息就会消失,而且不会为错误产生任何反馈。解决方法是,给hello页面渲染一个可选的错误信息,用它来告诉用户出现了什么问题:

@cask.postForm("/")

def postHello(name: String, msg: String)= {

- if (name != "" && name.length < 10 && msg != "" && msg.length < 160){

- messages = messages :+ (name -> msg)

- }

- hello

+ if (name == "") hello(Some("Name cannot be empty"))

+ else if (name.length >= 10) hello(Some("Name cannot be longer than 10 characters"))

+ else if (msg == "") hello(Some("Message cannot be empty"))

+ else if (msg.length >= 160) hello(Some("Message cannot be longer than 160 characters"))

+ else {

+ messages = messages :+ (name -> msg)

+ hello

+ }

}

@cask.get("/")

- def hello= {

+ def hello(errorOpt: Option[String] = None)= {

html(

hr,

+ for(error <- errorOpt)

+ yield i(color.red)(error),

form(action := "/", method := "post")(

现在,当名字或信息非法时,就可以正确地显示出错误信息了。

下一次提交时错误信息就会消失。

记住名字和消息

现在比较烦人的是,每次向聊天室中输入消息时,都要重新输入用户名。此外,如果用户名或信息非法,那消息就会被清除,只能重新输入并提交。可以让hello页面处理函数来填充这些字段,这样就可以解决:

@cask.get("/")

- def hello(errorOpt: Option[String] = None) = {

+ def hello(errorOpt: Option[String] = None,

+ userName: Option[String] = None,

+ msg: Option[String] = None) = {

html(

form(action := "/", method := "post")(

- input(`type` := "text", name := "name", placeholder := "User name", width := "20%", userName.map(value := _)),

- input(`type` := "text", name := "msg", placeholder := "Please write a message!", width := "60%"),

+ input(

+ `type` := "text",

+ name := "name",

+ placeholder := "User name",

+ width := "20%",

+ userName.map(value := _)

+ ),

+ input(

+ `type` := "text",

+ name := "msg",

+ placeholder := "Please write a message!",

+ width := "60%",

+ msg.map(value := _)

+ ),

input(`type` := "submit", width := "20%")

这里我们使用了可选的userName和msg查询参数,如果它们存在,则将其作为HTML input标签的value的默认值。

接下来在postHello的处理函数中渲染页面时,填充userName和msg,再发送给用户:

def postHello(name: String, msg: String)= {

- if (name == "") hello(Some("Name cannot be empty"))

- else if (name.length >= 10) hello(Some("Name cannot be longer than 10 characters"))

- else if (msg == "") hello(Some("Message cannot be empty"))

- else if (msg.length >= 160) hello(Some("Message cannot be longer than 160 characters"))

+ if (name == "") hello(Some("Name cannot be empty"), Some(name), Some(msg))

+ else if (name.length >= 10) hello(Some("Name cannot be longer than 10 characters"), Some(name), Some(msg))

+ else if (msg == "") hello(Some("Message cannot be empty"), Some(name), Some(msg))

+ else if (msg.length >= 160) hello(Some("Message cannot be longer than 160 characters"), Some(name), Some(msg))

else {

messages = messages :+ (name -> msg)

- hello

+ hello(None, Some(name), None)

}

注意任何情况下我们都保留name,但只有错误的情况才保留msg。这样做是正确的,因为我们只希望用户在出错时才进行编辑并重新提交。

完整的代码MinimalApplication.scala如下所示:

package app

import scalatags.Text.all._

object MinimalApplication extends cask.MainRoutes{

var messages = Vector(

("alice", "Hello World!"),

("bob", "I am cow, hear me moo"),

("charlie", "I weigh twice as you"),

)

@cask.postForm("/")

def postHello(name: String, msg: String) = {

if (name == "") hello(Some("Name cannot be empty"), Some(name), Some(msg))

else if (name.length >= 10) hello(Some("Name cannot be longer than 10 characters"), Some(name), Some(msg))

else if (msg == "") hello(Some("Message cannot be empty"), Some(name), Some(msg))

else if (msg.length >= 160) hello(Some("Message cannot be longer than 160 characters"), Some(name), Some(msg))

else {

messages = messages :+ (name -> msg)

hello(None, Some(name), None)

}

}

@cask.get("/")

def hello(errorOpt: Option[String] = None,

userName: Option[String] = None,

msg: Option[String] = None) = {

html(

head(

link(

rel := "stylesheet",

href := "https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"

)

),

body(

div(cls := "container")(

h1("Scala Chat!"),

hr,

div(

for((name, msg) <- messages)

yield p(b(name), " ", msg)

),

hr,

for(error <- errorOpt)

yield i(color.red)(error),

form(action := "/", method := "post")(

input(

`type` := "text",

name := "name",

placeholder := "User name",

width := "20%",

userName.map(value := _)

),

input(

`type` := "text",

name := "msg",

placeholder := "Please write a message!",

width := "60%",

msg.map(value := _)

),

input(`type` := "submit", width := "20%")

)

)

)

).render

}

initialize

}

利用Ajax实现动态页面更新

现在有了一个简单的、基于表单的聊天网站,用户可以发表消息,其他用户加载页面即可看到已发表的消息。下一步就是让网站变成动态的,这样用户不需要刷新页面就能发表消息了。

为实现这一点,我们需要做两件事情:

允许HTTP服务器发送网页的一部分,例如接收消息并渲染消息列表,而不是渲染整个页面

添加一些Javascript来手动提交表单数据。

渲染页面的一部分

要想只渲染需要更新的那部分页面,我们可以重构下代码,从hello页面处理函数中提取出messageList辅助函数:

)

+

+ def messageList = {

+ frag(

+ for((name, msg) <- messages)

+ yield p(b(name), " ", msg)

+ )

+ }

+

@cask.postForm("/")

hr,

- div(

- for((name, msg) <- messages)

- yield p(b(name), " ", msg)

+ div(id := "messageList")(

+ messageList

),

接下来,我们可以修改postHello处理函数,从而仅渲染可能发生了变化的messageList,而不是渲染整个页面:

- @cask.postForm("/")

- def postHello(name: String, msg: String)= {

- if (name == "") hello(Some("Name cannot be empty"), Some(name), Some(msg))

- else if (name.length >= 10) hello(Some("Name cannot be longer than 10 characters"), Some(name), Some(msg))

- else if (msg == "") hello(Some("Message cannot be empty"), Some(name), Some(msg))

- else if (msg.length >= 160) hello(Some("Message cannot be longer than 160 characters"), Some(name), Some(msg))

- else {

- messages = messages :+ (name -> msg)

- hello(None, Some(name), None)

+ @cask.postJson("/")

+ def postHello(name: String, msg: String)= {

+ if (name == "") ujson.Obj("success" -> false, "txt" -> "Name cannot be empty")

+ else if (name.length >= 10) ujson.Obj("success" -> false, "txt" -> "Name cannot be longer than 10 characters")

+ else if (msg == "") ujson.Obj("success" -> false, "txt" -> "Message cannot be empty")

+ else if (msg.length >= 160) ujson.Obj("success" -> false, "txt" -> "Message cannot be longer than 160 characters")

+ else {

+ messages = messages :+ (name -> msg)

+ ujson.Obj("success" -> true, "txt" -> messageList.render)

}

}

注意我们这里用@cask.postJson替换了@cask.postForm,此外不再调用hello来重新渲染整个页面,而是仅返回一个很小的JSON结构ujson.Obj,这样浏览器可以利用它更新HTML页面。ujson.Obj数据类型由uJson库提供。

利用Javascript更新页面

现在我们写好了服务器端代码,接下来我们编写相关的客户端代码,从服务器接收JSON响应,并利用它来更新HTML界面

要处理客户端逻辑,我们需要给一些关键的HTML元素添加ID,这样才能在Javascript中访问它们:

hr,

- for(error <- errorOpt)

- yield i(color.red)(error),

+ div(id := "errorDiv", color.red),

form(action := "/", method := "post")(

input(

`type` := "text",

- name := "name",

+ id := "nameInput",

placeholder := "User name",

width := "20%"

),

input(

`type` := "text",

- name := "msg",

+ id := "msgInput",

placeholder := "Please write a message!",

width := "60%"

),

接下来,在页面头部引入一系列Javascript:

head(

link(

rel := "stylesheet",

href := "https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"

- ),

+ )

+ script(raw("""

+ function submitForm{

+ fetch(

+ "/",

+ {

+ method: "POST",

+ body: JSON.stringify({name: nameInput.value, msg: msgInput.value})

+ }

+ ).then(response => response.json)

+ .then(json => {

+ if (json.success) {

+ messageList.innerHTML = json.txt

+ msgInput.value = ""

+ errorDiv.innerText = ""

+ } else {

+ errorDiv.innerText = json.txt

+ }

+ })

+ }

+ """))

),

从表单的onsubmit处理函数中调用该Javascript函数:

- form(action := "/", method := "post")(+ form(onsubmit := "submitForm; return false")(

这样就可以了。现在向网站添加聊天文本,文本就会立即出现在网页上,之后加载页面的其他人也能看见。

最终的代码如下:

package app

import scalatags.Text.all._

object MinimalApplication extends cask.MainRoutes{

var messages = Vector(

("alice", "Hello World!"),

("bob", "I am cow, hear me moo"),

("charlie", "I weigh twice as you"),

)

def messageList= {

frag(

for((name, msg) <- messages)

yield p(b(name), " ", msg)

)

}

@cask.postJson("/")

def postHello(name: String, msg: String)= {

if (name == "") ujson.Obj("success" -> false, "txt" -> "Name cannot be empty")

else if (name.length >= 10) ujson.Obj("success" -> false, "txt" -> "Name cannot be longer than 10 characters")

else if (msg == "") ujson.Obj("success" -> false, "txt" -> "Message cannot be empty")

else if (msg.length >= 160) ujson.Obj("success" -> false, "txt" -> "Message cannot be longer than 160 characters")

else {

messages = messages :+ (name -> msg)

ujson.Obj("success" -> true, "txt" -> messageList.render)

}

}

@cask.get("/")

def hello= {

html(

head(

link(

rel := "stylesheet",

href := "https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"

),

script(raw("""

function submitForm{

fetch(

"/",

{

method: "POST",

body: JSON.stringify({name: nameInput.value, msg: msgInput.value})

}

).then(response => response.json)

.then(json => {

if (json.success) {

messageList.innerHTML = json.txt

msgInput.value = ""

errorDiv.innerText = ""

} else {

errorDiv.innerText = json.txt

}

})

}

"""))

),

body(

div(cls := "container")(

h1("Scala Chat!"),

hr,

div(id := "messageList")(

messageList

),

hr,

div(id := "errorDiv", color.red),

form(onsubmit := "submitForm; return false")(

input(

`type` := "text",

id := "nameInput",

placeholder := "User name",

width := "20%"

),

input(

`type` := "text",

id := "msgInput",

placeholder := "Please write a message!",

width := "60%"

),

input(`type` := "submit", width := "20%")

)

)

)

).render

}

initialize

}

注意尽管你输入的消息你自己可以立即看到,但其他人只有刷新页面,或者输入自己的消息迫使messageList重新加载,才能看到你的消息。本文的最后一节将介绍怎样让所有人立即看到你的消息,而不需要手动刷新。

利用Websockets实时更新页面

推送更新的概念和简单:每次提交新消息后,就将消息”推送"到所有监听中的浏览器上,而不是等待浏览器刷新并“拉取”更新后的数据。实现这一目的有多种方法。本文我们使用Websockets。

Websockets可以让浏览器和服务器在正常的HTTP请求-响应流之外互相发送消息。连接一旦建立,任何一方都可以在任何时间发送消息,消息可以包含任意字符串或任意字节。

我们要实现的流程如下:

  1. 网站加载后,浏览器建立到服务器的websocket连接

  2. 连接建立后,浏览器将发送消息"0"到服务器,表明它已准备好接收更新

  3. 服务器将响应初始的txt,其中包含所有已经渲染的消息,以及一个index,表示当前的消息计数

  4. 每当收到消息时,浏览器就会将最后看到的index发送给服务器,然后等待新消息出现,再按照步骤3进行响应

在服务器上实现这一点的关键就是保持所有已打开的连接的集合:

var openConnections = Set.empty[cask.WsChannelActor]

该集合包含当前等待更新的浏览器的列表。每当新消息出现时,我们就会向这个列表进行广播。

接下来,定义@cask.websocket处理函数,接收进入的websocket连接并处理:

@cask.websocket("/subscribe")

def subscribe = {

cask.WsHandler { connection =>

cask.WsActor {

case cask.Ws.Text(msg) =>

if (msg.toInt < messages.length){

connection.send(

cask.Ws.Text(

ujson.Obj("index" -> messages.length, "txt" -> messageList.render).render

)

)

}else{

openConnections += connection

}

case cask.Ws.Close(_, _) => openConnections -= connection

}

}

}

该处理函数接收来自浏览器的msg,检查其内容是否应该立即响应,还是应该利用openConnections注册一个连接再稍后响应。

我们需要在postHello处理函数中做类似的修改:

messages = messages :+ (name -> msg)

+ val notification = cask.Ws.Text(

+ ujson.Obj("index" -> messages.length, "txt" -> messageList.render).render

+ )

+ for(conn <- openConnections) conn.send(notification)

+ openConnections = Set.empty

ujson.Obj("success" -> true, "txt" -> messageList.render)

这样,每当新的聊天消息提交时,就会将它发送给所有打开的连接,以通知它们。

最后,我们需要在浏览器的script标签中添加一点Javascript代码,来打开Websocket连接,并处理消息的交换:

var socket = new WebSocket("ws://" + location.host + "/subscribe");

var eventIndex = 0

socket.onopen = function(ev){ socket.send("" + eventIndex) }

socket.onmessage = function(ev){

var json = JSON.parse(ev.data)

eventIndex = json.index

socket.send("" + eventIndex)

messageList.innerHTML = json.txt

}

这里,我们首先打开一个连接,发送第一条"0"消息来启动整个流程,然后每次收到更新后,就将json.txt渲染到messageList中,然后将json.index发送回服务器,来订阅下一次更新。

现在,同时打开两个浏览器,就会看到一个窗口中发送的聊天消息立即出现在另一个窗口中:

本节的完整代码如下:

package app

import scalatags.Text.all._

object MinimalApplication extends cask.MainRoutes{

var messages = Vector(

("alice", "Hello World!"),

("bob", "I am cow, hear me moo"),

("charlie", "I weigh twice as you"),

)

var openConnections = Set.empty[cask.WsChannelActor]

def messageList= {

frag(

for((name, msg) <- messages)

yield p(b(name), " ", msg)

)

}

@cask.postJson("/")

def postHello(name: String, msg: String)= {

if (name == "") ujson.Obj("success" -> false, "txt" -> "Name cannot be empty")

else if (name.length >= 10) ujson.Obj("success" -> false, "txt" -> "Name cannot be longer than 10 characters")

else if (msg == "") ujson.Obj("success" -> false, "txt" -> "Message cannot be empty")

else if (msg.length >= 160) ujson.Obj("success" -> false, "txt" -> "Message cannot be longer than 160 characters")

else {

messages = messages :+ (name -> msg)

val notification = cask.Ws.Text(

ujson.Obj("index" -> messages.length, "txt" -> messageList.render).render

)

for(conn <- openConnections) conn.send(notification)

openConnections = Set.empty

ujson.Obj("success" -> true, "txt" -> messageList.render)

}

}

@cask.get("/")

def hello= {

html(

head(

link(

rel := "stylesheet",

href := "https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"

),

script(raw("""

function submitForm{

fetch(

"/",

{

method: "POST",

body: JSON.stringify({name: nameInput.value, msg: msgInput.value})

}

).then(response => response.json)

.then(json => {

if (json.success) {

messageList.innerHTML = json.txt

msgInput.value = ""

errorDiv.innerText = ""

} else {

errorDiv.innerText = json.txt

}

})

}

var socket = new WebSocket("ws://" + location.host + "/subscribe");

socket.onopen = function(ev){ socket.send("0") }

socket.onmessage = function(ev){

var json = JSON.parse(ev.data)

messageList.innerHTML = json.txt

socket.send("" + json.index)

}

"""))

),

body(

div(cls := "container")(

h1("Scala Chat!"),

hr,

div(id := "messageList")(

messageList

),

hr,

div(id := "errorDiv", color.red),

form(onsubmit := "submitForm; return false")(

input(

`type` := "text",

id := "nameInput",

placeholder := "User name",

width := "20%"

),

input(

`type` := "text",

id := "msgInput",

placeholder := "Please write a message!",

width := "60%"

),

input(`type` := "submit", width := "20%")

)

)

)

).render

}

@cask.websocket("/subscribe")

def subscribe= {

cask.WsHandler { connection =>

cask.WsActor {

case cask.Ws.Text(msg) =>

if (msg.toInt < messages.length){

connection.send(

cask.Ws.Text(

ujson.Obj("index" -> messages.length, "txt" -> messageList.render).render

)

)

}else{

openConnections += connection

}

case cask.Ws.Close(_, _) => openConnections -= connection

}

}

}

initialize

}

总结

本文我们介绍了怎样利用Scala实现实时聊天网站和API服务器。我们从静态网站开始,添加基于表单的交互,再利用Ajax访问JSON API实现动态页面,最后利用websocket实现推送通知。我们使用了Cask web框架,Scalatags HTML库,以及uJson序列化库,代码大约125行。

这里展示的聊天网站非常简单。我们故意忽略了将消息保存到持久数据库、认证、用户账号、多聊天室、使用量限制以及许多其他的功能。这里仅使用了内存上的messages列表和openConnections集合,从并发更新的角度来看,它们并非线程安全的。但无论如何,希望这篇文章能够让你体会到怎样使用Scala实现简单的网站和API服务器,进而用它构建更大、更宏伟的应用程序。

英文:Simple Web and Api Servers with Scala

原文链接:http://www.lihaoyi.com
/post/SimpleWebandApiServerswithScala.html

【END】

CSDN 博客诚邀入驻啦!

本着共享、协作、开源、技术之路我们共同进步的准则

只要你技术够干货,内容够扎实,分享够积极

欢迎加入 CSDN 大家庭!