Создайте веб-приложение с React и Kotlin/JS — учебник
В этом руководстве вы узнаете, как создать веб-приложение с Kotlin/JS и фреймворком React. Вы:
Выполните распространённые задачи, связанные с созданием типичного приложения React.
Изучите, как DSL Kotlin можно использовать для краткого и единообразного выражения концепций без потери читабельности, позволяя вам написать полноценное приложение полностью на Kotlin.
Научитесь использовать готовые компоненты npm, использовать внешние библиотеки и публиковать конечное приложение.
Выходным приложением будет веб-приложение KotlinConf Explorer, посвящённое событию KotlinConf, с ссылками на доклады конференции. Пользователи смогут просматривать все доклады на одной странице и помечать их как просмотренные или непросмотренные.
В этом руководстве предполагается, что вы имеете предварительные знания Kotlin и базовые знания HTML и CSS. Знание основных концепций React может помочь вам понять некоторые примеры кода, но это не строго необходимо.
Перед началом
Загрузите и установите последнюю версию IntelliJ IDEA.
-
Скопируйте шаблон проекта здесь и откройте его в 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, содержимое тела (включая divroot) загружается первым, чтобы убедиться, что браузер загрузит все элементы страницы перед скриптами.
-
Фрагмент кода в
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:

Для запуска программы из терминала используйте ./gradlew run.
После компиляции и сборки проекта в окне браузера появится пустая красная страница:
Включить режим горячей перезагрузки/непрерывный режим
Настройте режим непрерывной компиляции, чтобы не нужно было вручную компилировать и запускать свой проект каждый раз при внесении изменений. Прежде чем продолжить, убедитесь, что все запущенные экземпляры сервера разработки остановлены.
-
Отредактируйте конфигурацию запуска, которую IntelliJ IDEA автоматически генерирует после первого запуска задачи Gradle
run:
-
В диалоговом окне Конфигурации запуска/отладки добавьте параметр
--continuousв аргументы конфигурации запуска:
После применения изменений вы можете использовать кнопку Запуск внутри IntelliJ IDEA, чтобы снова запустить сервер разработки. Для запуска непрерывных сборок Gradle из терминала используйте
./gradlew run --continuous. -
Чтобы проверить эту функцию, измените цвет страницы на синий в файле
Main.kt, пока задача Gradle выполняется:document.bgColor = "blue"
Затем проект перекомпилируется, и после перезагрузки страница браузера будет нового цвета.
Вы можете оставить сервер разработки запущенным в непрерывном режиме во время процесса разработки. Он автоматически перестроит и перезагрузит страницу, когда вы внесете изменения.
Создание черновика веб-приложения
Добавление первой статической страницы с 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-страницу:
Преобразование 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.
Подождите, пока браузер перезагрузится. Страница теперь должна выглядеть так:
Добавление видео с помощью конструкций Kotlin в разметке
Существуют некоторые преимущества написания HTML на Kotlin с помощью этого DSL. Вы можете манипулировать своим приложением с помощью обычных конструкций Kotlin, таких как циклы, условия, коллекции и интерполяция строк.
Теперь вы можете заменить жёстко закодированный список видео списком объектов Kotlin:
-
В
Main.ktсоздайте данные класс для хранения всех атрибутов видео в одном месте:data class Video( val id: Int, val title: String, val speaker: String, val videoUrl: String ) -
Заполните два списка для не просмотренных видео и просмотренных видео соответственно. Добавьте эти объявления на уровне файла в
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") ) -
Чтобы использовать эти видео на странице, напишите цикл Kotlin
for, чтобы перебрать коллекцию объектов не просмотренныхVideo. Замените три тегаpпод "Видео для просмотра" следующей строкой:for (video in unwatchedVideos) { p { +"${video.speaker}: ${video.title}" } } -
Примените тот же процесс для изменения кода для единственного тега после "Просмотренные видео" также:
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() в целом описывает базовый компонент. Текущая структура вашего приложения выглядит следующим образом:
Если вы разложите свое приложение на отдельные компоненты, у вас получится более структурированная структура, в которой каждый компонент отвечает за свои обязанности:
Компоненты инкапсулируют определенную функциональность. Использование компонентов сокращает исходный код и делает его более читаемым и понятным.
Добавление основного компонента
Для начала создания структуры приложения, сначала явно укажите App, основной компонент для рендеринга в rootэлемент:
Создайте новый файл
App.ktв папкеsrc/main/kotlin.-
Внутри этого файла добавьте следующий фрагмент и перенесите безопасный для типов 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создает функциональный компонент. -
В файле
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.
-
Создайте новый файл
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}" } } } -
В
App.ktиспользуйте компонентVideoList, вызвав его без параметров:// . . . div { h3 { +"Videos to watch" } VideoList() h3 { +"Videos watched" } VideoList() } // . . .Пока компонент
Appне контролирует содержимое, отображаемое компонентомVideoList. Оно жёстко задано, поэтому вы видите один и тот же список дважды.
Добавление свойств для передачи данных между компонентами
Поскольку вы собираетесь повторно использовать компонент VideoList, вам необходимо заполнить его разным содержимым. Вы можете добавить возможность передавать список элементов как атрибут в компонент. В React эти атрибуты называются свойствами. Когда свойства компонента изменяются в React, фреймворк автоматически перерисовывает компонент.
Для VideoList вам потребуется свойство, содержащее список видео для отображения. Определите интерфейс, который содержит все свойства, которые могут быть переданы в компонент VideoList:
-
Добавьте следующее определение в файл
VideoList.kt:external interface VideoListProps : Props { var videos: List<Video> }Модификатор external сообщает компилятору, что реализация интерфейса предоставляется внешним образом, поэтому он не пытается генерировать код JavaScript из объявления.
-
Измените определение класса
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. -
В компоненте
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}"
}
// . . .
Если вы нажмете на один из элементов списка в окне браузера, вы получите информацию о видео в окне предупреждения, как показано ниже:
Добавление состояния для сохранения значений
Вместо простого вывода предупреждения вы можете добавить некоторую функциональность для выделения выбранного видео треугольником ▶. Для этого введите некоторое состояние, специфичное для этого компонента.
Состояние является одной из ключевых концепций в React. В современном React (который использует так называемый API хуков), состояние выражается с помощью useState хука.
-
Добавьте следующий код в начало объявления
VideoList:val VideoList = FC<VideoListProps> { props -> var selectedVideo: Video? by useState(null) // . . .Функциональный компонент
VideoListсохраняет состояние (значение, независимое от текущего вызова функции). Состояние является nullable и имеет типVideo?. Его значение по умолчанию равноnull.Функция
useState()из React инструктирует фреймворк отслеживать состояние при многократных вызовах функции. Например, даже если вы задаете значение по умолчанию, React гарантирует, что значение по умолчанию присваивается только в самом начале. При изменении состояния компонент будет перерисован на основе нового состояния.Ключевое слово
byуказывает, чтоuseState()действует как делегированное свойство. Как и с любой другой переменной, вы читаете и записываете значения. Реализация заuseState()позаботится о механизме, необходимом для работы состояния.
Для получения дополнительной информации о хуке состояния, ознакомьтесь с документацией React.
-
Измените реализацию компонента
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
В настоящее время два списка видео работают независимо, то есть каждый список отслеживает выбранное видео. Пользователи могут выбрать два видео, одно в списке «непросмотренные» и одно в списке «просмотренные», даже если есть только один проигрыватель:
Список не может отслеживать, какое видео выбрано, как внутри себя, так и внутри своего соседнего списка. Причина в том, что выбранное видео является частью не состояния списка, а состояния приложения. Это означает, что вам нужно поднять состояние из отдельных компонентов.
Поднятие состояния
React гарантирует, что свойства могут передаваться только от родительского компонента к дочерним. Это предотвращает жесткую привязку компонентов.
Если компонент хочет изменить состояние соседнего компонента, он должен сделать это через родительский компонент. В этот момент состояние также больше не принадлежит ни одному из дочерних компонентов, а родительскому компоненту.
Процесс перемещения состояния из компонентов в их родительские компоненты называется поднятием состояния. Для вашего приложения добавьте currentVideo в качестве состояния в компонент App:
-
В
App.ktдобавьте следующее в начало определения компонентаApp:val App = FC<Props> { var currentVideo: Video? by useState(null) // . . . }Компонент
VideoListбольше не нуждается в отслеживании состояния. Вместо этого он будет получать текущее видео в качестве свойства. Удалите вызов
useState()вVideoList.kt.-
Подготовьте компонент
VideoListдля получения выбранного видео в качестве свойства. Для этого расширьте интерфейсVideoListPropsтак, чтобы он содержалselectedVideo:external interface VideoListProps : Props { var videos: List<Video> var selectedVideo: Video? } -
Измените условие треугольника, чтобы он использовал
propsвместоstate:if (video == props.selectedVideo) { +"▶ " }
Передача обработчиков
В данный момент нет способа присвоить значение свойству, поэтому функция onClick не будет работать так, как сейчас настроено. Чтобы изменить состояние родительского компонента, необходимо снова поднять состояние.
В React состояние всегда передаётся от родителя к дочернему элементу. Таким образом, чтобы изменить состояние приложения из одного из дочерних компонентов, необходимо перенести логику обработки взаимодействия пользователя в родительский компонент, а затем передать логику в качестве свойства. Помните, что в Kotlin переменные могут иметь тип функции.
-
Ещё раз расширьте интерфейс
VideoListProps, чтобы он содержал переменнуюonSelectVideo, которая является функцией, принимающейVideoи возвращающейUnit:external interface VideoListProps : Props { // ... var onSelectVideo: (Video) -> Unit } -
В компоненте
VideoListиспользуйте новое свойство в обработчикеonClick:onClick = { props.onSelectVideo(video) } -
Теперь вы можете вернуться к компоненту
Appи передатьselectedVideoи обработчик дляonSelectVideoдля каждого из двух списков видео:VideoList { videos = unwatchedVideos // and watchedVideos respectively selectedVideo = currentVideo onSelectVideo = { video -> currentVideo = video } } Повторите предыдущий шаг для списка просмотренных видео.
Вернитесь в браузер и убедитесь, что при выборе видео выбор перескакивает между двумя списками без дублирования.
Добавление дополнительных компонентов
Извлечение компонента видеоплеера
Теперь вы можете создать ещё один автономный компонент — видеоплеер, который сейчас представляет собой изображение-заполнитель. Вашему видеоплееру нужно знать название доклада, автора доклада и ссылку на видео. Эта информация уже содержится в каждом объекте Video, поэтому вы можете передать её в качестве свойства и получить доступ к её атрибутам.
-
Создайте новый файл
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" } } } -
Поскольку интерфейс
VideoPlayerPropsуказывает, что компонентVideoPlayerпринимает непустоеVideo, убедитесь, что в компонентеAppэто обрабатывается соответствующим образом.В
App.ktзамените предыдущий фрагмент кода для видеоплеера на следующий:currentVideo?.let { curr -> VideoPlayer { video = curr } }Функция области видимости
letгарантирует, что компонентVideoPlayerдобавляется только тогда, когдаstate.currentVideoне null.
Теперь при нажатии на элемент в списке откроется видеоплеер и заполнится информацией из нажатого элемента.
Добавление кнопки и её подключение
Чтобы пользователи могли отмечать видео как просмотренные или непросмотренные и перемещать их между списками, добавьте кнопку в компонент VideoPlayer.
Поскольку эта кнопка будет перемещать видео между двумя различными списками, логика изменения состояния должна быть поднята из компонента VideoPlayer и передана в качестве свойства из родительского компонента. Кнопка должна выглядеть по-разному в зависимости от того, было ли видео просмотрено или нет. Эту информацию также необходимо передать в качестве свойства.
-
Расширьте интерфейс
VideoPlayerPropsвVideoPlayer.kt, чтобы он включал свойства для этих двух случаев:external interface VideoPlayerProps : Props { var video: Video var onWatchedButtonPressed: (Video) -> Unit var unwatchedVideo: Boolean } -
Теперь вы можете добавить кнопку в сам компонент. Скопируйте следующий фрагмент кода в тело компонента
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. При нажатии кнопки видео должно перемещаться из списка «непросмотренные» в список «просмотренные» или наоборот. Поскольку эти списки теперь могут меняться, поместите их в состояние приложения:
-
В
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") )) // . . . } Поскольку все демо-данные включены в значения по умолчанию для
watchedVideosиunwatchedVideosнапрямую, вам больше не нужны объявления на уровне файла. ВMain.ktудалите объявления дляwatchedVideosиunwatchedVideos.-
Измените место вызова
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.
-
Проверьте файл
build.gradle.kts. Пакетreact-playerдолжен быть уже включён:dependencies { // ... // Video Player implementation(npm("react-player", "2.10.1")) // ... }Как видите, npm зависимости могут быть добавлены в проект Kotlin/JS с помощью функции
npm()в блокеdependenciesфайла сборки. Затем плагин Gradle позаботится о загрузке и установке этих зависимостей. Для этого он использует собственный установленный менеджер пакетовyarn. -
Для использования 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:
-
Отредактируйте содержимое
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 } -
Теперь вы можете использовать новый
ReactPlayerдля замены серого прямоугольника-заглушки в компонентеVideoPlayer. ВVideoPlayer.ktзамените тегimgследующим фрагментом:ReactPlayer { url = props.video.videoUrl controls = true }
Добавление кнопок для совместного использования в социальных сетях
Легким способом совместного использования контента приложения является использование кнопок для обмена в мессенджерах и по электронной почте. Для этого также можно использовать готовый React-компонент, например, react-share:
-
Проверьте файл
build.gradle.kts. Эта библиотека npm должна быть уже включена:dependencies { // ... // Share Buttons implementation(npm("react-share", "4.4.0")) // ... } -
Для использования
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 } -
Добавьте новые компоненты в пользовательский интерфейс приложения. В
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 видео. Если кнопки не отображаются или не работают, вам может потребоваться отключить блокировщик рекламы и социальных сетей.

Не стесняйтесь повторять этот шаг с кнопками обмена для других социальных сетей, доступных в react-share.
Использование внешнего 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.
-
Проверьте файл
build.gradle.kts. Соответствующий фрагмент кода уже должен существовать:plugins { // . . . kotlin("plugin.serialization") version "1.7.10" } dependencies { // . . . // Serialization implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3") } -
В качестве подготовки к извлечению первого видео, необходимо сообщить библиотеке сериализации о классе
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:
-
Добавьте следующее реализацию в ваш
App.kt:suspend fun fetchVideos(): List<Video> = coroutineScope { (1..25).map { id -> async { fetchVideo(id) } }.awaitAll() }Следуя принципу структурированной конкурентности, реализация обернута в
coroutineScope. После этого вы можете запустить 25 асинхронных задач (по одному запросу) и подождать, пока все они завершатся. -
Теперь вы можете добавить данные в свое приложение. Добавьте определение для
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 хука). Он указывает, что компонент выполняет побочный эффект. Он не просто рендерит себя, но и взаимодействует с сетью.
Проверьте ваш браузер. Приложение должно отображать реальные данные:

При загрузке страницы:
Будет вызван код компонента
App. Это запускает код в блокеuseEffectOnce.Компонент
Appрендерится со списками просмотренных и непросмотренных видео, которые пусты.Когда запросы к API завершатся, блок
useEffectOnceприсвоит их состоянию компонентаApp. Это вызовет повторное рендеринг.Код компонента
Appбудет вызван снова, но блокuseEffectOnceне будет выполнен во второй раз.
Если вы хотите получить глубокое понимание работы сопрограмм, ознакомьтесь с практическим занятием по сопрограммам здесь.
Развёртывание в продакшене и облаке
Пришло время опубликовать приложение в облаке и сделать его доступным другим людям.
Создание сборки для продакшена
Чтобы собрать все ресурсы в режиме продакшена, выполните задачу build в Gradle через окно инструментов в IntelliJ IDEA или запустив ./gradlew build. Это сгенерирует оптимизированную сборку проекта, применяя различные улучшения, такие как DCE (исключение неиспользуемого кода).
После завершения сборки все файлы, необходимые для развертывания, можно найти в /build/distributions. Они включают файлы JavaScript, HTML и другие ресурсы, необходимые для запуска приложения. Вы можете разместить их на статическом HTTP-сервере, использовать GitHub Pages или разместить их на облачном провайдере по своему выбору.
Развёртывание на Heroku
Heroku значительно упрощает запуск приложения, доступного по собственному домену. Бесплатного тарифа должно быть достаточно для целей разработки.
-
Создайте репозиторий Git и прикрепите приложение Heroku, выполнив следующие команды в терминале в корне проекта:
git init heroku create git add . git commit -m "initial commit"
-
В отличие от обычного JVM-приложения, которое будет работать на Heroku (например, написанного с использованием Ktor или Spring Boot), ваше приложение генерирует статические HTML-страницы и JavaScript-файлы, которые необходимо соответствующим образом обслуживать. Вы можете настроить необходимые buildpacks для правильной работы программы:
heroku buildpacks:set heroku/gradle heroku buildpacks:add https://github.com/heroku/heroku-buildpack-static.git
-
Для корректной работы buildpack
heroku/gradle, необходимо, чтобы задачаstageприсутствовала в файлеbuild.gradle.kts. Эта задача эквивалентна задачеbuild, и соответствующий псевдоним уже включён в нижней части файла:// Heroku Deployment tasks.register("stage") { dependsOn("build") } Добавьте новый файл
static.jsonв корень проекта для настройкиbuildpack-static.-
Добавьте свойство
rootв этот файл:{ "root": "build/distributions" } -
Теперь вы можете запустить развертывание, например, выполнив следующую команду:
git add -A git commit -m "add stage task and static content root configuration" git push heroku master
Если развертывание прошло успешно, вы увидите URL, который пользователи могут использовать для доступа к приложению в интернете.

Что дальше
Добавить больше функций
Вы можете использовать полученное приложение в качестве отправной точки для изучения более сложных тем в области React, Kotlin/JS и других.
Поиск. Вы можете добавить поле поиска для фильтрации списка докладов – например, по названию или автору. Узнайте, как работают элементы HTML-формы в React здесь.
Персистентность. В настоящее время приложение теряет отслеживание списка просмотров каждый раз, когда страница перезагружается. Подумайте о создании собственного бэкенда, используя один из веб-фреймворков для Kotlin (например, Ktor). В качестве альтернативы, изучите способы хранения информации на клиенте.
Сложные API. Доступно множество наборов данных и API. Вы можете подключать всевозможные данные в своё приложение. Например, вы можете создать визуализатор для изображений кошек или API бесплатных стоковых фотографий.
Улучшение стиля: адаптивность и сетки
Дизайн приложения всё ещё очень прост и не будет хорошо выглядеть на мобильных устройствах или в узких окнах. Изучите CSS DSL для повышения доступности приложения.
Присоединяйтесь к сообществу и получайте помощь
Лучший способ сообщить о проблемах и получить помощь – это трекер проблем kotlin-wrappers. Если вы не найдёте тикет для своей проблемы, не стесняйтесь создать новый. Вы также можете присоединиться к официальному канале Kotlin Slack. Есть каналы для #javascript и #react.
Узнайте больше о корутинах
Если вы хотите узнать больше о том, как писать конкурирующий код, ознакомьтесь с практическим руководством по корутинам.
Узнайте больше о React
Теперь, когда вы знаете основные концепции React и как они переводятся на Kotlin, вы можете преобразовать другие концепции, описанные в официальных руководствах по React, в Kotlin.
© 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