【CSDN 編者按】大家都知道Web和API服務器在互聯網中的重要性,在計算機網絡方面提供了最基本的界面。本文主要介紹了怎樣利用Scala實現實時聊天網站和API服務器,通過本篇文章,你定將受益匪淺。
作者 | Haoyi譯者 |彎月,責編 | 劉靜出品 | CSDN(ID:)
以下為譯文:
Web和API服務器是互聯網系統的骨干,它們為計算機通過網絡交互提供了基本的界面,特別是在不同公司和組織之間。這篇指南將向你介紹如何利用Scala簡單的HTTP服務器,來提供Web內容和API。本文還會介紹一個完整的例子,告訴你如何構建簡單的實時聊天網站,同時支持HTML網頁和JSON API端點。
這篇文及章的目的是介紹怎樣用Scala實現簡單的HTTP服務器,從而提供網頁服務,以響應API請求。我們會建立一個簡單的聊天網站,可以讓用戶發表聊天信息,其他訪問網站的用戶都可以看見這些信息。為簡單起見,我們將忽略認證、性能、用戶掛歷、數據庫持久存儲等問題。但是,這篇文章應該足夠你開始用Scala構建網站和API服務器了,并為你學習并構建更多產品級項目打下基礎。
我們將使用Cask web框架:
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/.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",
????)
??}
}
如果你使用,那么可以運行如下命令來設置項目配置:
$?./mill?mill.scalalib.GenIdea/idea
現在你可以在中打開-0.3.0/目錄html提交表單到服務器,查看項目的目錄,也可以進行編輯。
可以利用Mill構建工具運行該程序,只需執行./mill:
$?./mill?-w?app.runBackground
該命令將在后臺運行Cask Web服務器,同時監視文件系統,如果文件發生了變化,則重啟服務器。然后我們可以使用瀏覽器瀏覽服務器,默認網址是:8080:
在/do-thing上還有個POST端點,可以在另一個終端上使用curl來訪問:
$?curl?-X?POST?--data?hello?http://localhost:8080/do-thing
olleh
可見,它接受數據hello,然后將反轉的字符串返回給客戶端。
然后可以運行app/test/src/.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網頁。最簡單的方式就是利用這個HTML生成庫。要在項目中使用,只需將其作為依賴項加入到build.sc文件即可:
??def?ivyDeps?=?Agg(
+????ivy"com.lihaoyi::scalatags:0.7.0",???
?????ivy"com.lihaoyi::cask:0.3.0"
???)
如果使用,那么還需要重新運行./mill mill../idea命令,來發現依賴項的變動,然后重新運行./mill -w app.讓Web服務器重新監聽改動。
然后,我們可以在.scala中導入:
package?app
+import?scalatags.Text.all._
?object?MinimalApplication?extends?cask.MainRoutes{
然后用一段最簡單的 HTML模板替換"Hello, World!"。
?def?hello()?=?{
-????"Hello?World!"
+????html(
+??????head(),
+??????body(
+????????h1("Hello!"),
+????????p("World")
+??????)
+????).render
???}
我們應該可以看到./mill -w app.命令重新編譯了代碼并重啟了服務器。然后刷新網頁額,就會看到純文本已經被替換成HTML頁面了。
為了讓頁面更好看一些,我們使用這個CSS框架。只需按照它的指南,使用link標簽引入:
?????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")
+????????)
???????)
現在字體不太一樣了:
雖然還不是最漂亮的網站,但現在已經足夠了。
在本節的末尾,我們修改一下的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框架和 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)
???????????),
這里我們簡單地使用了內存上的存儲。關于如何將消息持久存儲到數據庫中,我將在以后的文章中介紹。
接下來,我們需要讓頁面底部的兩個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.定義為根URL(即 / )添加了另一個處理函數,但該處理函數處理POST請求,而不處理GET請求。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")(
現在,當名字或信息非法時,就可以正確地顯示出錯誤信息了。
下一次提交時錯誤信息就會消失。
記住名字和消息
現在比較煩人的是,每次向聊天室中輸入消息時html提交表單到服務器,都要重新輸入用戶名。此外,如果用戶名或信息非法,那消息就會被清除,只能重新輸入并提交。可以讓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%")
這里我們使用了可選的和msg查詢參數,如果它們存在,則將其作為HTML input標簽的value的默認值。
接下來在的處理函數中渲染頁面時,填充和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。這樣做是正確的,因為我們只希望用戶在出錯時才進行編輯并重新提交。
完整的代碼.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實現動態頁面更新
現在有了一個簡單的、基于表單的聊天網站,用戶可以發表消息,其他用戶加載頁面即可看到已發表的消息。下一步就是讓網站變成動態的,這樣用戶不需要刷新頁面就能發表消息了。
為實現這一點,我們需要做兩件事情: