Разработка полнофункционального веб-приложения на 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.8.0"
application // to run the JVM part
kotlin("plugin.serialization") version "1.8.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 использовать 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значительно уменьшает количество передаваемых данных клиенту, сжимая исходящее содержимое при необходимости.
Соответствующая конфигурация 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 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 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 также поддерживает конфигурационные файлы, которые могут учитывать переменные среды. Чтобы узнать больше о том, как их использовать, ознакомьтесь с официальной документацией official documentation.
Указание переменной 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
Узнайте больше о том, как работает код 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