Kotlin Web Hello World

多平台开发
Kotlin Multiplatform 用于其他平台
使用 Kotlin Multiplatform 构建全栈 Web 应用程序

使用 Kotlin Multiplatform 构建全栈 Web 应用程序

本教程演示了如何使用 IntelliJ IDEA 构建连接的全栈应用程序。您将创建一个简单的 JSON API,并学习如何在使用 Kotlin 和 React 的 Web 应用程序中使用该 API。

该应用程序由使用 Kotlin/JVM 的服务器部分和使用 Kotlin/JS 的 Web 客户端组成。这两部分都将是一个 Kotlin 多平台项目。由于整个应用程序都在 Kotlin 中,因此您可以在前端和后端共享库和编程范式(例如使用 Coroutines 进行并发)。

在整个堆栈中使用 Kotlin 还可以编写可从应用程序的 JVM 和 JS 目标使用的类和函数。在本教程中,您将主要利用此功能在客户端和服务器之间共享数据的类型安全表示。

您还将使用流行的 Kotlin 多平台库和框架:

类型安全对象的序列化和反序列化被委托给kotlinx.serialization多平台库。这有助于您使数据通信安全且易于实施。

输出将是一个简单的购物清单应用程序,可让您计划您的杂货购物。

  • 用户界面将很简单:计划购买的列表和输入新购物项目的字段。
  • 如果用户点击购物清单中的某个项目,它将被删除。
  • 用户还可以通过添加感叹号来指定列表条目的优先级!。此信息将有助于订购购物清单。

对于本教程,您需要了解 Kotlin。一些关于 React 和 Kotlin 协程中基本概念的知识可能有助于理解一些示例代码,但这并不是严格要求的。

创建项目

从 GitHub克隆项目存储库并在 IntelliJ IDEA 中打开它。此模板已包含所有项目部分的所有配置和所需依赖项:JVM、JS 和公共代码。

在本教程中,您无需更改 Gradle 配置。如果您想直接开始编程,请直接进入下一节。

或者,您可以了解build.gradle.kts文件中的配置和项目设置,为其他项目做准备。查看下面有关 Gradle 结构的部分。

插件

与所有针对多个平台的 Kotlin 项目一样,您的项目使用 Kotlin Multiplatform Gradle 插件。它为应用程序目标(在本例中为 Kotlin/JVM 和 Kotlin/JS)提供了单点配置,并为它们公开了几个生命周期任务。

此外,您还需要两个插件:

  • application插件运行使用 JVM 的应用程序的服务器部分。
  • serialization插件提供 Kotlin 对象及其 JSON 文本表示之间的多平台转换。
plugins {
    kotlin("multiplatform") version "1.7.20"
    application // to run the JVM part
    kotlin("plugin.serialization") version "1.7.20"
}

目标

块内的目标配置kotlin负责设置您希望项目支持的平台。配置两个目标:(jvm服务器)和js(客户端)。在这里,您将对目标配置进行进一步调整。

jvm {
    withJava()
}
js {
    browser {
        binaries.executable()
    }
}

源集

Kotlin 源集是 Kotlin 源及其资源、依赖项和属于一个或多个目标的语言设置的集合。您可以使用它们来设置特定于平台的通用依赖块。

sourceSets {
    val commonMain by getting {
        dependencies {
            // ...
        }
    }
    val jvmMain by getting {
        dependencies {
            // ...
        }
    }
    val jsMain by getting {
        dependencies {
            // ...
        }
    }
}

每个源集还对应于目录中的一个文件夹src。在您的项目中,有、 和三个文件夹commonMain,其中包含自己的和文件夹。jsMainjvmMainresourceskotlin

构建后端

让我们从编写应用程序的服务器端开始。典型的 API 服务器实现了CRUD 操作——创建、读取、更新和删除。对于简单的购物清单,您可以只关注:

  • 在列表中创建新条目
  • 使用 API 读取条目
  • 删除条目

要创建后端,您可以使用 Ktor 框架,该框架旨在在连接的系统中构建异步服务器和客户端。它可以快速设置并随着系统变得更加复杂而增长。

您可以在其文档中找到有关 Ktor 的更多信息。

运行嵌入式服务器

使用 Ktor 实例化服务器。您需要告诉Ktor 附带的嵌入式服务器Netty在端口上使用引擎,在这种情况下,9090.

1.要定义应用程序的入口点,请将以下代码添加到src/jvmMain/kotlin/Server.kt

