Spec-Zone.ru › Kotlin 1.9

Создание веб-приложения с React и Kotlin/JS — руководство

Это руководство научит вас создавать веб-приложение с помощью Kotlin/JS и фреймворка React. Вы будете:

  • Выполнять общие задачи, связанные с созданием типичного приложения React.

  • Изучать, как DSL Kotlin можно использовать для выражения концепций лаконично и единообразно без потери читаемости, позволяя вам создавать полноценное приложение полностью на Kotlin.

  • Узнавать, как использовать готовые компоненты npm, использовать внешние библиотеки и опубликовать конечное приложение.

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

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

Вы можете получить конечное приложение здесь.

Перед началом

  1. Загрузите и установите последнюю версию IntelliJ IDEA.

  2. Скопируйте шаблон проекта и откройте его в IntelliJ IDEA. Шаблон включает базовый проект Kotlin Multiplatform Gradle со всеми необходимыми конфигурациями и зависимостями

    • Зависимости и задачи в файле build.gradle.kts:

    dependencies {
        // React, React DOM + Wrappers
        implementation(enforcedPlatform("org.jetbrains.kotlin-wrappers:kotlin-wrappers-bom:1.0.0-pre.430"))
        implementation("org.jetbrains.kotlin-wrappers:kotlin-react")
        implementation("org.jetbrains.kotlin-wrappers:kotlin-react-dom")
    
        // Kotlin React Emotion (CSS)
        implementation("org.jetbrains.kotlin-wrappers:kotlin-emotion")
    
        // Video Player
        implementation(npm("react-player", "2.12.0"))
    
        // Share Buttons
        implementation(npm("react-share", "4.4.1"))
    
        // Coroutines & serialization
        implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
        implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
    }
    
    • Страница шаблона HTML в src/jsMain/resources/index.html для вставки JavaScript-кода, который вы будете использовать в этом руководстве:

    <!doctype html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Hello, Kotlin/JS!</title>
    </head>
    <body>
        <div id="root"></div>
        <script src="confexplorer.js"></script>
    </body>
    </html>
    

    Kotlin/JS-проекты автоматически собираются со всем вашим кодом и его зависимостями в один JavaScript-файл с тем же именем, что и проект, confexplorer.js, при сборке. Как типичная конвенция JavaScript, содержимое тела (включая div root) загружается в первую очередь, чтобы гарантировать, что браузер загрузит все элементы страницы перед скриптами.

  • Фрагмент кода в src/jsMain/kotlin/Main.kt:

    import kotlinx.browser.document
    
    fun main() {
        document.bgColor = "red"
    }
    

Запуск сервера разработки

По умолчанию плагин Kotlin Multiplatform Gradle поставляется с поддержкой встроенного webpack-dev-server, позволяя запускать приложение из IDE без ручного настройки серверов.

Чтобы проверить, что программа успешно запускается в браузере, запустите сервер разработки, вызвав задачу run или browserDevelopmentRun (доступную в каталоге other или kotlin browser) из окна Gradle в IntelliJ IDEA:

Gradle tasks list

Чтобы запустить программу из терминала, используйте ./gradlew run вместо этого.

После компиляции и сборки проекта в окне браузера появится пустая красная страница:

Blank red page

Включение горячей перезагрузки / непрерывного режима

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

  1. Отредактируйте конфигурацию запуска, которую IntelliJ IDEA автоматически генерирует после первого запуска задачи Gradle run:

    Edit a run configuration
  2. В диалоговом окне Конфигурации запуска/отладки добавьте опцию --continuous в аргументы конфигурации запуска:

    Enable continuous mode

    После применения изменений можно использовать кнопку Запуск в IntelliJ IDEA, чтобы снова запустить сервер разработки. Чтобы запустить непрерывные сборки Gradle из терминала, используйте ./gradlew run --continuous вместо этого.

  3. Чтобы протестировать эту функцию, измените цвет страницы на синий в файле Main.kt, пока задача Gradle выполняется:

    document.bgColor = "blue"
    

    Затем проект перекомпилируется, и после перезагрузки страница браузера будет нового цвета.

Вы можете оставить сервер разработки запущенным в непрерывном режиме во время процесса разработки. Он автоматически пересобирает и перезагружает страницу при внесении изменений.

Вы можете найти это состояние проекта на ветке master здесь.

Создание черновика веб-приложения

Добавление первой статической страницы с React

Чтобы ваше приложение отображало простое сообщение, замените код в файле Main.kt следующим:

import kotlinx.browser.document
import react.*
import emotion.react.css
import csstype.Position
import csstype.px
import react.dom.html.ReactHTML.h1
import react.dom.html.ReactHTML.h3
import react.dom.html.ReactHTML.div
import react.dom.html.ReactHTML.p
import react.dom.html.ReactHTML.img
import react.dom.client.createRoot
import kotlinx.serialization.Serializable

