Spec-Zone.ru › Kotlin 1.9

Разработка полностекового веб-приложения с Kotlin Multiplatform

Этот учебник демонстрирует, как создать связанное полностековое приложение с помощью IntelliJ IDEA. Вы создадите простой JSON API и научитесь использовать API из веб-приложения с использованием Kotlin и React.

Приложение состоит из серверной части, использующей Kotlin/JVM, и веб-клиента, использующего Kotlin/JS. Обе части будут одним проектом Kotlin Multiplatform. Поскольку все приложение будет на Kotlin, вы можете использовать общие библиотеки и парадигмы программирования (например, использование Coroutines для конкурентности) как на фронте, так и на бэке.

Использование Kotlin на всех уровнях также позволяет писать классы и функции, которые можно использовать как с JVM, так и с JS-целями вашего приложения. В этом учебнике вы будете в основном использовать эту функциональность для обмена безопасным типом представления данных между клиентом и сервером.

Вы также будете использовать популярные библиотеки и фреймворки Kotlin multiplatform:

  • kotlinx.serialization

  • kotlinx.coroutines

  • Фреймворк Ktor

Сериализация и десериализация в типы-объекты делегируются библиотеке kotlinx.serialization multiplatform. Это помогает вам сделать передачу данных безопасной и легкой в реализации.

Выходом будет простое приложение для составления списка покупок, которое позволит вам планировать покупки продуктов.

  • Пользовательский интерфейс будет простым: список запланированных покупок и поле для ввода новых пунктов списка покупок.

  • Если пользователь нажмет на элемент в списке покупок, он будет удален.

  • Пользователь также может указать уровень приоритета для записей в списке, добавив знак восклицания !. Эта информация поможет упорядочить список покупок.

Для этого учебника предполагается, что вы знакомы с Kotlin. Некоторое знание основных концепций React и Kotlin coroutines может помочь понять некоторые примеры кода, но это не строго обязательно.

Создание проекта

Скопируйте репозиторий проекта из 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.

END_OF_DOCUMENT_MARKER

Построение бэкенда

Начнём с написания серверной части приложения. Типичный сервер API реализует операции CRUD — создание, чтение, обновление и удаление. Для простого списка покупок вы можете сосредоточиться только на:

  • Создание новых записей в списке

  • Чтение записей с помощью API

  • Удаление записей

Для создания бэкенда вы можете использовать фреймворк Ktor, предназначенный для создания асинхронных серверов и клиентов в связанных системах. Его можно быстро настроить, и он будет расти по мере усложнения систем.

Дополнительную информацию о Ktor вы найдете в его документации.

Запуск встроенного сервера

Инициализируйте сервер с помощью Ktor. Вам необходимо указать встроенному серверу Ktor, который поставляется вместе с Ktor, использовать Netty движок на порту, в данном случае 9090.

  1. Для определения точки входа в приложение добавьте следующий код в src/jvmMain/kotlin/Server.kt:

    import io.ktor.http.*
    import io.ktor.serialization.kotlinx.json.*
    import io.ktor.server.engine.*
    import io.ktor.server.netty.*
    import io.ktor.server.application.*
    import io.ktor.server.plugins.compression.*
    import io.ktor.server.plugins.contentnegotiation.*
    import io.ktor.server.plugins.cors.routing.*
    import io.ktor.server.request.*
    import io.ktor.server.response.*
    import io.ktor.server.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.

    • Все необходимые для остальной части учебника импорты уже добавлены.

  2. Для запуска приложения и проверки работоспособности выполните задачу Gradle run. Вы можете использовать команду ./gradlew run в терминале или запустить из окна инструмента Gradle:

    Execute the Gradle run task
  3. После завершения компиляции приложения и запуска сервера перейдите в веб-браузере по адресу http://localhost:9090/hello, чтобы увидеть первый маршрут в действии:

    Hello, API output