import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.application.*
import io.ktor.server.plugins.compression.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun main() {
    embeddedServer(Netty, 9090) {
        routing {
            get("/hello") {
                call.respondText("Hello, API!")
            }
        }
    }.start(wait = true)
}

第一个 API 端点是一个 HTTP 方法,get以及它应该可以访问的路由,/hello

本教程其余部分所需的所有导入都已添加。

2.要启动应用程序并查看一切正常,请执行 Gradlerun任务。您可以./gradlew run在终端中使用命令或从 Gradle 工具窗口运行:

3.应用程序完成编译并启动服务器后,使用 Web 浏览器导航以http://localhost:9090/hello查看第一个正在运行的路由:

稍后,就像 GET 请求的端点一样/hello,您将能够为routing块内的 API 配置所有端点。

安装 Ktor 插件

在继续应用程序开发之前,为嵌入式服务器安装所需的插件。Ktor 使用插件来支持应用程序中的更多功能,例如编码、压缩、日志记录和身份验证。

将以下行添加到embeddedServer块的顶部src/jvmMain/kotlin/Server.kt

install(ContentNegotiation) {
    json()
}
install(CORS) {
    allowMethod(HttpMethod.Get)
    allowMethod(HttpMethod.Post)
    allowMethod(HttpMethod.Delete)
    anyHost()
}
install(Compression) {
    gzip()
}
routing {
    // ...
}

每次调用都会向installKtor 应用程序添加一项功能:

  • ContentNegotiation提供基于请求Content-TypeAccept标头的请求的自动内容转换。与json()设置一起,这将启用 JSON 的自动序列化和反序列化,允许您将此任务委托给框架。
  • CORS配置跨域资源共享。需要 CORS 从任意 JavaScript 客户端进行调用,并有助于防止以后出现问题。
  • Compression在适用的情况下,通过 gzip 压缩传出内容,大大减少了发送到客户端的数据量。

使用 Ktor 所需的工件是文件中jvmMaindependencies块的一部分。build.gradle.kts这包括服务器、日志记录和支持库,用于通过kotlinx.serialization.

val jvmMain by getting {
    dependencies {
        implementation("io.ktor:ktor-serialization:$ktorVersion")
        implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion")
        implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
        implementation("io.ktor:ktor-server-cors:$ktorVersion")
        implementation("io.ktor:ktor-server-compression:$ktorVersion")
        implementation("io.ktor:ktor-server-core-jvm:$ktorVersion")
        implementation("io.ktor:ktor-server-netty:$ktorVersion")
        implementation("ch.qos.logback:logback-classic:$logbackVersion")
        implementation("org.litote.kmongo:kmongo-coroutine-serialization:$kmongoVersion")
    }
}

kotlinx.serialization它与 Ktor 的集成还需要存在一些常见的工件,您可以在commonMain源集中找到它们:

val commonMain by getting {
    dependencies {
        implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationVersion")
        implementation("io.ktor:ktor-client-core:$ktorVersion")
    }
}

创建数据模型

感谢 Kotlin Multiplatform,您可以一次将数据模型定义为通用抽象,然后从后端和前端引用它。

的数据模型ShoppingListItem应该有:

  • 项目的文字描述
  • 项目的数字优先级
  • 标识符

src/commonMain/中,创建一个kotlin/ShoppingListItem.kt包含以下内容的文件:

import kotlinx.serialization.Serializable

@Serializable
data class ShoppingListItem(val desc: String, val priority: Int) {
    val id: Int = desc.hashCode()

    companion object {
        const val path = "/shoppingList"
    }
}
  • @Serializable注释来自多平台库kotlinx.serialization,它允许您直接在通用代码中定义模型。
  • 一旦你ShoppingListItem从 JVM 和 JS 平台使用这个可序列化的类,就会为每个平台生成代码。此代码负责序列化和反序列化。
  • 存储有关模型的companion object附加信息 – 在这种情况path下,您将能够在 API 中访问它。通过引用此变量而不是将路由和请求定义为字符串,您可以将其更改path为模型操作。只需在此处对端点名称进行任何更改 – 客户端和服务器会自动调整。

此示例根据其描述计算一个简单idhashCode()。在这种情况下,这就足够了,但是在处理真实数据时,最好包含经过试验和测试的机制来为您的对象生成标识符——从UUID到您选择的数据库支持的自动递增 ID。

