Создание веб-приложения с 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 в безопасную для типов 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.
Подождите, пока браузер перезагрузится. Страница теперь должна выглядеть так:

Добавление видео с помощью конструкций Kotlin в разметке
Использование HTML на Kotlin с помощью этой DSL имеет некоторые преимущества. Вы можете манипулировать своим приложением с помощью обычных конструкций Kotlin, таких как циклы, условные операторы, коллекции и интерполяция строк.
Теперь вы можете заменить жёстко заданный список видео списком объектов Kotlin:
-
В
Main.ktсоздайтеVideoкласс данных, чтобы хранить все атрибуты видео в одном месте: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-in-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сохраняет состояние (значение, которое не зависит от текущего вызова функции). Состояние может быть нулевым и имеет тип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 в браузере, вы увидите, что объекты, возвращаемые API, имеют такую же структуру, что и объекты Video.
Использование функциональности JS из Kotlin
Браузеры уже поставляются с широким набором Web 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.8.0" } 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–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