Позже, как и с конечным пунктом для запросов 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 для операций с моделями. Все изменения имени конечного пункта нужно выполнять только здесь — клиент и сервер будут автоматически скорректированы.

Этот пример вычисляет простой id из hashCode() его описания. В данном случае этого достаточно, но при работе с реальными данными предпочтительнее использовать проверенные механизмы для генерации идентификаторов ваших объектов — от UUID до автоинкрементируемых идентификаторов, поддерживаемых базой данных по вашему выбору.

Добавление элементов в хранилище

Теперь вы можете использовать модель ShoppingListItem для создания некоторых образцовых элементов и отслеживания любых добавлений или удалений, произведенных через API.

Поскольку сейчас базы данных нет, создайте MutableList для временного хранения ShoppingListItem. Для этого добавьте следующее объявление уровня файла в src/jvmMain/kotlin/Server.kt:

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

Классы common используются как и любой другой класс в Kotlin — они общие для всех целевых платформ.

END_OF_DOCUMENT_MARKER

Создание маршрутов для JSON API

Добавьте маршруты, которые поддерживают создание, получение и удаление ShoppingListItem.

  1. Внутри src/jvmMain/kotlin/Server.kt, измените свой блок routing следующим образом:

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

    Маршруты сгруппированы на основе общего пути. Вам не нужно указывать путь route как String. Вместо этого используется путь path из модели ShoppingListItem. Код работает следующим образом:

    • Запрос get к пути модели (/shoppingList) возвращает весь список покупок.

    • Запрос post к пути модели (/shoppingList) добавляет запись в список покупок.

    • Запрос delete к пути модели и предоставленному id (shoppingList/47) удаляет запись из списка покупок.

    Вы можете получать объекты напрямую из запросов и отвечать на запросы объектами (и даже списками объектов) напрямую. Поскольку вы ранее настроили ContentNegotiation с поддержкой json(), объекты, помеченные как @Serializable автоматически преобразуются в JSON перед отправкой (в случае запроса GET) или получением (в случае запроса POST).

  2. Проверьте, чтобы убедиться, что все работает как запланировано. Перезапустите приложение, перейдите по адресу http://localhost:9090/shoppingList и проверьте, что данные правильно передаются. Вы должны увидеть примеры элементов в формате JSON:

    Shopping list in JSON formatting

Для проверки запросов post и delete используйте HTTP-клиент, который поддерживает файлы .http. Если вы используете IntelliJ IDEA Ultimate Edition, вы можете сделать это прямо из IDE.

  1. В корне проекта создайте файл под названием AddShoppingListElement.http и добавьте объявление HTTP-POST-запроса следующим образом:

    POST http://localhost:9090/shoppingList
    Content-Type: application/json
    
    {
      "desc": "Peppers 🌶",
      "priority": 5
    }
    
  2. После запуска сервера выполните запрос, используя кнопку запуска в метке.

    Если все прошло хорошо, окно "run" должно отобразить HTTP/1.1 200 OK, и вы можете снова посетить http://localhost:9090/shoppingList, чтобы проверить, что запись была добавлена правильно:

    Successful connection to localhost
  3. Повторите этот процесс для файла под названием 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-артефакт в корневой директории.

  1. В 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) {
        // ...
    }
    
  2. Чтобы убедиться, что все прошло успешно, запустите приложение снова с задачей Gradle run.

  3. Перейдите по адресу http://localhost:9090/. Вы должны увидеть страницу с надписью "Привет, Kotlin/JS":

    Hello, Kotlin/JS output

Редактирование конфигурации

Во время разработки система сборки генерирует разработочные артефакты. Это означает, что при преобразовании Kotlin-кода в JavaScript не применяются оптимизации. Это ускоряет время компиляции, но также приводит к увеличению размера JS-файлов. При развертывании приложения в веб это то, чего следует избегать.