将项目添加到商店

您现在可以使用该ShoppingListItem模型来实例化一些示例项目并跟踪通过 API 进行的任何添加或删除。

因为目前没有数据库,所以创建一个MutableList来临时存储ShoppingListItems。为此,将以下文件级声明添加到src/jvmMain/kotlin/Server.kt

val shoppingList = mutableListOf(
    ShoppingListItem("Cucumbers ?", 1),
    ShoppingListItem("Tomatoes ?", 2),
    ShoppingListItem("Orange Juice ?", 3)
)

这些common类在 Kotlin 中被称为任何其他类——它们在所有目标之间共享。

为 JSON API 创建路由

添加支持创建、检索和删除ShoppingListItems 的路由。

1.在内部src/jvmMain/kotlin/Server.kt,将您的routing块更改为如下所示:

routing {
    route(ShoppingListItem.path) {
        get {
            call.respond(shoppingList)
        }
        post {
            shoppingList += call.receive<ShoppingListItem>()
            call.respond(HttpStatusCode.OK)
        }
        delete("/{id}") {
            val id = call.parameters["id"]?.toInt() ?: error("Invalid delete request")
            shoppingList.removeIf { it.id == id }
            call.respond(HttpStatusCode.OK)
        }
    }
}

路由根据公共路径进行分组。您不必将route路径指定为String. 而是path使用ShoppingListItem模型中的。代码的行为如下:

对模型路径 ( )的get请求以/shoppingList整个购物清单进行响应。

post对模型路径 ( )的请求/shoppingList会在购物清单中添加一个条目。

对模型路径的delete请求和提供的idshoppingList/47) 从购物清单中删除一个条目。您可以直接从请求中接收对象,并直接使用对象(甚至对象列表)响应请求。因为您之前设置了ContentNegotiation支持json(),所以标记为的对象@Serializable在发送(在 GET 请求的情况下)或接收(在 POST 请求的情况下)之前会自动转换为 JSON。

检查以确保一切都按计划进行。重新启动应用程序,前往http://localhost:9090/shoppingList并验证数据是否正确提供。您应该看到 JSON 格式的示例项目:

要测试postdelete请求,请使用支持.http文件的 HTTP 客户端。如果您使用的是 IntelliJ IDEA Ultimate Edition,则可以直接从 IDE 执行此操作。

1.在项目根目录中,创建一个名为AddShoppingListElement.http并添加 HTTP POST 请求的声明的文件,如下所示:

POST http://localhost:9090/shoppingList
Content-Type: application/json

{
  "desc": "Peppers ?",
  "priority": 5
}

2.在服务器运行的情况下,使用装订线中的运行按钮执行请求。如果一切顺利,“运行”工具窗口应该会显示HTTP/1.1 200 OK,您可以http://localhost:9090/shoppingList再次访问以验证条目是否已正确添加:

3.对名为 的文件重复此过程,该文件DeleteShoppingListElement.http包含以下内容:
DELETE http://localhost:9090/shoppingList/AN_ID_GOES_HERE
要尝试此请求,请替换AN_ID_GOES_HERE为现有 ID。

现在您有了一个可以支持功能性购物清单的所有必要操作的后端。继续为应用程序构建一个 JavaScript 前端,这将允许用户轻松地检查、添加和检查购物清单中的元素。

设置前端

要使您的服务器版本可用,请构建一个小型 Kotlin/JS Web 应用程序,该应用程序可以查询服务器的 API,以列表的形式显示它们,并允许用户添加和删除元素。

服务前端

除非另有明确配置,否则 Kotlin 多平台项目仅意味着您可以为每个平台构建应用程序,在本例中为 JVM 和 JavaScript。但是,要使应用程序正常运行,您需要同时编译后端和前端。事实上,您希望后端也提供属于前端的所有资产——一个 HTML 页面和相应的.js文件。

在模板项目中,已经对 Gradle 文件进行了调整。每当您使用runGradle 任务运行服务器时,前端也会构建并包含在生成的工件中。要了解有关其工作原理的更多信息,请参阅相关 Gradle 配置部分。

该模板已经在文件夹中附带了一个样板index.html文件src/commonMain/resources。它有一个root用于渲染组件的节点和一个script包含应用程序的标签:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Full Stack Shopping List</title>
    </head>
    <body>
        <div id="root"></div>
        <script src="shoppinglist.js"></script>
    </body>
