Разработка полностекового веб-приложения с Kotlin Multiplatform
Этот учебник демонстрирует, как создать связанное полностековое приложение с помощью IntelliJ IDEA. Вы создадите простой JSON API и научитесь использовать API из веб-приложения с использованием Kotlin и React.
Приложение состоит из серверной части, использующей Kotlin/JVM, и веб-клиента, использующего Kotlin/JS. Обе части будут одним проектом Kotlin Multiplatform. Поскольку все приложение будет на Kotlin, вы можете использовать общие библиотеки и парадигмы программирования (например, использование Coroutines для конкурентности) как на фронте, так и на бэке.
Использование Kotlin на всех уровнях также позволяет писать классы и функции, которые можно использовать как с JVM, так и с JS-целями вашего приложения. В этом учебнике вы будете в основном использовать эту функциональность для обмена безопасным типом представления данных между клиентом и сервером.
Вы также будете использовать популярные библиотеки и фреймворки Kotlin multiplatform:
Фреймворк Ktor
Сериализация и десериализация в типы-объекты делегируются библиотеке kotlinx.serialization multiplatform. Это помогает вам сделать передачу данных безопасной и легкой в реализации.
Выходом будет простое приложение для составления списка покупок, которое позволит вам планировать покупки продуктов.
Пользовательский интерфейс будет простым: список запланированных покупок и поле для ввода новых пунктов списка покупок.
Если пользователь нажмет на элемент в списке покупок, он будет удален.
Пользователь также может указать уровень приоритета для записей в списке, добавив знак восклицания
!. Эта информация поможет упорядочить список покупок.
Создание проекта
Скопируйте репозиторий проекта из 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.9.0"
application // to run the JVM part
kotlin("plugin.serialization") version "1.9.0"
}
Цели
Конфигурация целей внутри блока kotlin отвечает за настройку платформ, которые вы хотите поддерживать в своем проекте. Настройте две цели: jvm (сервер) и js (клиент). Здесь вы внесёте дополнительные изменения в конфигурации целей.
jvm {
withJava()
}
js {
browser {
binaries.executable()
}
}
Для получения более подробной информации о целях, см. Структура проекта Multiplatform.
Наборы исходных файлов
Наборы исходных файлов Kotlin — это коллекция исходных файлов Kotlin, их ресурсов, зависимостей и параметров языка, относящихся к одной или нескольким целям. Вы используете их для настройки платформенно-специфических и общих блоков зависимостей.
sourceSets {
val commonMain by getting {
dependencies {
// ...
}
}
val jvmMain by getting {
dependencies {
// ...
}
}
val jsMain by getting {
dependencies {
// ...
}
}
}
Каждый набор исходных файлов также соответствует папке в каталоге src. В вашем проекте есть три папки, commonMain, jsMain, и jvmMain, которые содержат свои папки resources и kotlin.
Для получения подробной информации о наборах исходных файлов, см. Структура проекта Multiplatform.
Построение бэкенда
Начнём с написания серверной части приложения. Типичный сервер API реализует операции CRUD — создание, чтение, обновление и удаление. Для простого списка покупок вы можете сосредоточиться только на:
Создание новых записей в списке
Чтение записей с помощью API
Удаление записей
Для создания бэкенда вы можете использовать фреймворк Ktor, предназначенный для создания асинхронных серверов и клиентов в связанных системах. Его можно быстро настроить, и он будет расти по мере усложнения систем.
Дополнительную информацию о Ktor вы найдете в его документации.
Запуск встроенного сервера
Инициализируйте сервер с помощью Ktor. Вам необходимо указать встроенному серверу Ktor, который поставляется вместе с Ktor, использовать Netty движок на порту, в данном случае 9090.
-
Для определения точки входа в приложение добавьте следующий код в
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.http.content.* import io.ktor.server.routing.* fun main() { embeddedServer(Netty, 9090) { routing { get("/hello") { call.respondText("Hello, API!") } } }.start(wait = true) }Первый конечный пункт API — это HTTP-метод
get, и маршрут, по которому он должен быть доступен,/hello.Все необходимые для остальной части учебника импорты уже добавлены.
-
Для запуска приложения и проверки работоспособности выполните задачу Gradle
run. Вы можете использовать команду./gradlew runв терминале или запустить из окна инструмента Gradle:
-
После завершения компиляции приложения и запуска сервера перейдите в веб-браузере по адресу
http://localhost:9090/hello, чтобы увидеть первый маршрут в действии:
Позже, как и с конечным пунктом для запросов GET на /hello, вы сможете настроить все конечные пункты API внутри блока routing.
Установка плагинов 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 {
// ...
}
Каждый вызов install добавляет одну функцию в приложение Ktor:
ContentNegotiationобеспечивает автоматическое преобразование содержимого запросов на основе их заголовковContent-TypeиAccept. Вместе с настройкойjson(), это позволяет автоматически сериализовывать и десериализовывать в JSON, позволяя делегировать эту задачу фреймворку.CORSнастраивает Cross-Origin Resource Sharing. CORS необходим для выполнения вызовов из произвольных JavaScript-клиентов и помогает предотвратить проблемы позже.Compressionзначительно уменьшает количество данных, отправляемых клиенту, сжимая исходящее содержимое с помощью gzip, когда это возможно.
Соответствующая конфигурация Gradle для Ktor
Необходимые артефакты для использования Ktor являются частью блока jvmMain dependencies в файле 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для операций с моделями. Все изменения имени конечного пункта нужно выполнять только здесь — клиент и сервер будут автоматически скорректированы.
Добавление элементов в хранилище
Теперь вы можете использовать модель ShoppingListItem для создания некоторых образцовых элементов и отслеживания любых добавлений или удалений, произведенных через API.
Поскольку сейчас базы данных нет, создайте MutableList для временного хранения ShoppingListItem. Для этого добавьте следующее объявление уровня файла в src/jvmMain/kotlin/Server.kt:
val shoppingList = mutableListOf(
ShoppingListItem("Cucumbers 🥒", 1),
ShoppingListItem("Tomatoes 🍅", 2),
ShoppingListItem("Orange Juice 🍊", 3)
)
Классы common используются как и любой другой класс в Kotlin — они общие для всех целевых платформ.
Создание маршрутов для JSON API
Добавьте маршруты, которые поддерживают создание, получение и удаление ShoppingListItem.
-
Внутри
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к пути модели и предоставленномуid(shoppingList/47) удаляет запись из списка покупок.
-
Проверьте, чтобы убедиться, что все работает как запланировано. Перезапустите приложение, перейдите по адресу
http://localhost:9090/shoppingListи проверьте, что данные правильно передаются. Вы должны увидеть примеры элементов в формате JSON:
Для проверки запросов post и delete используйте HTTP-клиент, который поддерживает файлы .http. Если вы используете IntelliJ IDEA Ultimate Edition, вы можете сделать это прямо из IDE.
-
В корне проекта создайте файл под названием
AddShoppingListElement.httpи добавьте объявление HTTP-POST-запроса следующим образом:POST http://localhost:9090/shoppingList Content-Type: application/json { "desc": "Peppers 🌶", "priority": 5 } -
После запуска сервера выполните запрос, используя кнопку запуска в метке.
Если все прошло хорошо, окно "run" должно отобразить
HTTP/1.1 200 OK, и вы можете снова посетитьhttp://localhost:9090/shoppingList, чтобы проверить, что запись была добавлена правильно:
-
Повторите этот процесс для файла под названием
DeleteShoppingListElement.http, который содержит следующее:DELETE http://localhost:9090/shoppingList/AN_ID_GOES_HERE
Чтобы попробовать этот запрос, замените
AN_ID_GOES_HEREсуществующим идентификатором.
Теперь у вас есть бэкенд, который может поддерживать все необходимые операции для функционального списка покупок. Переходите к разработке JavaScript-фронта приложения, которое позволит пользователям легко просматривать, добавлять и отмечать элементы в своем списке покупок.
Настройка фронта
Для использования вашей версии сервера создайте небольшое веб-приложение Kotlin/JS, которое может запрашивать API сервера, отображать их в виде списка и позволять пользователю добавлять и удалять элементы.
Развертывание фронта
Если не указано иное, проект Kotlin Multiplatform означает, что вы можете собрать приложение для каждой платформы, в данном случае JVM и JavaScript. Однако для корректной работы приложения необходимо выполнить компиляцию как бэкенда, так и фронта. Фактически, вы хотите, чтобы бэкенд также предоставлял все ресурсы, относящиеся к фронту – HTML-страницу и соответствующий файл .js.
В шаблоне проекта уже внесены изменения в файл Gradle. При каждом запуске сервера с задачей Gradle run фронт также компилируется и включается в результирующие артефакты. Более подробную информацию о работе см. в разделе Соответствующая конфигурация 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 источников, чтобы задачи для запуска JS-приложения в браузере (jsBrowserDevelopmentRun и jsBrowserProductionRun) также были доступны файлу. Это полезно, если нужно запустить только веб-приложение без бэкенда.
Хотя вам не нужно гарантировать, что файл должным образом доступен на сервере, вам все равно нужно указать 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 правильно распознает JS-артефакты, путь класса корректируется следующим образом:
tasks.getByName<JavaExec>("run") {
classpath(tasks.getByName<Jar>("jvmJar")) // so that the JS artifacts generated by `jvmJar` can be found and served
}
Предоставление HTML и JavaScript-файлов из Ktor
Для простоты файл index.html будет предоставляться по корневому маршруту / и экспонировать JS-артефакт в корневой директории.
-
В
src/jvmMain/kotlin/Server.kt, добавьте соответствующие маршруты в блокrouting:get("/") { call.respondText( this::class.java.classLoader.getResource("index.html")!!.readText(), ContentType.Text.Html ) } staticResources("/", "static") route(ShoppingListItem.path) { // ... } Чтобы убедиться, что все прошло успешно, запустите приложение снова с задачей Gradle
run.-
Перейдите по адресу
http://localhost:9090/. Вы должны увидеть страницу с надписью "Привет, Kotlin/JS":
Редактирование конфигурации
Во время разработки система сборки генерирует разработочные артефакты. Это означает, что при преобразовании Kotlin-кода в JavaScript не применяются оптимизации. Это ускоряет время компиляции, но также приводит к увеличению размера JS-файлов. При развертывании приложения в веб это то, чего следует избегать.
Чтобы указать Gradle на генерацию оптимизированных производственных ресурсов, установите необходимую переменную среды. Если вы работаете с приложением на системе развертывания, вы можете настроить ее на установку этой переменной во время сборки. Если вы хотите попробовать режим производства локально, вы можете сделать это в терминале или добавив переменную в конфигурацию запуска:
-
В IntelliJ IDEA выберите действие Изменить конфигурации:

-
В меню Конфигурации запуска/отладки установите переменную среды:
ORG_GRADLE_PROJECT_isProduction=true

Последующие сборки с этой конфигурацией запуска будут выполнять все доступные оптимизации для фронта приложения, включая удаление неиспользуемого кода. Они все же будут медленнее, чем сборки разработки, поэтому будет полезно снова убрать этот флаг во время разработки.
Создание фронтенда
Для отрисовки и управления элементами пользовательского интерфейса используйте популярную фреймворк React вместе с доступными обёртками для Kotlin. Настройка полного проекта с React позволит вам повторно использовать его и его конфигурацию в качестве отправной точки для более сложных кроссплатформенных приложений.
Для более глубокого понимания типичных рабочих процессов и того, как приложения разрабатываются с React и Kotlin/JS, обратитесь к учебнику по созданию веб-приложения с React и Kotlin/JS.
Написание клиента 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.*
val jsonClient = HttpClient {
install(ContentNegotiation) {
json()
}
}
suspend fun getShoppingList(): List<ShoppingListItem> {
return jsonClient.get(ShoppingListItem.path).body()
}
suspend fun addShoppingListItem(shoppingListItem: ShoppingListItem) {
jsonClient.post(ShoppingListItem.path) {
contentType(ContentType.Application.Json)
setBody(shoppingListItem)
}
}
suspend fun deleteShoppingListItem(shoppingListItem: ShoppingListItem) {
jsonClient.delete(ShoppingListItem.path + "/${shoppingListItem.id}")
}
Создание пользовательского интерфейса
Вы подготовили основу на стороне клиента и имеете чистый API для доступа к данным, предоставляемым сервером. Теперь вы можете приступить к отображению списка покупок на экране в приложении React.
Настройка точки входа для приложения
Вместо отрисовки простого текста "Hello, Kotlin/JS", заставьте приложение отрисовывать функциональный App компонент. Для этого замените содержимое внутри src/jsMain/kotlin/Main.kt следующим:
import web.dom.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 следующим образом:
-
Создайте и заполните файл
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 при первом инициализации компонента.React-хуки
useEffectOnceиuseStateпомогают вам лаконично использовать функционал React. Для получения дополнительной информации о работе React-хуков ознакомьтесь с официальной документацией React. Для получения дополнительной информации о React с Kotlin/JS, см. учебник по созданию веб-приложения с React и Kotlin/JS.
Запустите приложение, используя задачу Gradle
run.-
Перейдите к
http://localhost:9090/, чтобы увидеть список:
Добавление компонента поля ввода
Далее, позвольте пользователям добавлять новые записи в список покупок с помощью поля ввода текста. Вам потребуется компонент ввода, который предоставляет обратный вызов при отправке пользователями записи в список покупок для получения ввода.
-
Создайте файл
src/jsMain/kotlin/InputComponent.ktи заполните его следующим определением:import web.html.HTMLFormElement import react.* import web.html.HTMLInputElement import react.dom.events.ChangeEventHandler import react.dom.events.FormEventHandler import web.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 } } }inputComponentотслеживает своё внутреннее состояние (что пользователь ввёл до сих пор) и предоставляет обработчикonSubmit, который вызывается, когда пользователь отправляет форму (обычно нажав клавишуEnter). -
Для использования этого
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с сервера, обновления состояния приложения и перерисовки содержимого React.
Реализация удаления элементов
Добавьте возможность удаления завершённых элементов из списка, чтобы он не становился слишком длинным. Вместо добавления нового элемента пользовательского интерфейса (например, кнопки «Удалить»), вы можете разрешить пользователям удалять элемент, нажав на него.
Для этого передайте соответствующий обработчик в onClick элементов списка:
-
В
src/jsMain/kotlin/App.kt, обновите блокli(внутри блокаul):li { key = item.toString() onClick = { scope.launch { deleteShoppingListItem(item) shoppingList = getShoppingList() } } +"[${item.priority}] ${item.desc} " }Клиент API вызывается вместе с элементом, который должен быть удалён. Сервер обновляет список покупок, что перерисовывает пользовательский интерфейс.
Запустите приложение, используя задачу Gradle
run.-
Перейдите к
http://localhost:9090/и попробуйте добавлять и удалять элементы из списка:
Включение базы данных для хранения данных
В настоящее время приложение не сохраняет данные, поэтому список покупок исчезает при завершении процесса сервера. Для решения этой проблемы используйте базу данных MongoDB для хранения и извлечения элементов списка покупок даже при выключении сервера.
MongoDB проста в настройке, быстра, поддерживается библиотекой для Kotlin и предоставляет простое хранилище документов NoSQL, что более чем достаточно для базового приложения. Вы можете использовать другой механизм хранения данных для своего приложения.
Для предоставления всех функций, используемых в этом разделе, вам необходимо включить несколько библиотек из экосистем Kotlin и JavaScript (npm). См. блок зависимостей jsMain в файле build.gradle.kts с полными настройками.
Настройка MongoDB
Установите MongoDB Community Edition на свой локальный компьютер с официального сайта MongoDB. В качестве альтернативы вы можете использовать инструмент контейнеризации, такой как podman, для запуска контейнеризованной инстанции MongoDB.
После установки убедитесь, что вы запускаете службу mongodb-community для остальной части руководства. Вы будете использовать её для хранения и извлечения записей списка.
Включение KMongo в процесс
KMongo — созданная сообществом Kotlin-рамка, которая упрощает работу с MongoDB из кода Kotlin/JVM. Она также хорошо работает с kotlinx.serialization, которая используется для облегчения связи между клиентом и сервером.
Используя внешнюю базу данных, вам больше не нужно хранить коллекцию shoppingListItems на сервере. Вместо этого настройте клиента базы данных и получите базу данных и коллекцию из него.
-
Внутри
src/jvmMain/kotlin/Server.ktудалите объявление дляshoppingListи добавьте следующие три переменные верхнего уровня:val client = KMongo.createClient().coroutine val database = client.getDatabase("shoppingList") val collection = database.getCollection<ShoppingListItem>() -
В
src/jvmMain/kotlin/Server.kt, замените определения маршрутов GET, POST и DELETE наShoppingListItem, чтобы использовать доступные операции с коллекцией: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из базы данных. Запустите сервер с помощью задачи
runи перейдите по ссылкеhttp://localhost:9090/. При первом запуске вы увидите пустой список покупок, как ожидается при запросе к пустой базе данных.Добавьте несколько элементов в свой список покупок. Сервер сохранит их в базе данных.
Для проверки перезапустите сервер и обновите страницу.
Просмотр MongoDB
Чтобы увидеть, какая информация фактически сохраняется в базе данных, вы можете просмотреть базу данных с помощью внешних инструментов.
Если у вас есть IntelliJ IDEA Ultimate Edition или DataGrip, вы можете просмотреть содержимое базы данных с помощью этих инструментов. В качестве альтернативы вы можете использовать mongosh командную оболочку.
-
Для подключения к локальной инстанции MongoDB в IntelliJ IDEA Ultimate или DataGrip перейдите на вкладку База данных и выберите + | Источник данных | MongoDB:

-
Если это ваш первый раз при подключении к базе данных MongoDB таким образом, вам может потребоваться загрузить отсутствующие драйверы:

При работе с локальной установкой MongoDB, использующей настройки по умолчанию, корректировки конфигурации не требуются. Вы можете проверить соединение с помощью кнопки Проверить соединение, которая должна вывести версию MongoDB и дополнительную информацию.
-
Нажмите ОК. Теперь вы можете использовать окно База данных, чтобы перейти к вашей коллекции и посмотреть всё, что в ней хранится:

Соответствующая конфигурация Gradle для Kmongo
KMongo добавляется с одной зависимостью в проект, определенная версия, которая включает поддержку сопроцедур и сериализации из коробки:
val jvmMain by getting {
dependencies {
// ...
implementation("org.litote.kmongo:kmongo-coroutine-serialization:$kmongoVersion")
}
}
Развертывание в облаке
Вместо запуска вашего приложения на localhost, вы можете вынести его в веб, развернув его в облаке.
Для запуска приложения на управляемой инфраструктуре (например, облачных провайдеров) необходимо интегрировать его с переменными окружения, предоставленными выбранной платформой, и добавить необходимые конфигурации в проект. В частности, передайте порт приложения и строку подключения к MongoDB.
Укажите переменную PORT
На управляемых платформах порт, на котором должно работать приложение, часто определяется внешне и отображается через переменную окружения PORT. При наличии вы можете учесть это, настроив embeddedServer в 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 в строку подключения.
Чтобы удовлетворить этим требованиям, инициализируйте переменные client и database в 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 создаётся на основе этой информации всякий раз, когда установлены переменные окружения. В противном случае (например, на localhost), подключение к базе данных инициализируется как прежде.
Создание файла Procfile
Управляемые облачные платформы, такие как Heroku или PaaS-реализации, такие как Dokku, также обрабатывают жизненный цикл вашего приложения. Для этого они требуют определения «точки входа». Эти две платформы используют файл Procfile, который у вас находится в корне проекта. Он указывает на выходные данные, создаваемые задачей stage (которая уже включена в шаблон Gradle):
web: ./build/install/shoppingList/bin/shoppingList
Включение режима производства
Чтобы включить компиляцию с оптимизациями для JavaScript-активов, передайте ещё один флаг в процесс сборки. В меню Настройки запуска/отладки установите переменную окружения ORG_GRADLE_PROJECT_isProduction в true. Вы можете установить эту переменную окружения при развертывании приложения в целевой среде.
Соответствующая конфигурация 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")
}
}
}
}
Что дальше
Добавить больше функций
Узнайте, как расширить и улучшить ваше приложение:
Улучшить дизайн. Вы можете использовать
styled-components, одну из библиотек, у которой есть обертки для Kotlin. Если вы хотите увидетьstyled-components, обратитесь к учебнику Создание веб-приложения с React и Kotlin/JS.Добавить перечёркнутые пункты списка. Сейчас пункты списка просто исчезают, без записи о том, что они существовали. Вместо удаления элемента используйте перечёркивание.
Реализовать редактирование. Пока запись в списке покупок нельзя редактировать. Подумайте о добавлении кнопки редактирования.
Присоединяйтесь к сообществу и получите помощь
Вы можете присоединиться к официальным каналам Kotlin Slack, #ktor, #javascript, и другим, чтобы получить помощь по проблемам, связанным с Kotlin, от сообщества.
Узнать больше о Kotlin/JS
Вы можете найти дополнительные учебные материалы, ориентированные на Kotlin/JS: Настройка проекта Kotlin/JS и Запуск Kotlin/JS.
Узнать больше о Ktor
Для получения подробной информации о фреймворке Ktor, включая демонстрационные проекты, посетите ktor.io.
Если у вас возникнут проблемы, обратитесь к трекеру проблем Ktor на YouTrack – и если вы не можете найти свою проблему, не стесняйтесь создать новую.
Узнать больше о Kotlin Multiplatform
Узнайте больше о том, как работает многоплатформенный код на Kotlin.
© 2010–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/multiplatform-full-stack-app.html