很早就有人想讓我寫(xiě)一個(gè) kotlin x nodejs 的開(kāi)發(fā)教程,利用 kotlin 可以編譯為 js 的特性,理論上是可以很好的與 nodejs 配合的,而事實(shí)上也是如此。本篇即是講述如何在 kotlin 的體系下玩轉(zhuǎn) nodejs,并最終實(shí)現(xiàn)一個(gè)簡(jiǎn)單的服務(wù)端應(yīng)用。
首先我們要準(zhǔn)備好 nodejs 的環(huán)境,采用以下命令來(lái)安裝,如果已經(jīng)裝好了,可以跳過(guò)這一步:
for Mac
$ brew install nodejs
for Ubuntu
$ apt install nodejs
接著建立一個(gè) Kotlin(Javascript) 項(xiàng)目,需要注意的是,如果選擇使用本地 gradle 來(lái)編譯,那么 gradle 的版本要大于 4.4,我自己的環(huán)境采用的版本是 4.6。
然后對(duì) build.gradle 文件進(jìn)行一定的修改,加入 compileKotlin2Js 的配置項(xiàng):
compileKotlin2Js {
kotlinOptions.outputFile = "${projectDir}/web/ktnode.js"
kotlinOptions.moduleKind = "commonjs"
kotlinOptions.sourceMap = true
}
需要特別注意的是,對(duì)于純前端的 KotlinJs 項(xiàng)目,moduleKind 應(yīng)當(dāng)被配置為 umd,而對(duì)于后端(可以帶前端)的項(xiàng)目,應(yīng)當(dāng)配置為 commonjs,我們此處開(kāi)發(fā)的是后端項(xiàng)目。
另外,還需要再對(duì) gradle 進(jìn)行一些改造,以正常的 web 項(xiàng)目部署的目錄結(jié)構(gòu)來(lái)輸出編譯結(jié)果,這樣我們可以直接用熱更新的方式來(lái)部署項(xiàng)目,方便調(diào)試。
build.doLast() {
configurations.compile.each { File file ->
copy {
includeEmptyDirs = false
from zipTree(file.absolutePath)
into "${projectDir}/web"
include { fileTreeElement ->
def path = fileTreeElement.path
path.endsWith(".js") && (path.startsWith("META-INF/resources/") || !path.startsWith("META-INF/"))
}
}
}
copy {
includeEmptyDirs = false
from new File("src/main/resources")
into "web"
}
}
準(zhǔn)備好之后,我們可以把這個(gè)項(xiàng)目轉(zhuǎn)為使用 npm 管理,也就是可以直接拿來(lái)運(yùn)行了,在項(xiàng)目的根目錄下執(zhí)行命令:
$ npm init
這個(gè)命令將生成 package.json 文件,同樣的,我們需要修改這個(gè)文件,以完成運(yùn)行和依賴的配置:
{
"name": "ktnode",
"version": "1.0.0",
"description": "",
"scripts": {
"start": "node ./web/ktnode.js"
},
"author": "rarnu",
"license": "GPLv3",
"dependencies": {
"kotlin": "^1.3.20",
"express": "^4.15.4",
"mongoose": "^4.11.7"
}
}
這里需要注意的是,在 scripts 內(nèi)的 start 選項(xiàng),將指出從何開(kāi)始運(yùn)行 node 項(xiàng)目,此處指向我們的編譯目標(biāo),即 ktnode.js,你也可以把這個(gè)名稱修改為你自己項(xiàng)目的。
接著是依賴,要使用 Kotlin 來(lái)開(kāi)發(fā),那么也就必須在 nodejs 環(huán)境內(nèi)安裝 Kotlin 的包,除此之外,其他都是 nodejs 的常規(guī)操作了,運(yùn)行 install 命令來(lái)完成依賴的安裝:
$ npm install
現(xiàn)在可以嘗試寫(xiě)一個(gè)文件,來(lái)跑起第一個(gè) Kotlin x Nodejs 服務(wù)器了:
package com.rarnu.ktnode
import kotlin.js.json
external fun require(module: String): dynamic
external val process: dynamic
val express = require("express")
app = express()
fun main(args: Array<String>) {
app.get("/") { _, resp ->
resp.type("text/plain")
resp.send("Hello Kotlin x Nodejs")
}
app.listen(port) {
println("Listening on port $port")
}
}
然后編譯,運(yùn)行之:
$ gradle build
$ npm start
然后就可以在瀏覽器打開(kāi) http://localhost:8888 并看到這個(gè)頁(yè)面啦。

