【CSDN 編者按】大家都知道Web和API服務(wù)器在互聯(lián)網(wǎng)中的重要性,在計(jì)算機(jī)網(wǎng)絡(luò)方面提供了最基本的界面。本文主要介紹了怎樣利用Scala實(shí)現(xiàn)實(shí)時(shí)聊天網(wǎng)站和API服務(wù)器,通過(guò)本篇文章,你定將受益匪淺。
作者 | Haoyi譯者 |彎月,責(zé)編 | 劉靜出品 | CSDN(ID:)
以下為譯文:
Web和API服務(wù)器是互聯(lián)網(wǎng)系統(tǒng)的骨干,它們?yōu)橛?jì)算機(jī)通過(guò)網(wǎng)絡(luò)交互提供了基本的界面,特別是在不同公司和組織之間。這篇指南將向你介紹如何利用Scala簡(jiǎn)單的HTTP服務(wù)器,來(lái)提供Web內(nèi)容和API。本文還會(huì)介紹一個(gè)完整的例子,告訴你如何構(gòu)建簡(jiǎn)單的實(shí)時(shí)聊天網(wǎng)站,同時(shí)支持HTML網(wǎng)頁(yè)和JSON API端點(diǎn)。
這篇文及章的目的是介紹怎樣用Scala實(shí)現(xiàn)簡(jiǎn)單的HTTP服務(wù)器,從而提供網(wǎng)頁(yè)服務(wù),以響應(yīng)API請(qǐng)求。我們會(huì)建立一個(gè)簡(jiǎn)單的聊天網(wǎng)站,可以讓用戶發(fā)表聊天信息,其他訪問(wèn)網(wǎng)站的用戶都可以看見(jiàn)這些信息。為簡(jiǎn)單起見(jiàn),我們將忽略認(rèn)證、性能、用戶掛歷、數(shù)據(jù)庫(kù)持久存儲(chǔ)等問(wèn)題。但是,這篇文章應(yīng)該足夠你開(kāi)始用Scala構(gòu)建網(wǎng)站和API服務(wù)器了,并為你學(xué)習(xí)并構(gòu)建更多產(chǎn)品級(jí)項(xiàng)目打下基礎(chǔ)。
我們將使用Cask web框架:
Cask是一個(gè)Scala的HTTP為框架,可以用來(lái)架設(shè)簡(jiǎn)單的網(wǎng)站并迅速運(yùn)行。
開(kāi)始
要開(kāi)始使用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
運(yùn)行find來(lái)看看有哪些文件:
$?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進(jìn)行構(gòu)建:
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",
????)
??}
}
如果你使用,那么可以運(yùn)行如下命令來(lái)設(shè)置項(xiàng)目配置:
$?./mill?mill.scalalib.GenIdea/idea
現(xiàn)在你可以在中打開(kāi)-0.3.0/目錄html提交表單到服務(wù)器,查看項(xiàng)目的目錄,也可以進(jìn)行編輯。
可以利用Mill構(gòu)建工具運(yùn)行該程序,只需執(zhí)行./mill:
$?./mill?-w?app.runBackground
該命令將在后臺(tái)運(yùn)行Cask Web服務(wù)器,同時(shí)監(jiān)視文件系統(tǒng),如果文件發(fā)生了變化,則重啟服務(wù)器。然后我們可以使用瀏覽器瀏覽服務(wù)器,默認(rèn)網(wǎng)址是:8080:
在/do-thing上還有個(gè)POST端點(diǎn),可以在另一個(gè)終端上使用curl來(lái)訪問(wèn):
$?curl?-X?POST?--data?hello?http://localhost:8080/do-thing
olleh
可見(jiàn),它接受數(shù)據(jù)hello,然后將反轉(zhuǎn)的字符串返回給客戶端。
然后可以運(yùn)行app/test/src/.scala中的自動(dòng)化測(cè)試:
$?./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
現(xiàn)在基本的東西已經(jīng)運(yùn)行起來(lái)了,我們來(lái)重新運(yùn)行Web服務(wù)器:
$?./mill?-w?app.runBackground
然后開(kāi)始實(shí)現(xiàn)我們的聊天網(wǎng)站!
提供HTML服務(wù)
第一件事就是將純文本的"Hello, World!"轉(zhuǎn)換成HTML網(wǎng)頁(yè)。最簡(jiǎn)單的方式就是利用這個(gè)HTML生成庫(kù)。要在項(xiàng)目中使用,只需將其作為依賴(lài)項(xiàng)加入到build.sc文件即可:
??def?ivyDeps?=?Agg(
+????ivy"com.lihaoyi::scalatags:0.7.0",???
?????ivy"com.lihaoyi::cask:0.3.0"
???)
如果使用,那么還需要重新運(yùn)行./mill mill../idea命令,來(lái)發(fā)現(xiàn)依賴(lài)項(xiàng)的變動(dòng),然后重新運(yùn)行./mill -w app.讓W(xué)eb服務(wù)器重新監(jiān)聽(tīng)改動(dòng)。
然后,我們可以在.scala中導(dǎo)入:
package?app
+import?scalatags.Text.all._
?object?MinimalApplication?extends?cask.MainRoutes{
然后用一段最簡(jiǎn)單的 HTML模板替換"Hello, World!"。
?def?hello()?=?{
-????"Hello?World!"
+????html(
+??????head(),
+??????body(
+????????h1("Hello!"),
+????????p("World")
+??????)
+????).render
???}
我們應(yīng)該可以看到./mill -w app.命令重新編譯了代碼并重啟了服務(wù)器。然后刷新網(wǎng)頁(yè)額,就會(huì)看到純文本已經(jīng)被替換成HTML頁(yè)面了。
為了讓頁(yè)面更好看一些,我們使用這個(gè)CSS框架。只需按照它的指南,使用link標(biāo)簽引入:
?????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")
+????????)
???????)
現(xiàn)在字體不太一樣了:
雖然還不是最漂亮的網(wǎng)站,但現(xiàn)在已經(jīng)足夠了。
在本節(jié)的末尾,我們修改一下的HTML模板,加上硬編碼的聊天文本和假的輸入框,讓它看起來(lái)更像一個(gè)聊天應(yīng)用程序。
?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%")
+??????????)
?????????)
???????)
現(xiàn)在我們有了一個(gè)簡(jiǎn)單的靜態(tài)網(wǎng)站,其利用Cask web框架和 HTML庫(kù)提供HTML網(wǎng)頁(yè)服務(wù)。現(xiàn)在的服務(wù)器代碼如下所示:
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()
}
接下來(lái),我們來(lái)看看怎樣讓它支持交互!
表單和數(shù)據(jù)
為網(wǎng)站添加交互的第一次嘗試是使用HTML表單。首先我們要?jiǎng)h掉硬編碼的消息列表,轉(zhuǎn)而根據(jù)數(shù)據(jù)來(lái)輸出HTML網(wǎng)頁(yè):
?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)
???????????),
這里我們簡(jiǎn)單地使用了內(nèi)存上的存儲(chǔ)。關(guān)于如何將消息持久存儲(chǔ)到數(shù)據(jù)庫(kù)中,我將在以后的文章中介紹。
接下來(lái),我們需要讓頁(yè)面底部的兩個(gè)input支持交互。為實(shí)現(xiàn)這一點(diǎn),我們需要將它們包裹在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%")
??????????)
這樣我們就有了一個(gè)可以交互的表單,外觀跟之前的差不多。但是,提交表單會(huì)導(dǎo)致Error 404: Not Found錯(cuò)誤。這是因?yàn)槲覀冞€沒(méi)有將表單與服務(wù)器連接起來(lái),來(lái)處理表單提交并獲取新的聊天信息。我們可以這樣做:
???-??)
+
+??@cask.postForm("/")
+??def?postHello(name:?String,?msg:?String)?=?{
+????messages?=?messages?:+?(name?->?msg)
+????hello()
+??}
+
???@cask.get("/")
@cast.定義為根URL(即 / )添加了另一個(gè)處理函數(shù),但該處理函數(shù)處理POST請(qǐng)求,而不處理GET請(qǐng)求。Cask文檔()中還有關(guān)于@cask.*注釋的其他例子,你可以利用它們來(lái)定義處理函數(shù)。
驗(yàn)證
現(xiàn)在,用戶能夠以任何名字提交任何評(píng)論。但是,并非所有的評(píng)論和名字都是有效的:最低限度,我們希望保證評(píng)論和名字字段非空,同時(shí)我們還需要限制最大長(zhǎng)度。
實(shí)現(xiàn)這一點(diǎn)很簡(jiǎn)單:
??@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,但出現(xiàn)了另一個(gè)問(wèn)題:用戶輸入了非法的名字或信息并提交,那么這些信息就會(huì)消失,而且不會(huì)為錯(cuò)誤產(chǎn)生任何反饋。解決方法是,給hello()頁(yè)面渲染一個(gè)可選的錯(cuò)誤信息,用它來(lái)告訴用戶出現(xiàn)了什么問(wèn)題:
?@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")(
現(xiàn)在,當(dāng)名字或信息非法時(shí),就可以正確地顯示出錯(cuò)誤信息了。
下一次提交時(shí)錯(cuò)誤信息就會(huì)消失。
記住名字和消息
現(xiàn)在比較煩人的是,每次向聊天室中輸入消息時(shí)html提交表單到服務(wù)器,都要重新輸入用戶名。此外,如果用戶名或信息非法,那消息就會(huì)被清除,只能重新輸入并提交。可以讓hello頁(yè)面處理函數(shù)來(lái)填充這些字段,這樣就可以解決:
?@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查詢(xún)參數(shù),如果它們存在,則將其作為HTML input標(biāo)簽的value的默認(rèn)值。
接下來(lái)在的處理函數(shù)中渲染頁(yè)面時(shí),填充和msg,再發(fā)送給用戶:
??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,但只有錯(cuò)誤的情況才保留msg。這樣做是正確的,因?yàn)槲覀冎幌M脩粼诔鲥e(cuò)時(shí)才進(jìn)行編輯并重新提交。
完整的代碼.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實(shí)現(xiàn)動(dòng)態(tài)頁(yè)面更新
現(xiàn)在有了一個(gè)簡(jiǎn)單的、基于表單的聊天網(wǎng)站,用戶可以發(fā)表消息,其他用戶加載頁(yè)面即可看到已發(fā)表的消息。下一步就是讓網(wǎng)站變成動(dòng)態(tài)的,這樣用戶不需要刷新頁(yè)面就能發(fā)表消息了。
為實(shí)現(xiàn)這一點(diǎn),我們需要做兩件事情: