Создайте веб-приложение с полным стеком с помощью Kotlin Multiplatform
В этом руководстве показано, как создать связанное приложение с полным стеком с помощью IntelliJ IDEA. Вы создадите простой JSON-API и научитесь использовать API из веб-приложения с помощью Kotlin и React.
Приложение состоит из серверной части, использующей Kotlin/JVM, и веб-клиента, использующего Kotlin/JS. Обе части будут одним проектом Kotlin Multiplatform. Поскольку всё приложение будет на Kotlin, вы сможете совместно использовать библиотеки и парадигмы программирования (например, использование сопрограмм для конкурентности) как на фронте, так и на бэке.
Использование Kotlin во всём стеке также позволяет создавать классы и функции, которые можно использовать как из JVM, так и из JS-целей вашего приложения. В этом руководстве вы будете главным образом использовать эту функциональность для совместного использования безопасного типа данных представления данных между клиентом и сервером.
Вы также будете использовать популярные многоплатформенные библиотеки и фреймворки Kotlin:
Фреймворк Ktor
Сериализация и десериализация в типы-объекты и из них делегируется многоплатформенной библиотеке kotlinx.serialization. Это помогает сделать коммуникацию с данными безопасной и лёгкой в реализации.
Выходным результатом будет простое приложение для списка покупок, которое позволит вам планировать свои походы по магазинам.
Пользовательский интерфейс будет простым: список запланированных покупок и поле для ввода новых пунктов покупок.
Если пользователь нажмёт на элемент в списке покупок, он будет удалён.
Пользователь также может указать уровень приоритета для записей в списке, добавив восклицательный знак
!. Эта информация поможет упорядочить список покупок.
Создание проекта
Скопируйте репозиторий проекта из 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-RC"
application //to run JVM part
kotlin("plugin.serialization") version "1.7.20-RC"
}
Цели
Конфигурация целей внутри блока 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.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 } -
Запустив сервер, выполните запрос, используя кнопку запуска в строке состояния.
Если всё пройдёт успешно, окно «запуск» должно отобразить
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 вашем приложении, а также при подготовке к развёртыванию на другой целевой системе или облачной платформе.
Чтобы убедиться, что задача 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 будет предоставляться по корневому маршруту / и будет экспонировать JavaScript-артефакт в корневом каталоге.
-
В
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) { // ... } Чтобы подтвердить, что всё прошло как запланировано, снова запустите приложение с задачей 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.*
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.
Настройка точки входа для приложения
Вместо рендеринга простого сообщения "Привет, 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 компонент следующим образом:
-
Создайте и заполните файл
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с сервера при первом инициализации компонента.React-хуки
useEffectOnceиuseStateпомогут вам лаконично использовать функциональность React. Для получения дополнительной информации о работе React-хуков ознакомьтесь с официальной документацией React. Для получения более подробной информации о React с Kotlin/JS обратитесь к учебнику Разработка веб-приложения с React и Kotlin/JS.
Запустите приложение с помощью Gradle
runзадачи.-
Перейдите по ссылке
http://localhost:9090/, чтобы увидеть список:
Добавление компонента поля ввода
Далее, позвольте пользователям добавлять новые записи в список покупок с помощью поля ввода текста. Вам понадобится компонент ввода, который предоставляет обратный вызов при отправке пользователем записи в список покупок для получения ввода.
-
Создайте файл
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 } } }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 MongoDB website. В качестве альтернативы вы можете использовать инструмент контейнеризации, такой как 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 type-safe queries для получения и удаления правильного
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.
Узнать больше о Kotlin Multiplatform
Узнайте больше о том, как работает многоплатформенный код в Kotlin.
© 2010–2022 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