</html>

该文件被放置在common资源中而不是jvm源集中,以使在浏览器(jsBrowserDevelopmentRunjsBrowserProductionRun)中运行 JS 应用程序的任务也可以访问该文件。如果您只需要运行没有后端的浏览器应用程序,这将很有帮助。

虽然您不需要确保文件在服务器上正确可用,但您仍然需要指示 Ktor 在请求时向浏览器提供.html.js文件。

前端的相关 Gradle 配置

应用程序的 Gradle 配置包含一个片段,它使服务器端 JVM 应用程序的执行和打包依赖于前端应用程序的构建,同时尊重与环境变量相关的development设置production。它确保无论何时jar从应用程序构建文件,它都包含 Kotlin/JS 代码:

// include JS artifacts in any generated JAR
tasks.getByName<Jar>("jvmJar") {
    val taskName = if (project.hasProperty("isProduction")
        || project.gradle.startParameter.taskNames.contains("installDist")
    ) {
        "jsBrowserProductionWebpack"
    } else {
        "jsBrowserDevelopmentWebpack"
    }
    val webpackTask = tasks.getByName<KotlinWebpack>(taskName)
    dependsOn(webpackTask) // make sure JS gets compiled first
    from(File(webpackTask.destinationDirectory, webpackTask.outputFileName)) // bring output file along into the JAR
}

jvmJar此处修改的任务由application负责该run任务的distributions插件和负责该任务的插件等调用installDist。这意味着当您run的应用程序以及准备将其部署到另一个目标系统或云平台时,组合构建将起作用。

为确保run任务正确识别 JS 工件,类路径调整如下:

tasks.getByName<JavaExec>("run") {
    classpath(tasks.getByName<Jar>("jvmJar")) // so that the JS artifacts generated by `jvmJar` can be found and served
}

从 Ktor 提供 HTML 和 JavaScript 文件

为简单起见,该index.html文件将在根路由上提供,/并在根目录中公开 JavaScript 工件。

1.在src/jvmMain/kotlin/Server.kt中,将相应的路由添加到routing块中:

get("/") {
    call.respondText(
        this::class.java.classLoader.getResource("index.html")!!.readText(),
        ContentType.Text.Html
    )
}
static("/") {
    resources("")
}
route(ShoppingListItem.path) {
    // ...
}

2.要确认一切都按计划进行,请使用 Gradlerun任务再次运行应用程序。

3.导航到http://localhost:9090/。您应该会看到一个页面显示“Hello,Kotlin/JS”:

编辑配置

在您进行开发时,构建系统会生成开发工件。这意味着在将 Kotlin 代码转换为 JavaScript 时不会应用任何优化。这使得编译时间更快,但也导致更大的 JS 文件。当您将应用程序部署到 Web 时,这是您想要避免的。

要指示 Gradle 生成优化的生产资产,请设置必要的环境变量。如果您在部署系统上运行应用程序,则可以将其配置为在构建期间设置此环境变量。如果您想在本地尝试生产模式,可以在终端中进行,或者将变量添加到运行配置中:

1.在 IntelliJ IDEA 中,选择Edit Configurations操作:

2.在Run/Debug Configurations菜单中,设置环境变量:ORG_GRADLE_PROJECT_isProduction=true

使用此运行配置的后续构建将对应用程序的前端部分执行所有可用的优化,包括消除死代码。它们仍然会比开发版本慢,因此在开发时再次删除此标志会很好。

构建前端

要呈现和管理用户界面元素,请使用流行的框架React以及Kotlin的可用wrappers。使用 React 设置一个完整的项目将允许您重用它及其配置作为更复杂的多平台应用程序的起点。

如需更深入地了解典型工作流程以及如何使用 React 和 Kotlin/JS 开发应用程序,请参阅使用 React 和 Kotlin/JS构建 Web 应用程序教程。

编写 API 客户端

要显示数据,您需要从服务器获取数据。为此,构建一个小型 API 客户端。

此 API 客户端将使用该ktor-clients库向 HTTP 端点发送请求。Ktor 客户端使用 Kotlin 的协程来提供非阻塞网络并支持 Ktor 服务器等插件。

在此配置中,JsonFeature用于kotlinx.serialization提供一种创建类型安全 HTTP 请求的方法。它负责在 Kotlin 对象及其 JSON 表示之间进行自动转换,反之亦然。