fun main() {
    val container = document.getElementById("root") ?: error("Couldn't find root container!")
    createRoot(container).render(Fragment.create {
        h1 {
            +"Hello, React+Kotlin/JS!"
        }
    })
}
  • Функция render() инструктирует kotlin-react-dom отобразить первый HTML-элемент внутри фрагмента в элемент root. Этот элемент является контейнером, определённым в src/jsMain/resources/index.html, который был включён в шаблон.

  • Содержание представляет собой заголовок <h1> и использует безопасную для типов DSL для отображения HTML.

  • h1 — это функция, принимающая лямбда-параметр. Когда вы добавляете знак + перед строковой литеральной, функция unaryPlus() фактически вызывается с помощью перегрузки операторов. Она добавляет строку к заключённому HTML-элементу.

При повторной компиляции проекта браузер отобразит эту HTML-страницу:

An HTML page example

Преобразование HTML в безопасную для типов DSL HTML Kotlin

Kotlin-обертки для React поставляются с обёртками и специально-доменным языком (DSL), который позволяет писать HTML в чистом Kotlin-коде. Таким образом, он похож на JSX из JavaScript. Однако, поскольку этот разметка — Kotlin, вы получаете все преимущества статически типизированного языка, такие как автодополнение или проверка типов.

Сравните классический HTML-код вашего будущего веб-приложения и его безопасный для типов аналог на Kotlin:

<h1>KotlinConf Explorer</h1>
<div>
    <h3>Videos to watch</h3>
    <p>John Doe: Building and breaking things</p>
    <p>Jane Smith: The development process</p>
    <p>Matt Miller: The Web 7.0</p>
    <h3>Videos watched</h3>
    <p>Tom Jerry: Mouseless development</p>
</div>
<div>
    <h3>John Doe: Building and breaking things</h3>
    <img src="https://via.placeholder.com/640x360.png?text=Video+Player+Placeholder">
</div>
h1 {
    +"KotlinConf Explorer"
}
div {
    h3 {
        +"Videos to watch"
    }
    p {
        + "John Doe: Building and breaking things"
    }
    p {
        +"Jane Smith: The development process"
    }
    p {
        +"Matt Miller: The Web 7.0"
    }
    h3 {
        +"Videos watched"
    }
    p {
        +"Tom Jerry: Mouseless development"
    }
}
div {
    h3 {
        +"John Doe: Building and breaking things"
    }
    img {
       src = "https://via.placeholder.com/640x360.png?text=Video+Player+Placeholder"
    }
}

Скопируйте Kotlin-код и обновите вызов функции Fragment.create() внутри функции main(), заменив предыдущий тег h1.

Подождите, пока браузер перезагрузится. Страница теперь должна выглядеть так:

The web app draft

Добавление видео с использованием Kotlin-конструктов в разметке

Существуют некоторые преимущества написания HTML на Kotlin с помощью этой DSL. Вы можете манипулировать своим приложением с помощью обычных Kotlin-конструктов, таких как циклы, условия, коллекции и интерполяция строк.

Теперь вы можете заменить жёстко заданный список видео списком Kotlin-объектов:

  1. В Main.kt создайте Video класс данных, чтобы сохранить все атрибуты видео в одном месте:

    data class Video(
        val id: Int,
        val title: String,
        val speaker: String,
        val videoUrl: String
    )
    
  2. Заполните два списка: для не просмотренных видео и просмотренных видео, соответственно. Добавьте эти объявления на уровне файла в Main.kt:

    val unwatchedVideos = listOf(
        Video(1, "Opening Keynote", "Andrey Breslav", "https://youtu.be/PsaFVLr8t4E"),
        Video(2, "Dissecting the stdlib", "Huyen Tue Dao", "https://youtu.be/Fzt_9I733Yg"),
        Video(3, "Kotlin and Spring Boot", "Nicolas Frankel", "https://youtu.be/pSiZVAeReeg")
    )
    
    val watchedVideos = listOf(
        Video(4, "Creating Internal DSLs in Kotlin", "Venkat Subramaniam", "https://youtu.be/JzTeAM8N1-o")
    )
    
  3. Чтобы использовать эти видео на странице, напишите Kotlin-цикл for для итерации по коллекции объектов не просмотренных Video. Замените три тега p под «Видео к просмотру» следующим фрагментом:

    for (video in unwatchedVideos) {
        p {
            +"${video.speaker}: ${video.title}"
        }
    }
    
  4. Примените тот же процесс для изменения кода для единственного тега после «Просмотренные видео»:

    for (video in watchedVideos) {
        p {
            +"${video.speaker}: ${video.title}"
        }
    }
    

Подождите, пока браузер перезагрузится. Макет должен остаться таким же, как и раньше. Вы можете добавить ещё несколько видео в список, чтобы убедиться, что цикл работает.

Добавление стилей с безопасной для типов CSS

Обёртка kotlin-emotion для библиотеки Emotion позволяет указывать атрибуты CSS — даже динамические — прямо рядом с HTML с JavaScript. По сути, это похоже на CSS-в-JS — но для Kotlin. Преимущество использования DSL заключается в том, что вы можете использовать Kotlin-конструкты для выражения правил форматирования.

Шаблон проекта для этого учебника уже включает необходимую зависимость для использования kotlin-emotion:

dependencies {
    // ...
    // Kotlin React Emotion (CSS) (chapter 3)
    implementation("org.jetbrains.kotlin-wrappers:kotlin-emotion")
    // ...
}

С помощью kotlin-emotion можно указать блок css внутри HTML-элементов div и h3, где можно определить стили.

Чтобы переместить видеоплеер в верхний правый угол страницы, используйте CSS и измените код для видеоплеера (последний div в фрагменте):

div {
    css {
        position = Position.absolute
        top = 10.px
        right = 10.px
    }
    h3 {
        +"John Doe: Building and breaking things"
    }
    img {
        src = "https://via.placeholder.com/640x360.png?text=Video+Player+Placeholder"
    }
}

Не стесняйтесь экспериментировать с другими стилями. Например, вы можете изменить fontFamily или добавить некоторые color в свой интерфейс пользователя.

Проектирование компонентов приложения

Основными строительными блоками в React являются компоненты. Сами компоненты также могут быть составлены из других, более мелких компонентов. Объединяя компоненты, вы создаёте своё приложение. Если вы структурируете компоненты так, чтобы они были универсальными и могли повторно использоваться, вы сможете применять их в нескольких частях приложения, не дублируя код или логику.

Содержание функции render() обычно описывает базовый компонент. Текущая структура вашего приложения выглядит так:

Current layout

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

Structured layout with components

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

Добавление основного компонента

Для начала создания структуры приложения, сначала явно укажите App, главный компонент для отрисовки в элементе root:

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

  2. Внутри этого файла добавьте следующий фрагмент и перенесите типизированный HTML из Main.kt в него:

    import kotlinx.coroutines.async
    import react.*
    import react.dom.*
    import kotlinx.browser.window
    import kotlinx.coroutines.*
    import kotlinx.serialization.decodeFromString
    import kotlinx.serialization.json.Json
    import emotion.react.css
    import csstype.Position
    import csstype.px
    import react.dom.html.ReactHTML.h1
    import react.dom.html.ReactHTML.h3
    import react.dom.html.ReactHTML.div
    import react.dom.html.ReactHTML.p
    import react.dom.html.ReactHTML.img
    
    val App = FC<Props> {
        // typesafe HTML goes here, starting with the first h1 tag!
    }
    

    Функция FC создаёт функциональный компонент.

  3. В файле Main.kt обновите функцию main() следующим образом:

    fun main() {
        val container = document.getElementById("root") ?: error("Couldn't find root container!")
        createRoot(container).render(App.create())
    }
    

    Теперь программа создаёт экземпляр компонента App и отображает его в указанном контейнере.

Для получения дополнительной информации о концепциях React, ознакомьтесь с документацией и руководствами.

Извлечение компонента списка

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

Компонент VideoList следует той же схеме, что и компонент App. Он использует функцию-билдер FC и содержит код из списка unwatchedVideos.

  1. Создайте новый файл VideoList.kt в папке src/jsMain/kotlin и добавьте следующий код:

    import kotlinx.browser.window
    import react.*
    import react.dom.*
    import react.dom.html.ReactHTML.p
    
    val VideoList = FC<Props> {
        for (video in unwatchedVideos) {
            p {
                +"${video.speaker}: ${video.title}"
            }
        }
    }
    
  2. В App.kt используйте компонент VideoList, вызвав его без параметров:

    // . . .
    
    div {
        h3 {
            +"Videos to watch"
        }
        VideoList()
    
        h3 {
            +"Videos watched"
        }
        VideoList()
    }
    
    // . . .
    

    Пока компонент App не контролирует содержимое, отображаемое компонентом VideoList. Оно жёстко закодировано, поэтому вы видите один и тот же список дважды.

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

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

Для VideoList вам понадобится свойство, содержащее список видео, которые нужно показать. Определите интерфейс, содержащий все свойства, которые могут быть переданы компоненту VideoList:

  1. Добавьте следующее определение в файл VideoList.kt:

    external interface VideoListProps : Props {
        var videos: List<Video>
    }
    

    Модификатор external сообщает компилятору, что реализация интерфейса предоставляется внешне, поэтому он не пытается сгенерировать JavaScript-код из объявления.

  2. Измените определение класса VideoList, чтобы использовать свойства, переданные в блок FC как параметр:

    val VideoList = FC<VideoListProps> { props ->
        for (video in props.videos) {
            p {
                key = video.id.toString()
                +"${video.speaker}: ${video.title}"
            }
        }
    }
    

    Атрибут key помогает рендереру React определить, что делать, когда значение props.videos изменяется. Он использует ключ, чтобы определить, какие части списка нужно обновить, а какие останутся прежними. Более подробную информацию о списках и ключах можно найти в руководстве React.

  3. В компоненте App убедитесь, что дочерние компоненты инициализируются с правильными атрибутами. В App.kt замените два цикла под элементами h3 вызовом VideoList вместе со свойствами для unwatchedVideos и watchedVideos. В Kotlin DSL вы присваиваете их внутри блока, принадлежащего компоненту VideoList:

    h3 {
        +"Videos to watch"
    }
    VideoList {
        videos = unwatchedVideos
    }
    h3 {
        +"Videos watched"
    }
    VideoList {
        videos = watchedVideos
    }
    

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