好了,有了這個(gè)基礎(chǔ),我們可以走得更遠(yuǎn),下面來(lái)說(shuō)一下如果要返回靜態(tài)頁(yè)面以及帶入靜態(tài)的 js 等文件要怎么操作。
假設(shè)我們已經(jīng)在 Kotlin 項(xiàng)目的 resources 目錄下放置了 index.html 和 jquery.js,html 的代碼如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript">
function callapi() {
$.ajax({
url :"/api",
dataType: "text",
data: { name: "rarnu" },
success: function (data) { $("#result")[0].innerHTML = data; }
});
}
</script>
</head>
<body>
<p><input type="button" onclick="callapi();" value="CallAPI"></p>
<div id="result"></div>
</body>
</html>
然后我們需要設(shè)置靜態(tài)頁(yè)面路徑,只有設(shè)置了靜態(tài)頁(yè)面路徑,這個(gè)頁(yè)面以及它加載的 js 才會(huì)正常展示:
var path = require("path")
app.use(express.static(path.join(__dirname, "")))
同樣的,你可以嘗試不設(shè)置靜態(tài)頁(yè)面路徑,然后看看會(huì)發(fā)生什么。
好了,暫時(shí)不去管那個(gè)內(nèi)置的 js 函數(shù),現(xiàn)在我們?cè)賮?lái)看看如何動(dòng)態(tài)的返回頁(yè)面,這種技巧通常用于代碼模板,從模板來(lái)生成一個(gè)頁(yè)面并返回給用戶。此時(shí)就需要從 resources 內(nèi)加載頁(yè)面的模板文件,然后將其返回給用戶:
app.get("/sample") { _, resp ->
val fs = require("fs")
fs.readFile("index.html", "utf8") { _, data ->
resp.type("text/html")
resp.send(data)
}
}
接著,要講一下如何來(lái)接受請(qǐng)求參數(shù),在 nodejs 里,有三種接參數(shù)的方式,如下表所示:
| 代碼 | 描述 | 示例 |
|---|---|---|
| req.params | 獲取路由參數(shù) | 從 /index 請(qǐng)求中獲取 index |
| req.query | 獲取查詢字符串中的參數(shù) | 從 /index?id=1 中獲取 id |
| req.body | 獲取經(jīng)過(guò) URLEncoded 的 body 參數(shù) | 從 /index 中獲取 post 上來(lái)的 id |
在 Kotlin 代碼中,可以輕易的完成對(duì)于參數(shù)的獲取:
app.get("/sample") { req, resp ->
val id = req.query.id
resp.type("text/plain")
resp.send(id)
}
要獲取 post 方法傳來(lái)的參數(shù),需要額外的進(jìn)行配置:
val bodyParser = require("body-parser")
app.use(bodyParser.urlencoded(extended = false))
app.post("/sample") {
val id = req.body.id
... ...
}
在 Kotlin 中,包含了許多方便開(kāi)發(fā)的庫(kù),如果要返回的內(nèi)容是一個(gè) json 串,可以用一些簡(jiǎn)單的方法來(lái)進(jìn)行拼裝:
import kotlin.js.json
... ...
app.get("/sample") { _, resp ->
resp.type("text/json")
resp.send(json("result" to 0, "message" to "success"))
}
這段代碼將返回以下 json 串:
{"result":0,"message":"success"}
還記得上面埋了個(gè)伏筆不,html 里的那個(gè) callapi 函數(shù),現(xiàn)在可以利用上面的內(nèi)容,把這個(gè)函數(shù)寫(xiě)出來(lái)了:
app.get("/api") { req, resp ->
val name = req.query?.name ?: ""
resp.type("text/json")
resp.send(json("result" to 0, "message" to "Hello World: $name"))
}
再回過(guò)頭來(lái)看一些能夠方便開(kāi)發(fā)的東西,比如說(shuō)熱部署,你一定會(huì)發(fā)現(xiàn),在使用 npm start 后,不論怎么編輯頁(yè)面或代碼,都不能立即在瀏覽器上看到效果。如果想立即看到效果要怎么辦呢?
$ sudo npm install -g supervisor
$ supervisor ktnode.js
這樣就可以搞定了,supervisor 會(huì)監(jiān)控 ktnode.js 所在的目錄,在目錄內(nèi)容有變化時(shí),可以即時(shí)刷新,這樣就可以瀏覽器內(nèi)實(shí)時(shí)預(yù)覽了。
最后,在這個(gè) Demo 程序的基礎(chǔ)上,其實(shí)還是可以做很多抽象的,比如一個(gè)好的基礎(chǔ)庫(kù)可以讓你事半功倍,比如這樣:
package com.rarnu.ktnode
import kotlin.js.json
fun main(args: Array<String>) {
initServer()
routing("/index") { req, resp ->
resp.type("text/html")
loadRes("index.html") { resp.send(it) }
}
routing("/api") { req, resp ->
val name = req.query?.name ?: ""
resp.type("text/json")
resp.send(json("result" to 0, "message" to "Hello World: $name"))
}
startListen()
}
寫(xiě)成這樣就會(huì)比較舒服,專心的處理請(qǐng)求響應(yīng)的邏輯而無(wú)需關(guān)心其他內(nèi)容,這需要在實(shí)際開(kāi)發(fā)中多加積累。
我在 Github 上放了一個(gè)簡(jiǎn)單的 Demo,希望能起到拋磚引玉的作用。點(diǎn)此去玩 Demo