通过利用这些属性,您可以将 API 包装器创建为一组接受或返回的挂起函数ShoppingItems。创建一个名为Api.kt并在其中实现它们的文件src/jsMain/kotlin

import io.ktor.http.*
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*

import kotlinx.browser.window

val endpoint = window.location.origin // only needed until https://youtrack.jetbrains.com/issue/KTOR-453 is resolved

val jsonClient = HttpClient {
    install(ContentNegotiation) {
        json()
    }
}

suspend fun getShoppingList(): List<ShoppingListItem> {
    return jsonClient.get(endpoint + ShoppingListItem.path).body()
}

suspend fun addShoppingListItem(shoppingListItem: ShoppingListItem) {
    jsonClient.post(endpoint + ShoppingListItem.path) {
        contentType(ContentType.Application.Json)
        setBody(shoppingListItem)
    }
}

suspend fun deleteShoppingListItem(shoppingListItem: ShoppingListItem) {
    jsonClient.delete(endpoint + ShoppingListItem.path + "/${shoppingListItem.id}")
}

构建用户界面

您已经为客户端打下了基础,并拥有一个干净的 API 来访问服务器提供的数据。现在您可以在 React 应用程序的屏幕上显示购物清单。

为应用程序配置入口点

不要渲染一个简单的“Hello,Kotlin/JS”字符串,而是让应用程序渲染一个函数式App组件。为此,将里面的内容替换为src/jsMain/kotlin/Main.kt以下内容:

import kotlinx.browser.document
import react.create
import react.dom.client.createRoot

fun main() {
    val container = document.getElementById("root") ?: error("Couldn't find container!")
    createRoot(container).render(App.create())
}

构建和渲染购物清单

接下来,实现App组件。对于购物清单应用,它需要:

  • 保留购物清单的“本地状态”以了解要显示哪些元素。
  • 从服务器加载购物清单元素并相应地设置状态。
  • 向 React 提供有关如何呈现列表的说明。

根据这些要求,您可以App按如下方式实现该组件:

1.创建并填写src/jsMain/kotlin/App.kt文件:

import react.*
import kotlinx.coroutines.*
import react.dom.html.ReactHTML.h1
import react.dom.html.ReactHTML.li
import react.dom.html.ReactHTML.ul

private val scope = MainScope()

val App = FC<Props> {
var shoppingList by useState(emptyList<ShoppingListItem>())

    useEffectOnce {
        scope.launch {
            shoppingList = getShoppingList()
        }
    }

    h1 {
        +"Full-Stack Shopping List"
    }
    ul {
        shoppingList.sortedByDescending(ShoppingListItem::priority).forEach { item ->
            li {
                key = item.toString()
                +"[${item.priority}] ${item.desc} "
            }
        }
    }
}

在这里,Kotlin DSL 用于定义应用程序的 HTML 表示。

launch用于ShoppingListItem在组件首次初始化时从 API 中获取 s 的列表。

React 挂钩useEffectOnceuseState帮助您简洁地使用 React 的功能。有关 React 钩子如何工作的更多信息,请查看官方 React 文档。要了解有关使用 Kotlin/JS 进行 React 的更多信息,请参阅使用 React 和 Kotlin/JS 构建 Web 应用程序教程。

2.使用 Gradlerun任务启动应用程序。

3.导航以http://localhost:9090/查看列表:

添加输入字段组件

接下来,允许用户使用文本输入字段向购物清单添加新条目。当用户将他们的条目提交到购物清单以接收输入时,您将需要一个输入组件来提供回调。

1.创建src/jsMain/kotlin/InputComponent.kt文件并用以下定义填充它:

import org.w3c.dom.HTMLFormElement
import react.*
import org.w3c.dom.HTMLInputElement
import react.dom.events.ChangeEventHandler
import react.dom.events.FormEventHandler
import react.dom.html.InputType
import react.dom.html.ReactHTML.form
import react.dom.html.ReactHTML.input

external interface InputProps : Props {
    var onSubmit: (String) -> Unit
}

val inputComponent = FC<InputProps> { props ->
val (text, setText) = useState("")

    val submitHandler: FormEventHandler<HTMLFormElement> = {
        it.preventDefault()
        setText("")
        props.onSubmit(text)
    }

    val changeHandler: ChangeEventHandler<HTMLInputElement> = {
        setText(it.target.value)
    }

    form {
        onSubmit = submitHandler
        input {
            type = InputType.text
            onChange = changeHandler
            value = text
        }
    }
}

