Spec-Zone.ru › Kotlin 1.7

Создайте веб-приложение с полным стеком с помощью Kotlin Multiplatform

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

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

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

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

  • kotlinx.serialization

  • kotlinx.coroutines

  • Фреймворк Ktor

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

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

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

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

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

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

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

Скопируйте репозиторий проекта из GitHub и откройте его в IntelliJ IDEA. Этот шаблон уже включает всю конфигурацию и необходимые зависимости для всех частей проекта: JVM, JS и общий код.

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

В качестве альтернативы, вы можете получить представление о конфигурации и настройке проекта в файле build.gradle.kts для подготовки к другим проектам. Ознакомьтесь с разделами о структуре Gradle ниже.

Плагины

Как и все проекты Kotlin, нацеленные на несколько платформ, ваш проект использует плагин Kotlin Multiplatform Gradle. Он обеспечивает единую точку конфигурации для целевых платформ приложения (в данном случае Kotlin/JVM и Kotlin/JS) и предоставляет несколько задач жизненного цикла для них.

Кроме того, вам понадобятся ещё два плагина:

  • Плагин application запускает серверную часть приложения, использующую JVM.

  • Плагин serialization предоставляет многоплатформенные преобразования между объектами Kotlin и их текстовым представлением JSON.

plugins {
    kotlin("multiplatform") version "1.7.20-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.

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

    import io.ktor.http.*
    import io.ktor.serialization.kotlinx.json.*
    import io.ktor.server.engine.*
    import io.ktor.server.netty.*
    import io.ktor.server.application.*
    import io.ktor.server.plugins.compression.*
    import io.ktor.server.plugins.contentnegotiation.*
    import io.ktor.server.plugins.cors.routing.*
    import io.ktor.server.request.*
    import io.ktor.server.response.*
    import io.ktor.server.routing.*
    
    fun main() {
        embeddedServer(Netty, 9090) {
            routing {
                get("/hello") {
                    call.respondText("Hello, API!")
                }
            }
        }.start(wait = true)
    }
    
    • Первый конечный пункт API — HTTP-метод get, и маршрут, по которому он должен быть доступен, /hello.

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

  2. Чтобы запустить приложение и убедиться, что всё работает, выполните задачу 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. Запустив сервер, выполните запрос, используя кнопку запуска в строке состояния.

    Если всё пройдёт успешно, окно «запуск» должно отобразить 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 вашем приложении, а также при подготовке к развёртыванию на другой целевой системе или облачной платформе.

Чтобы убедиться, что задача 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-артефакт в корневом каталоге.

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

    get("/") {
        call.respondText(
            this::class.java.classLoader.getResource("index.html")!!.readText(),
            ContentType.Text.Html
        )
    }
    static("/") {
        resources("")
    }
    route(ShoppingListItem.path) {
        // ...
    }
    
  2. Чтобы подтвердить, что всё прошло как запланировано, снова запустите приложение с задачей 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

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

END_OF_DOCUMENT_MARKER ```

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

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

  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 с сервера при первом инициализации компонента.

    • 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 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).

  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 MongoDB website. В качестве альтернативы вы можете использовать инструмент контейнеризации, такой как 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 type-safe queries для получения и удаления правильного 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")
            }
        }
    }
}
END_OF_DOCUMENT_MARKER

Что дальше

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

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

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

Последнее изменение: 07 сентября 2022 г.
Настройка целей для Kotlin Multiplatform Создание и публикация многоплатформенной библиотеки – учебник

© 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

Spec-Zone.ru

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