Spec-Zone.ru › Kotlin 1.7

Создайте веб-приложение с 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/JS-проект Gradle со всеми необходимыми конфигурациями и зависимостями

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

    dependencies {
        // React, React DOM + Wrappers
        implementation(enforcedPlatform("org.jetbrains.kotlin-wrappers:kotlin-wrappers-bom:1.0.0-pre.354"))
        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.10.1"))
    
        // Share Buttons
        implementation(npm("react-share", "4.4.0"))
    
        // Coroutines & serialization
        implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3")
        implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3")
    }
    
    • Шаблон страницы HTML в файле src/main/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/main/kotlin/Main.kt:

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

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

По умолчанию плагин Kotlin/JS 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/main/resources/index.html, который был включён в шаблон.

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

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

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

An HTML page example

Преобразование HTML в типова-безопасный HTML DSL 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 {
    +"Hello, React+Kotlin/JS!"
}
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 создайте данные класс для хранения всех атрибутов видео в одном месте:

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

Этот вариант проекта доступен в ветке 02-first-static-page здесь.

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

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

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

Current layout

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

Structured layout with components

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

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

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

  1. Создайте новый файл App.kt в папке src/main/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/main/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. Измените реализацию компонента 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.

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

Это состояние проекта можно найти на ветке 03-first-component здесь.

Компоненты 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)
    }
    
  3. Теперь вы можете вернуться к компоненту App и передать selectedVideo и обработчик для onSelectVideo для каждого из двух списков видео:

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

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

Этот проект можно найти на ветке 04-composing-components здесь.

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

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

Теперь вы можете создать ещё один автономный компонент — видеоплеер, который сейчас представляет собой изображение-заполнитель. Вашему видеоплееру нужно знать название доклада, автора доклада и ссылку на видео. Эта информация уже содержится в каждом объекте 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
            }
        }
    }
    

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

Этот проект можно найти на ветке 05-more-components здесь.

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

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

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

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

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

Документацию компонента и описание API см. в его README в GitHub.

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

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

    Как видите, 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. Отредактируйте содержимое ReactPlayer.kt соответственно:

    @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.0"))
        // ...
    }
    
  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.

Вы можете найти эту версию проекта на ветке 06-packages-from-npm здесь.

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

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

Для этого учебника доступен небольшой API. Он предоставляет только один конечный пункт, videos, и принимает числовой параметр для доступа к элементу из списка. Если вы посетите 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.3")
}

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

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

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

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

    plugins {
        // . . .
        kotlin("plugin.serialization") version "1.7.10"
    }
    
    dependencies {
        // . . .
        // Serialization
        implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3")
    }
    
  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 не будет выполнен во второй раз.

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

Вы можете найти это состояние проекта в ветке 07-using-external-rest-api здесь.

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

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

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

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

После завершения сборки все файлы, необходимые для развертывания, можно найти в /build/distributions. Они включают файлы 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 08-deploying-to-production: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.

Последнее изменение: 22 августа 2022 г.
Примеры Начало работы с Kotlin/Native в IntelliJ IDEA

© 2010–2022 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