跟踪其内部状态(用户到目前为止键入的内容)并公开一个处理onSubmit程序,当用户提交表单时(通常通过Enter按键)调用该处理程序。

2.要inputComponent在应用程序中使用它,请将以下代码段添加到块src/jsMain/kotlin/App.kt的底部FC(在ul元素的右大括号之后):

inputComponent {
    onSubmit = { input ->
        val cartItem = ShoppingListItem(input.replace("!", ""), input.count { it == '!' })
        scope.launch {
            addShoppingListItem(cartItem)
            shoppingList = getShoppingList()
        }
    }
}

当用户提交文本时,ShoppingListItem会创建一个新文本。其优先级设置为输入中感叹号的个数,其描述为去掉所有感叹号的输入。这变成Peaches!! ?ShoppingListItem(desc="Peaches ?", priority=2).

生成ShoppingListItem的内容与您之前构建的客户端一起发送到服务器。

ShoppingListItem然后,通过从服务器获取新的 s 列表、更新应用程序状态并让 React 重新渲染内容来更新 UI 。

Implement item移除

添加从列表中删除已完成项目的功能,这样它就不会太长。您可以修改现有列表,而不是添加另一个 UI 元素(如“删除”按钮)。当用户单击列表中的一项时,应用程序将其删除。

为此,请将相应的处理程序传递给onClick列表元素:

1.在src/jsMain/kotlin/App.kt中,更新li块(ul块内):

li {
    key = item.toString()
    onClick = {
        scope.launch {
            deleteShoppingListItem(item)
            shoppingList = getShoppingList()
        }
    }
    +"[${item.priority}] ${item.desc} "
}

API 客户端与应删除的元素一起被调用。服务器更新购物清单,重新呈现用户界面。

2.使用 Gradlerun任务启动应用程序。

3.导航到http://localhost:9090/,然后尝试在列表中添加和删除元素:

包括一个数据库来存储数据

目前,应用程序不保存数据,这意味着当您终止服务器进程时,购物清单就会消失。为了解决这个问题,即使服务器关闭,也可以使用 MongoDB 数据库来存储和检索购物清单项目。

MongoDB 简单、快速设置,具有对 Kotlin 的库支持,并提供简单的NoSQL文档存储,这对于基本应用程序来说绰绰有余。您可以自由地为您的应用程序配备不同的数据存储机制。

要提供本节中使用的所有功能,您需要包含来自 Kotlin 和 JavaScript (npm) 生态系统的多个库。请参阅文件中包含完整设置的jsMain依赖项块。build.gradle.kts

设置 MongoDB

从官方 MongoDB 网站在本地机器上安装 MongoDB Community Edition 。或者,您可以使用podman 之类的容器化工具来运行 MongoDB 的容器化实例。

安装后,确保您在mongodb-community本教程的其余部分运行该服务。您将使用它来存储和检索列表条目。

在过程中包含 KMongo

KMongo是一个社区创建的 Kotlin 框架,可以轻松地从 Kotlin/JVM 代码使用 MongoDB。它也可以很好地与kotlinx.serialization用于促进客户端和服务器之间的通信的 .

通过使代码使用外部数据库,您不再需要shoppingListItems在服务器上保留一个集合。相反,设置一个数据库客户端并从中获取一个数据库和一个集合。

1.在内部src/jvmMain/kotlin/Server.kt,删除声明shoppingList并添加以下三个顶级变量:

val client = KMongo.createClient().coroutine
val database = client.getDatabase("shoppingList")
val collection = database.getCollection<ShoppingListItem>()

2.在src/jvmMain/kotlin/Server.kt中,将 GET、POST 和 DELETE 路由的定义替换为 aShoppingListItem以利用可用的收集操作:

get {
    call.respond(collection.find().toList())
}
post {
    collection.insertOne(call.receive<ShoppingListItem>())
    call.respond(HttpStatusCode.OK)
}
delete("/{id}") {
    val id = call.parameters["id"]?.toInt() ?: error("Invalid delete request")
    collection.deleteOne(ShoppingListItem::id eq id)
    call.respond(HttpStatusCode.OK)
}

在 DELETE 请求中,KMongo 的类型安全查询用于ShoppingListItem从数据库中获取并删除正确的。

3.使用该任务启动服务器run,然后导航到http://localhost:9090/。在第一次开始时,当查询空数据库时,您会看到一个空的购物清单。

