【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请求-响应流之外互相发送消息。连接一旦建立,任何一方都可以在任何时间发送消息,消息可以包含任意字符串或任意字节。
我们要实现的流程如下:
网站加载后,浏览器建立到服务器的websocket连接
连接建立后,浏览器将发送消息"0"到服务器,表明它已准备好接收更新
服务器将响应初始的txt,其中包含所有已经渲染的消息,以及一个index,表示当前的消息计数
每当收到消息时,浏览器就会将最后看到的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 大家庭!