Spec-Zone.ru › Kotlin 1.8

Разработка полнофункционального веб-приложения на 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.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.

END_OF_DOCUMENT_MARKER

Создание бэкенда

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

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

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

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

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

Соответствующая конфигурация 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 — они общие для всех целевых платформ.

Создание маршрутов для 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 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 следующим образом:

  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 MongoDB documentation.

Указание переменной 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. Вы можете установить эту переменную среды при развертывании приложения в целевой среде.

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

Соответствующая конфигурация 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.

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

© 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