4.将一些物品添加到您的购物清单中。服务器会将它们保存到数据库中。

5.要检查这一点,请重新启动服务器并重新加载页面。

检查 MongoDB

要查看数据库中实际保存了哪些信息,您可以使用外部工具检查数据库。

如果您有 IntelliJ IDEA Ultimate Edition 或 DataGrip,您可以使用这些工具检查数据库内容。或者,您可以使用mongosh命令行客户端。

1.要连接到本地 MongoDB 实例,在 IntelliJ IDEA Ultimate 或 DataGrip 中,转到Database选项卡并选择+ | 数据源MongoDB

2.如果这是您第一次以这种方式连接到 MongoDB 数据库,系统可能会提示您下载缺少的驱动程序:

3.当使用使用默认设置的本地 MongoDB 安装时,无需调整配置。您可以使用“测试连接”按钮测试连接,该按钮应输出 MongoDB 版本和一些附加信息。

4.单击确定。现在您可以使用“数据库”窗口导航到您的集合并查看其中存储的所有内容:

Kmongo 的相关 Gradle 配置

Kmongo 与项目的单个依赖项一起添加,该特定版本包括开箱即用的协程和序列化支持:

val jvmMain by getting {
    dependencies {
        // ...
        implementation("org.litote.kmongo:kmongo-coroutine-serialization:$kmongoVersion")
    }
}

部署到云端

无需在 上打开您的应用程序localhost,您可以通过将其部署到云中将其带到网络上。

要让应用程序在托管基础架构(例如云提供商)上运行,您需要将其与所选平台提供的环境变量集成,并向项目添加任何所需的配置。具体来说,传递应用程序端口和 MongoDB 连接字符串。

在应用程序部署期间,您可能需要更改防火墙规则以允许应用程序访问数据库。有关更多详细信息,请参阅MongoDB 文档

指定端口变量

在托管平台上,应用程序应运行的端口通常由外部确定并通过PORT环境变量公开。如果存在,您可以通过配置embeddedServerin遵守此设置src/jvmMain/kotlin/Server.kt

fun main() {
    val port = System.getenv("PORT")?.toInt() ?: 9090
    embeddedServer(Netty, port) {
        // ...
    }
}

Ktor 还支持可以尊重环境变量的配置文件。要了解有关如何使用它们的更多信息,请查看官方文档

指定 MONGODB_URI 变量

托管平台通常通过环境变量公开连接字符串——对于 MongoDB,这可能是MONGODB_URI字符串,客户端需要使用它来连接数据库。根据您尝试连接的特定 MongoDB 实例,您可能需要将retryWrites=false参数附加到连接字符串。

要正确满足这些要求,请在 中实例化clientdatabase变量src/jvmMain/kotlin/Server.kt

val connectionString: ConnectionString? = System.getenv("MONGODB_URI")?.let {
    ConnectionString("$it?retryWrites=false")
}

val client =
    if (connectionString != null) KMongo.createClient(connectionString).coroutine else KMongo.createClient().coroutine
val database = client.getDatabase(connectionString?.database ?: "shoppingList")

这可确保在client设置环境变量时基于此信息创建。否则(例如, on localhost),数据库连接将像以前一样实例化。

创建过程文件

Heroku 等托管云平台或Dokku等 PaaS 实现也处理应用程序的生命周期。为此,它们需要一个“入口点”定义。Procfile这两个平台使用您在项目根目录中的一个名为的文件。它指向任务生成的输出stage(已包含在 Gradle 模板中):

web: ./build/install/shoppingList/bin/shoppingList

开启生产模式

要启用对 JavaScript 资产进行优化的编译,请将另一个标志传递给构建过程。在Run/Debug Configurations菜单中,将环境变量设置ORG_GRADLE_PROJECT_isProductiontrue. 您可以在将应用程序部署到目标环境时设置此环境变量。

您可以在 GitHub 上的final分支上找到完成的应用程序。

相关 Gradle 配置

stage任务是 的别名installDist

// Alias "installDist" as "stage" (for cloud providers)
tasks.create("stage") {
    dependsOn(tasks.getByName("installDist"))
}

// only necessary until https://youtrack.jetbrains.com/issue/KT-37964 is resolved
distributions {
    main {
        contents {
            from("$buildDir/libs") {
                rename("${rootProject.name}-jvm", rootProject.name)
                into("lib")
            }
        }
    }
}