Чтобы указать Gradle на генерацию оптимизированных производственных ресурсов, установите необходимую переменную среды. Если вы работаете с приложением на системе развертывания, вы можете настроить ее на установку этой переменной во время сборки. Если вы хотите попробовать режим производства локально, вы можете сделать это в терминале или добавив переменную в конфигурацию запуска:

  1. В IntelliJ IDEA выберите действие Изменить конфигурации:

    Edit run configuration in IntelliJ IDEA
  2. В меню Конфигурации запуска/отладки установите переменную среды:

    ORG_GRADLE_PROJECT_isProduction=true
    
    Set the environment variable

Последующие сборки с этой конфигурацией запуска будут выполнять все доступные оптимизации для фронта приложения, включая удаление неиспользуемого кода. Они все же будут медленнее, чем сборки разработки, поэтому будет полезно снова убрать этот флаг во время разработки.

Создание фронтенда

Для отрисовки и управления элементами пользовательского интерфейса используйте популярную фреймворк 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 следующим образом:

  1. Создайте и заполните файл src/jsMain/kotlin/App.kt:

    import react.*
    import kotlinx.coroutines.*
    import react.dom.html.ReactHTML.h1
    import react.dom.html.ReactHTML.li
    import react.dom.html.ReactHTML.ul
    
    private val scope = MainScope()
    
    val App = FC<Props> {
        var shoppingList by useState(emptyList<ShoppingListItem>())
    
        useEffectOnce {
            scope.launch {
                shoppingList = getShoppingList()
            }
        }
    
        h1 {
            +"Full-Stack Shopping List"
        }
        ul {
            shoppingList.sortedByDescending(ShoppingListItem::priority).forEach { item ->
                li {
                    key = item.toString()
                    +"[${item.priority}] ${item.desc} "
                }
            }
        }
    }
    
    • Здесь используется Kotlin DSL для определения HTML-представления приложения.

    • launch используется для получения списка ShoppingListItem с API при первом инициализации компонента.

    • React-хуки useEffectOnce и useState помогают вам лаконично использовать функционал React. Для получения дополнительной информации о работе React-хуков ознакомьтесь с официальной документацией React. Для получения дополнительной информации о React с Kotlin/JS, см. учебник по созданию веб-приложения с React и Kotlin/JS.

  2. Запустите приложение, используя задачу Gradle run.

  3. Перейдите к http://localhost:9090/, чтобы увидеть список:

    New shopping list rendering

Добавление компонента поля ввода

Далее, позвольте пользователям добавлять новые записи в список покупок с помощью поля ввода текста. Вам потребуется компонент ввода, который предоставляет обратный вызов при отправке пользователями записи в список покупок для получения ввода.

  1. Создайте файл 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).

  2. Для использования этого inputComponent из приложения, добавьте следующий фрагмент в src/jsMain/kotlin/App.kt в нижней части блока FC (после закрывающей фигурной скобки для элемента ul):

    inputComponent {
        onSubmit = { input ->
            val cartItem = ShoppingListItem(input.replace("!", ""), input.count { it == '!' })
            scope.launch {
                addShoppingListItem(cartItem)
                shoppingList = getShoppingList()
            }
        }
    }
    
    • При отправке текста создаётся новая ShoppingListItem. Её приоритет устанавливается в виде количества восклицательных знаков в вводе, а описание — ввод со всеми восклицательными знаками, удалёнными. Это превращает Peaches!! 🍑 в ShoppingListItem(desc="Peaches 🍑", priority=2).

    • Сгенерированный ShoppingListItem отправляется на сервер с клиентом, который вы создали ранее.

    • Затем пользовательский интерфейс обновляется путём получения нового списка ShoppingListItem с сервера, обновления состояния приложения и перерисовки содержимого React.

Реализация удаления элементов

Добавьте возможность удаления завершённых элементов из списка, чтобы он не становился слишком длинным. Вместо добавления нового элемента пользовательского интерфейса (например, кнопки «Удалить»), вы можете разрешить пользователям удалять элемент, нажав на него.