Делаем список интерактивным

Сначала добавьте сообщение об ошибке, которое появляется при нажатии на запись в списке. В VideoList.kt добавьте обработчик onClick, который вызывает окно с текущим видео:

// . . .

p {
    key = video.id.toString()
    onClick = {
        window.alert("Clicked $video!")
    }
    +"${video.speaker}: ${video.title}"
}

// . . .

Если вы нажмёте на один из элементов списка в окне браузера, вы получите информацию о видео в всплывающем окне, например:

Browser alert window

Определение функции onClick непосредственно как лямбда-выражения краткое и очень полезное для прототипирования. Однако из-за способа работы равенства в настоящее время в Kotlin/JS с точки зрения производительности это не самый оптимальный способ передачи обработчиков щелчков. Если вы хотите оптимизировать производительность рендеринга, рассмотрите возможность сохранения ваших функций в переменной и передачи их.

Добавление состояния для сохранения значений

Вместо простого отображения сообщения об ошибке, вы можете добавить функциональность для выделения выбранного видео треугольником ▶. Для этого введите некоторое состояние, специфичное для этого компонента.

Состояние — одна из ключевых концепций в React. В современном React (который использует так называемый API хуков), состояние выражается с помощью useState хука.

  1. Добавьте следующий код в начало объявления VideoList:

    val VideoList = FC<VideoListProps> { props ->
        var selectedVideo: Video? by useState(null)
    
    // . . .
    
    • Функциональный компонент VideoList сохраняет состояние (значение, независимое от текущего вызова функции). Состояние является nullable и имеет тип Video?. Его значение по умолчанию — null.

    • Функция useState() из React сообщает фреймворку отслеживать состояние во время многократных вызовов функции. Например, даже если вы указываете значение по умолчанию, React гарантирует, что значение по умолчанию устанавливается только в самом начале. Когда состояние изменяется, компонент будет перерисовываться на основе нового состояния.

    • Ключевое слово by указывает, что useState() выступает в качестве делегированного свойства. Как и с любой другой переменной, вы читаете и записываете значения. Реализация за useState() берёт на себя механику, необходимую для работы состояния.

    Чтобы узнать больше о хуке состояния, обратитесь к документации React.

  2. Измените обработчик onClick и текст в компоненте VideoList следующим образом:

    val VideoList = FC<VideoListProps> { props ->
        var selectedVideo: Video? by useState(null)
        for (video in props.videos) {
            p {
                key = video.id.toString()
                onClick = {
                    selectedVideo = video
                }
                if (video == selectedVideo) {
                    +"▶ "
                }
                +"${video.speaker}: ${video.title}"
            }
        }
    }
    
    • Когда пользователь нажимает на видео, его значение присваивается переменной selectedVideo.

    • При рендеринге выбранной записи списка треугольник добавляется в начало.

Дополнительные сведения об управлении состоянием можно найти в часто задаваемых вопросах React.

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

Компоненты Compose

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

Two videos are selected in both lists simultaneously

Список не может отслеживать, какое видео выбрано, как внутри себя, так и внутри списка-побратима. Причина в том, что выбранное видео является частью не состояния списка, а состояния приложения. Это означает, что вам нужно поднять состояние из отдельных компонентов.

Поднятие состояния

React гарантирует, что свойства могут передаваться только от родительского компонента к дочерним. Это предотвращает жесткую привязку компонентов.

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

Процесс миграции состояния из компонентов в родительские компоненты называется подъёмом состояния. Для вашего приложения добавьте currentVideo в качестве состояния в компонент App:

  1. В App.kt добавьте следующее в начало определения компонента App:

    val App = FC<Props> {
        var currentVideo: Video? by useState(null)
    
        // . . .
    }
    

    Компоненту VideoList больше не нужно отслеживать состояние. Вместо этого он будет получать текущее видео в качестве свойства.

  2. Удалите вызов useState() в VideoList.kt.

  3. Подготовьте компонент VideoList к приему выбранного видео в качестве свойства. Для этого расширьте интерфейс VideoListProps, чтобы он содержал selectedVideo:

    external interface VideoListProps : Props {
        var videos: List<Video>
        var selectedVideo: Video?
    }
    
  4. Измените условие треугольника, чтобы он использовал props вместо state:

    if (video == props.selectedVideo) {
        +"▶ "
    }
    

Передача обработчиков

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

В React состояние всегда передаётся от родителя к потомку. Таким образом, чтобы изменить состояние приложения из одного из дочерних компонентов, необходимо перенести логику обработки взаимодействия с пользователем в родительский компонент, а затем передать эту логику как свойство. Помните, что в Kotlin переменные могут иметь тип функции.

  1. Ещё раз расширьте интерфейс VideoListProps, чтобы он содержал переменную onSelectVideo, которая является функцией, принимающей Video и возвращающей Unit:

    external interface VideoListProps : Props {
        // ...
        var onSelectVideo: (Video) -> Unit
    }
    
  2. В компоненте VideoList используйте новое свойство в обработчике onClick:

    onClick = {
        props.onSelectVideo(video)
    }
    

    Теперь можно удалить переменную selectedVideo из компонента VideoList.

  3. Вернитесь к компоненту App и передайте selectedVideo и обработчик для onSelectVideo для каждого из двух списков видео:

    VideoList {
        videos = unwatchedVideos // and watchedVideos respectively
        selectedVideo = currentVideo
        onSelectVideo = { video ->
            currentVideo = video
        }
    }
    
  4. Повторите предыдущий шаг для списка просмотренных видео.

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

Добавление дополнительных компонентов

Извлечение компонента видеоплеера

Теперь вы можете создать ещё один самостоятельный компонент — видеоплеер, который сейчас представляет собой заполнитель изображения. Видеоплееру необходимо знать название выступления, автора выступления и ссылку на видео. Эта информация уже содержится в каждом объекте Video, поэтому вы можете передать её как свойство и получить доступ к его атрибутам.

  1. Создайте новый файл VideoPlayer.kt и добавьте в него следующее реализацию компонента VideoPlayer:

    import csstype.*
    import react.*
    import emotion.react.css
    import react.dom.html.ReactHTML.button
    import react.dom.html.ReactHTML.div
    import react.dom.html.ReactHTML.h3
    import react.dom.html.ReactHTML.img
    
    external interface VideoPlayerProps : Props {
        var video: Video
    }
    
    val VideoPlayer = FC<VideoPlayerProps> { props ->
        div {
            css {
                position = Position.absolute
                top = 10.px
                right = 10.px
            }
            h3 {
                +"${props.video.speaker}: ${props.video.title}"
            }
            img {
                src = "https://via.placeholder.com/640x360.png?text=Video+Player+Placeholder"              
            }
        }
    }
    
  2. Поскольку интерфейс VideoPlayerProps определяет, что компонент VideoPlayer принимает непустое Video, убедитесь, что вы обрабатываете это в компоненте App соответствующим образом.

    В App.kt замените предыдущий фрагмент кода для видеоплеера следующим:

    currentVideo?.let { curr ->
        VideoPlayer {
            video = curr
        }
    }
    

    Функция let гарантирует, что компонент VideoPlayer добавляется только тогда, когда state.currentVideo не равен null.

Теперь при нажатии на элемент в списке откроется видеоплеер и заполнится информацией из нажатого элемента.

Добавление кнопки и её подключение

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

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

  1. Расширьте интерфейс VideoPlayerProps в VideoPlayer.kt, чтобы он включал свойства для этих двух случаев:

    external interface VideoPlayerProps : Props {
        var video: Video
        var onWatchedButtonPressed: (Video) -> Unit
        var unwatchedVideo: Boolean
    }
    
  2. Теперь вы можете добавить кнопку в сам компонент. Скопируйте следующий фрагмент кода в тело компонента VideoPlayer между тегами h3 и img:

    button {
        css {
            display = Display.block
            backgroundColor = if (props.unwatchedVideo) NamedColor.lightgreen else NamedColor.red
        }
        onClick = {
            props.onWatchedButtonPressed(props.video)
        }
        if (props.unwatchedVideo) {
            +"Mark as watched"
        } else {
            +"Mark as unwatched"
        }
    }
    

    С помощью Kotlin CSS DSL, который позволяет динамически изменять стили, вы можете изменить цвет кнопки, используя простое выражение Kotlin if.

Перенос списков видео в состояние приложения

Теперь пришло время изменить место использования VideoPlayer в компоненте App. При нажатии кнопки видео должно перемещаться из списка непросмотренных в список просмотренных или наоборот. Поскольку эти списки теперь могут фактически изменяться, переместите их в состояние приложения:

  1. В App.kt добавьте следующие свойства с вызовами useState() в начало компонента App:

    val App = FC<Props> {
        var currentVideo: Video? by useState(null)
        var unwatchedVideos: List<Video> by useState(listOf(
            Video(1, "Opening Keynote", "Andrey Breslav", "https://youtu.be/PsaFVLr8t4E"),
            Video(2, "Dissecting the stdlib", "Huyen Tue Dao", "https://youtu.be/Fzt_9I733Yg"),
            Video(3, "Kotlin and Spring Boot", "Nicolas Frankel", "https://youtu.be/pSiZVAeReeg")
        ))
        var watchedVideos: List<Video> by useState(listOf(
            Video(4, "Creating Internal DSLs in Kotlin", "Venkat Subramaniam", "https://youtu.be/JzTeAM8N1-o")
        ))
    
        // . . .
    }
    
  2. Поскольку все демонстрационные данные включены в значения по умолчанию для watchedVideos и unwatchedVideos непосредственно, вам больше не нужны объявления на уровне файла. В Main.kt удалите объявления для watchedVideos и unwatchedVideos.

  3. Измените место вызова VideoPlayer в компоненте App, относящемся к видеоплееру, на следующее:

    VideoPlayer {
        video = curr
        unwatchedVideo = curr in unwatchedVideos
        onWatchedButtonPressed = {
            if (video in unwatchedVideos) {
                unwatchedVideos = unwatchedVideos - video
                watchedVideos = watchedVideos + video
            } else {
                watchedVideos = watchedVideos - video
                unwatchedVideos = unwatchedVideos + video
            }
        }
    }
    

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

Использование пакетов из npm

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

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

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

Для замены заглушки видеокомпонента на настоящий плеер YouTube используйте пакет react-player из npm. Он может воспроизводить видео и позволяет управлять внешним видом плеера.

Для получения документации по компоненту и описания API см. его README в GitHub.

  1. Проверьте файл build.gradle.kts. Пакет react-player должен быть уже включён:

    dependencies {
        // ...
        // Video Player
        implementation(npm("react-player", "2.12.0"))
        // ...
    }
    

    Как вы можете видеть, зависимости npm могут быть добавлены в проект Kotlin/JS, используя функцию npm() в блоке dependencies файла сборки. Плагин Gradle затем позаботится о загрузке и установке этих зависимостей для вас. Для этого он использует собственный установленный менеджер пакетов yarn.

  2. Для использования JavaScript пакета внутри приложения React необходимо указать Kotlin-компилятору, чего ожидать, предоставив ему внешние объявления.

    Создайте новый файл ReactYouTube.kt и добавьте в него следующее содержимое:

    @file:JsModule("react-player")
    @file:JsNonModule
    
    import react.*
    
    @JsName("default")
    external val ReactPlayer: ComponentClass<dynamic>
    

    Когда компилятор видит внешнее объявление, такое как ReactPlayer, он предполагает, что реализация соответствующего класса предоставляется зависимостью, и не генерирует код для него.

    Последние две строки эквивалентны JavaScript импорту, такому как require("react-player").default;. Они сообщают компилятору, что с уверенностью можно утверждать, что компонент будет соответствовать ComponentClass<dynamic> во время выполнения.

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

Лучший вариант — создать external interface, который определяет, какие свойства принадлежат свойствам этого внешнего компонента. Вы можете узнать о интерфейсе свойств в README для компонента. В этом случае используйте свойства url и controls:

  1. Измените содержимое ReactYouTube.kt, заменив dynamic внешним интерфейсом:

    @file:JsModule("react-player")
    @file:JsNonModule
    
    import react.*
    
    @JsName("default")
    external val ReactPlayer: ComponentClass<ReactPlayerProps>
    
    external interface ReactPlayerProps : Props {
        var url: String
        var controls: Boolean
    }
    
  2. Теперь вы можете использовать новый ReactPlayer для замены серого прямоугольника-заглушки в компоненте VideoPlayer. В VideoPlayer.kt замените тег img на следующий фрагмент:

    ReactPlayer {
        url = props.video.videoUrl
        controls = true
    }
    

Добавление кнопок обмена в социальные сети

Простым способом поделиться контентом приложения является добавление кнопок обмена в мессенджеры и электронную почту. Вы также можете использовать готовый React компонент для этого, например, react-share:

  1. Проверьте файл build.gradle.kts. Эта библиотека npm должна быть уже включена:

    dependencies {
        // ...
        // Share Buttons
        implementation(npm("react-share", "4.4.1"))
        // ...
    }
    
  2. Для использования react-share из Kotlin вам нужно написать более простые внешние объявления. Примеры на GitHub показывают, что кнопка обмена состоит из двух React компонентов: EmailShareButton и EmailIcon, например. Разные типы кнопок обмена и значков имеют одинаковый вид интерфейса. Вы создадите внешние объявления для каждого компонента так же, как вы уже сделали для видеоплеера.

    Добавьте следующий код в новый файл ReactShare.kt:

    @file:JsModule("react-share")
    @file:JsNonModule
    
    import react.ComponentClass
    import react.Props
    
    @JsName("EmailIcon")
    external val EmailIcon: ComponentClass<IconProps>
    
    @JsName("EmailShareButton")
    external val EmailShareButton: ComponentClass<ShareButtonProps>
    
    @JsName("TelegramIcon")
    external val TelegramIcon: ComponentClass<IconProps>
    
    @JsName("TelegramShareButton")
    external val TelegramShareButton: ComponentClass<ShareButtonProps>
    
    external interface ShareButtonProps : Props {
        var url: String
    }
    
    external interface IconProps : Props {
        var size: Int
        var round: Boolean
    }
    
  3. Добавьте новые компоненты в пользовательский интерфейс приложения. В VideoPlayer.kt добавьте две кнопки обмена в div прямо над использованием ReactPlayer:

    // . . .
    
    div {
        css {
             position = Position.absolute
             top = 10.px
             right = 10.px
         }
        EmailShareButton {
            url = props.video.videoUrl
            EmailIcon {
                size = 32
                round = true
            }
        }
        TelegramShareButton {
            url = props.video.videoUrl
            TelegramIcon {
                size = 32
                round = true
            }
        }
    }
    
    // . . .
    

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

Share window

Вы можете повторить этот шаг с кнопками обмена для других социальных сетей, доступными в react-share.

Использование внешнего REST API

Теперь вы можете заменить жёстко закодированные демонстрационные данные реальными данными из REST API в приложении.

Для этого урока есть небольшой API API. Он предлагает только один конечный пункт, videos, и принимает числовой параметр для доступа к элементу из списка. Если вы посетите API в браузере, вы увидите, что объекты, возвращаемые API, имеют ту же структуру, что и объекты Video.

Использование функциональности JS из Kotlin

Браузеры уже поставляются с большим набором веб-API. Вы также можете использовать их из Kotlin/JS, так как он включает в себя обёртки для этих API по умолчанию. Одним из примеров является API fetch, который используется для выполнения HTTP-запросов.

Первая потенциальная проблема заключается в том, что браузерные API, такие как fetch(), используют обратные вызовы для выполнения асинхронных операций. Когда несколько обратных вызовов должны выполняться один за другим, они должны быть вложены. Естественно, код сильно отступывается, и всё больше и больше функциональных элементов размещаются друг внутри друга, что усложняет чтение.

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

Вторая проблема возникает из-за динамически типизированной природы JavaScript. Нет гарантий относительно типа данных, возвращаемых внешним API. Чтобы решить это, вы можете использовать библиотеку kotlinx.serialization.

Проверьте файл build.gradle.kts. Соответствующий фрагмент кода уже должен существовать:

dependencies {
    // . . .

    // Coroutines & serialization
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
}

Добавление сериализации

При вызове внешнего API вы получаете текст в формате JSON, который ещё необходимо преобразовать в объект Kotlin, с которым можно работать.

kotlinx.serialization — это библиотека, которая позволяет выполнять такие преобразования из строк JSON в объекты Kotlin.

  1. Проверьте файл build.gradle.kts. Соответствующий фрагмент кода уже должен существовать:

    plugins {
        // . . .
        kotlin("plugin.serialization") version "1.9.0"
    }
    
    dependencies {
        // . . .
    
        // Serialization
        implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
    }
    
  2. В качестве подготовки к извлечению первого видео необходимо сообщить библиотеке сериализации о классе Video. В файле Main.kt добавьте аннотацию @Serializable к его определению:

    @Serializable
    data class Video(
        val id: Int,
        val title: String,
        val speaker: String,
        val videoUrl: String
    )
    

Получение видео

Чтобы получить видео из API, добавьте следующую функцию в App.kt (или в новый файл):

suspend fun fetchVideo(id: Int): Video {
    val response = window
        .fetch("https://my-json-server.typicode.com/kotlin-hands-on/kotlinconf-json/videos/$id")
        .await()
        .text()
        .await()
    return Json.decodeFromString(response)
}
  • Подвешенная функция fetch() извлекает видео с заданным id из API. Этот ответ может занять некоторое время, поэтому вы await() результат. Затем text(), которая использует обратный вызов, считывает тело ответа. После этого вы await() его завершение.

  • Перед возвращением значения функции вы передаёте его в Json.decodeFromString, функцию из kotlinx.coroutines. Она преобразует текст JSON, полученный из запроса, в объект Kotlin с соответствующими полями.

  • Вызов функции window.fetch возвращает объект Promise. Обычно вам нужно было бы определить обработчик обратного вызова, который вызывается, как только Promise разрешится и результат будет доступен. Однако с сопрограммами вы можете await() эти обещания. Всякий раз, когда вызывается функция, подобная await(), метод приостанавливает своё выполнение. Выполнение возобновляется, когда Promise можно разрешить.

Чтобы предоставить пользователям выбор видео, определите функцию fetchVideos(), которая получит 25 видео из того же API, что и выше. Чтобы запустить все запросы одновременно, используйте функциональность async, предоставляемую сопрограммами Kotlin:

  1. Добавьте следующее реализацию в App.kt:

    suspend fun fetchVideos(): List<Video> = coroutineScope {
        (1..25).map { id ->
            async {
                fetchVideo(id)
            }
        }.awaitAll()
    }
    

    Следуя принципу структурированной конкурентности, реализация обернута в coroutineScope. После этого вы можете запустить 25 асинхронных задач (по одному запросу на каждую) и дождаться их завершения.

  2. Теперь вы можете добавить данные в своё приложение. Добавьте определение для mainScope и измените свой компонент App так, чтобы он начинался со следующего фрагмента. Не забудьте заменить демонстрационные значения на экземпляры emptyLists:

    val mainScope = MainScope()
    
    val App = FC<Props> {
        var currentVideo: Video? by useState(null)
        var unwatchedVideos: List<Video> by useState(emptyList())
        var watchedVideos: List<Video> by useState(emptyList())
    
        useEffectOnce {
            mainScope.launch {
                unwatchedVideos = fetchVideos()
            }
        }
    
    // . . .
    
    • MainScope() — это часть модели структурированной параллельности Kotlin и создаёт область видимости для выполнения асинхронных задач.

    • useEffectOnce — это ещё один React хук (в частности, упрощённая версия useEffect хука). Он указывает, что компонент выполняет побочное действие. Он не просто отображает себя, но и взаимодействует с сетью.

Проверьте свой браузер. Приложение должно отображать реальные данные:

Fetched data from API

Когда вы загружаете страницу:

  • Вызывается код компонента App. Это запускает код в блоке useEffectOnce.

  • Компонент App отображается со списками пустых отслеживаемых и неотслеживаемых видео.

  • После завершения запросов к API блок useEffectOnce присваивает их состоянию компонента App. Это приводит к повторному рендерингу.

  • Код компонента App вызывается снова, но блок useEffectOnce не будет выполняться во второй раз.

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

Развёртывание в производство и облако

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

Создание сборки для производства

Чтобы собрать все ресурсы в режиме производства, запустите задачу build в Gradle через окно инструментов в IntelliJ IDEA или запустив ./gradlew build. Это сгенерирует оптимизированную сборку проекта, применяя различные улучшения, такие как исключение неиспользуемого кода (DCE).

После завершения сборки все необходимые файлы для развертывания можно найти в /build/dist. Они включают JavaScript-файлы, HTML-файлы и другие ресурсы, необходимые для запуска приложения. Вы можете разместить их на статическом HTTP-сервере, использовать GitHub Pages или разместить их на выбранном вами облачном провайдере.

Развёртывание на Heroku

Heroku позволяет довольно просто создать доступное по своему домену приложение. Бесплатный тариф должен быть достаточным для целей разработки.

  1. Создайте учётную запись.

  2. Установите и аутентифицируйте клиентскую утилиту CLI.

  3. Создайте репозиторий Git и привяжите приложение Heroku, выполнив следующие команды в терминале, находясь в корне проекта:

    git init
    heroku create
    git add .
    git commit -m "initial commit"
    
  4. В отличие от обычного JVM-приложения, которое будет работать на Heroku (например, написанного с использованием Ktor или Spring Boot), ваше приложение генерирует статические HTML-страницы и JavaScript-файлы, которые необходимо обслуживать соответствующим образом. Вы можете настроить необходимые buildpacks для правильного обслуживания программы:

    heroku buildpacks:set heroku/gradle
    heroku buildpacks:add https://github.com/heroku/heroku-buildpack-static.git
    
  5. Чтобы buildpack heroku/gradle работал правильно, необходимо задать задачу stage в файле build.gradle.kts. Эта задача эквивалентна задаче build, и соответствующий псевдоним уже включён в нижней части файла:

    // Heroku Deployment
    tasks.register("stage") {
        dependsOn("build")
    }
    
  6. Добавьте новый файл static.json в корень проекта для настройки buildpack-static.

  7. Добавьте свойство root в этот файл:

    {
        "root": "build/distributions"
    }
    
  8. Теперь вы можете запустить развертывание, например, выполнив следующую команду:

    git add -A
    git commit -m "add stage task and static content root configuration"
    git push heroku master
    

Если вы выполняете push из ветки, отличной от main, скорректируйте команду, чтобы выполнить push в удалённый репозиторий main, например, git push heroku feature-branch:main.

Если развертывание прошло успешно, вы увидите URL-адрес, который пользователи могут использовать для доступа к приложению в интернете.

Web app deployment to production

Вы можете найти состояние проекта на ветке finished здесь.

Что дальше

Добавление дополнительных функций

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

  • Поиск. Вы можете добавить поле поиска для фильтрации списка докладов — например, по заголовку или автору. Узнайте о том, как работают элементы форм HTML в React.

  • Персистентность. В настоящее время приложение теряет информацию о списке просмотренных докладов каждый раз при перезагрузке страницы. Подумайте о создании собственного бэкенда, используя один из веб-фреймворков для Kotlin (такой как Ktor). Или изучите способы хранения информации на клиенте.

  • Сложные API. Доступны многочисленные наборы данных и API. Вы можете получать различные данные в своё приложение. Например, вы можете создать визуализатор для изображений котов или API стоковых фотографий.

Улучшение стиля: адаптивность и сетки

Дизайн приложения всё ещё очень прост и не будет выглядеть хорошо на мобильных устройствах или в узких окнах. Изучите больше возможностей CSS DSL, чтобы сделать приложение более доступным.

Присоединяйтесь к сообществу и получайте помощь

Лучший способ сообщить о проблемах и получить помощь — это трекер проблем kotlin-wrappers. Если вы не можете найти обращение к своей проблеме, можете создать новое. Также вы можете присоединиться к официальному каналу Kotlin Slack. Там есть каналы для #javascript и #react.

Узнайте больше о сопроцедурах

Если вы хотите узнать больше о том, как писать конкурентный код, ознакомьтесь с учебником по сопроцедурам.

Узнайте больше о React

Теперь, когда вы знаете основные понятия React и как они переводятся на Kotlin, вы можете преобразовать некоторые другие понятия, описанные в официальных руководствах по React, в 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/js-react.html

Spec-Zone.ru

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