【CSDN 編者按】大家都知道Web和API伺服器在網際網路中的重要性,在計算機網絡方面提供了最基本的界面。本文主要介紹了怎樣利用Scala實現實時聊天網站和API伺服器,通過本篇文章,你定將受益匪淺。
作者 | Haoyi
譯者 | 彎月,責編 | 劉靜
以下為譯文:
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 appobject MinimalApplication extends cask.MainRoutes{@cask.get("/")defhello() = {"Hello World!" } @cask.post("/do-thing")defdoThing(request: cask.Request) = { new String(request.readAllBytes()).reverse } initialize()}
用build.sc進行構建:
import mill._, scalalib._object app extends ScalaModule{defscalaVersion = "2.13.0"defivyDeps = Agg(ivy"com.lihaoyi::cask:0.3.0" ) object test extends Tests{deftestFrameworks = Seq("utest.runner.Framework")defivyDeps = 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文件即可:
defivyDeps = 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:
packageapp+importscalatags.Text.all._objectMinimalApplicationextendscask.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 appimport 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("/")+ defpostHello(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("/")defpostHello(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("/")defpostHello(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"))+ elseif (name.length >= 10) hello(Some("Name cannot be longer than 10 characters"))+ elseif (msg == "") hello(Some("Message cannot be empty"))+ elseif (msg.length >= 160) hello(Some("Message cannot be longer than 160 characters"))+ else {+ messages = messages :+ (name -> msg)+ hello()+ } }
@cask.get("/")- defhello() = {+ defhello(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,再發送給用戶:
defpostHello(name: String, msg: String) = {- if (name == "") hello(Some("Name cannot be empty"))- elseif (name.length >= 10) hello(Some("Name cannot be longer than 10 characters"))- elseif (msg == "") hello(Some("Message cannot be empty"))- elseif (msg.length >= 160) hello(Some("Message cannot be longer than 160 characters"))+ if (name == "") hello(Some("Name cannot be empty"), Some(name), Some(msg))+ elseif (name.length >= 10) hello(Some("Name cannot be longer than 10 characters"), Some(name), Some(msg))+ elseif (msg == "") hello(Some("Message cannot be empty"), Some(name), Some(msg))+ elseif (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 appimport 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))elseif (name.length >= 10) hello(Some("Name cannot be longer than 10 characters"), Some(name), Some(msg))elseif (msg == "") hello(Some("Message cannot be empty"), Some(name), Some(msg))elseif (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("/")- defpostHello(name: String, msg: String) = {- if (name == "") hello(Some("Name cannot be empty"), Some(name), Some(msg))- elseif (name.length >= 10) hello(Some("Name cannot be longer than 10 characters"), Some(name), Some(msg))- elseif (msg == "") hello(Some("Message cannot be empty"), Some(name), Some(msg))- elseif (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("/")+ defpostHello(name: String, msg: String) = {+ if (name == "") ujson.Obj("success" -> false, "txt" -> "Name cannot be empty")+ elseif (name.length >= 10) ujson.Obj("success" -> false, "txt" -> "Name cannot be longer than 10 characters")+ elseif (msg == "") ujson.Obj("success" -> false, "txt" -> "Message cannot be empty")+ elseif (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 appimport 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"), )defmessageList() = { frag(for((name, msg) <- messages)yield p(b(name), " ", msg) ) } @cask.postJson("/")defpostHello(name: String, msg: String) = {if (name == "") ujson.Obj("success" -> false, "txt" -> "Name cannot be empty")elseif (name.length >= 10) ujson.Obj("success" -> false, "txt" -> "Name cannot be longer than 10 characters")elseif (msg == "") ujson.Obj("success" -> false, "txt" -> "Message cannot be empty")elseif (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("/")defhello() = { 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.emptyujson.Obj("success" -> true, "txt" -> messageList().render)
這樣,每當新的聊天消息提交時,就會將它發送給所有打開的連接,以通知它們。
最後,我們需要在瀏覽器的script標籤中添加一點Javascript代碼,來打開Websocket連接,並處理消息的交換:
var socket = new WebSocket("ws://" + location.host + "/subscribe");var eventIndex = 0socket.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 appimport 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]defmessageList() = { frag(for((name, msg) <- messages)yield p(b(name), " ", msg) ) } @cask.postJson("/")defpostHello(name: String, msg: String) = {if (name == "") ujson.Obj("success" -> false, "txt" -> "Name cannot be empty")elseif (name.length >= 10) ujson.Obj("success" -> false, "txt" -> "Name cannot be longer than 10 characters")elseif (msg == "") ujson.Obj("success" -> false, "txt" -> "Message cannot be empty")elseif (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("/")defhello() = { 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")defsubscribe() = { 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