Для этого передайте соответствующий обработчик в onClick элементов списка:

  1. В src/jsMain/kotlin/App.kt, обновите блок li (внутри блока ul):

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

    Клиент API вызывается вместе с элементом, который должен быть удалён. Сервер обновляет список покупок, что перерисовывает пользовательский интерфейс.

  2. Запустите приложение, используя задачу Gradle run.

  3. Перейдите к http://localhost:9090/ и попробуйте добавлять и удалять элементы из списка:

    Final shopping list

Включение базы данных для хранения данных

В настоящее время приложение не сохраняет данные, поэтому список покупок исчезает при завершении процесса сервера. Для решения этой проблемы используйте базу данных 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 на сервере. Вместо этого настройте клиента базы данных и получите базу данных и коллекцию из него.

  1. Внутри src/jvmMain/kotlin/Server.kt удалите объявление для shoppingList и добавьте следующие три переменные верхнего уровня:

    val client = KMongo.createClient().coroutine
    val database = client.getDatabase("shoppingList")
    val collection = database.getCollection<ShoppingListItem>()
    
  2. В src/jvmMain/kotlin/Server.kt, замените определения маршрутов GET, POST и DELETE на 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 из базы данных.

  3. Запустите сервер с помощью задачи run и перейдите по ссылке http://localhost:9090/. При первом запуске вы увидите пустой список покупок, как ожидается при запросе к пустой базе данных.

  4. Добавьте несколько элементов в свой список покупок. Сервер сохранит их в базе данных.

  5. Для проверки перезапустите сервер и обновите страницу.

Просмотр MongoDB

Чтобы увидеть, какая информация фактически сохраняется в базе данных, вы можете просмотреть базу данных с помощью внешних инструментов.

Если у вас есть IntelliJ IDEA Ultimate Edition или DataGrip, вы можете просмотреть содержимое базы данных с помощью этих инструментов. В качестве альтернативы вы можете использовать mongosh командную оболочку.

  1. Для подключения к локальной инстанции MongoDB в IntelliJ IDEA Ultimate или DataGrip перейдите на вкладку База данных и выберите + | Источник данных | MongoDB:

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

    Download missing drivers for MongoDB
  3. При работе с локальной установкой MongoDB, использующей настройки по умолчанию, корректировки конфигурации не требуются. Вы можете проверить соединение с помощью кнопки Проверить соединение, которая должна вывести версию MongoDB и дополнительную информацию.

  4. Нажмите ОК. Теперь вы можете использовать окно База данных, чтобы перейти к вашей коллекции и посмотреть всё, что в ней хранится:

    Use the Database tool for collection analysis

Соответствующая конфигурация Gradle для Kmongo

KMongo добавляется с одной зависимостью в проект, определенная версия, которая включает поддержку сопроцедур и сериализации из коробки:

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

Развертывание в облаке

Вместо запуска вашего приложения на localhost, вы можете вынести его в веб, развернув его в облаке.

Для запуска приложения на управляемой инфраструктуре (например, облачных провайдеров) необходимо интегрировать его с переменными окружения, предоставленными выбранной платформой, и добавить необходимые конфигурации в проект. В частности, передайте порт приложения и строку подключения к MongoDB.

При развертывании приложения вам может потребоваться изменить правила брандмауэра, чтобы разрешить приложению доступ к базе данных. Более подробную информацию см. в документации 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. Вы можете установить эту переменную окружения при развертывании приложения в целевой среде.

Вы можете найти готовое приложение на GitHub на final ветке.

Соответствующая конфигурация Gradle

Задача stage — это псевдоним для installDist:

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

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

Что дальше

Добавить больше функций

Узнайте, как расширить и улучшить ваше приложение:

  • Улучшить дизайн. Вы можете использовать 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.

Последнее изменение: 14 июля 2023 г.

© 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API