Создание веб-приложения с React и Kotlin/JS — руководство
Это руководство научит вас создавать веб-приложение с помощью Kotlin/JS и фреймворка React. Вы будете:
Выполнять общие задачи, связанные с созданием типичного приложения React.
Изучать, как DSL Kotlin можно использовать для выражения концепций лаконично и единообразно без потери читаемости, позволяя вам создавать полноценное приложение полностью на Kotlin.
Узнавать, как использовать готовые компоненты npm, использовать внешние библиотеки и опубликовать конечное приложение.
Выходным результатом будет веб-приложение KotlinConf Explorer, посвящённое мероприятию KotlinConf, с ссылками на доклады конференции. Пользователи смогут просматривать все доклады на одной странице и отмечать их как просмотренные или непросмотренные.
Руководство предполагает, что у вас есть предварительные знания Kotlin и базовые знания HTML и CSS. Понимание основных концепций React может помочь вам понять некоторые примеры кода, но это не строго обязательно.
Перед началом
Загрузите и установите последнюю версию IntelliJ IDEA.
-
Скопируйте шаблон проекта и откройте его в IntelliJ IDEA. Шаблон включает базовый проект Kotlin Multiplatform Gradle со всеми необходимыми конфигурациями и зависимостями
Зависимости и задачи в файле
build.gradle.kts:
dependencies { // React, React DOM + Wrappers implementation(enforcedPlatform("org.jetbrains.kotlin-wrappers:kotlin-wrappers-bom:1.0.0-pre.430")) implementation("org.jetbrains.kotlin-wrappers:kotlin-react") implementation("org.jetbrains.kotlin-wrappers:kotlin-react-dom") // Kotlin React Emotion (CSS) implementation("org.jetbrains.kotlin-wrappers:kotlin-emotion") // Video Player implementation(npm("react-player", "2.12.0")) // Share Buttons implementation(npm("react-share", "4.4.1")) // Coroutines & serialization implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") }Страница шаблона HTML в
src/jsMain/resources/index.htmlдля вставки JavaScript-кода, который вы будете использовать в этом руководстве:
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Hello, Kotlin/JS!</title> </head> <body> <div id="root"></div> <script src="confexplorer.js"></script> </body> </html>Kotlin/JS-проекты автоматически собираются со всем вашим кодом и его зависимостями в один JavaScript-файл с тем же именем, что и проект,
confexplorer.js, при сборке. Как типичная конвенция JavaScript, содержимое тела (включая divroot) загружается в первую очередь, чтобы гарантировать, что браузер загрузит все элементы страницы перед скриптами.
-
Фрагмент кода в
src/jsMain/kotlin/Main.kt:import kotlinx.browser.document fun main() { document.bgColor = "red" }
Запуск сервера разработки
По умолчанию плагин Kotlin Multiplatform Gradle поставляется с поддержкой встроенного webpack-dev-server, позволяя запускать приложение из IDE без ручного настройки серверов.
Чтобы проверить, что программа успешно запускается в браузере, запустите сервер разработки, вызвав задачу run или browserDevelopmentRun (доступную в каталоге other или kotlin browser) из окна Gradle в IntelliJ IDEA:

Чтобы запустить программу из терминала, используйте ./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/jsMain/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-в-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/jsMain/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/jsMain/kotlinи добавьте следующий код:import kotlinx.browser.window import react.* import react.dom.* import react.dom.html.ReactHTML.p val VideoList = FC<Props> { for (video in unwatchedVideos) { p { +"${video.speaker}: ${video.title}" } } } -
В
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.
-
Измените обработчик
onClickи текст в компонентеVideoListследующим образом:val VideoList = FC<VideoListProps> { props -> var selectedVideo: Video? by useState(null) for (video in props.videos) { p { key = video.id.toString() onClick = { selectedVideo = video } if (video == selectedVideo) { +"▶ " } +"${video.speaker}: ${video.title}" } } }Когда пользователь нажимает на видео, его значение присваивается переменной
selectedVideo.При рендеринге выбранной записи списка треугольник добавляется в начало.
Дополнительные сведения об управлении состоянием можно найти в часто задаваемых вопросах React.
Проверьте браузер и нажмите на элемент в списке, чтобы убедиться, что всё работает правильно.
Компоненты Compose
В настоящее время два списка видео работают независимо, то есть каждый список отслеживает выбранное видео. Пользователи могут выбрать два видео, одно в списке непросмотренных, а другое — в просмотренных, даже если есть только один плеер:
Список не может отслеживать, какое видео выбрано, как внутри себя, так и внутри списка-побратима. Причина в том, что выбранное видео является частью не состояния списка, а состояния приложения. Это означает, что вам нужно поднять состояние из отдельных компонентов.
Поднятие состояния
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) }Теперь можно удалить переменную
selectedVideoиз компонентаVideoList. -
Вернитесь к компоненту
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.12.0")) // ... }Как вы можете видеть, зависимости 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:
-
Измените содержимое
ReactYouTube.kt, заменивdynamicвнешним интерфейсом:@file:JsModule("react-player") @file:JsNonModule import react.* @JsName("default") external val ReactPlayer: ComponentClass<ReactPlayerProps> external interface ReactPlayerProps : Props { var url: String var controls: Boolean } -
Теперь вы можете использовать новый
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.1")) // ... } -
Для использования
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 API. Он предлагает только один конечный пункт, videos, и принимает числовой параметр для доступа к элементу из списка. Если вы посетите API в браузере, вы увидите, что объекты, возвращаемые API, имеют ту же структуру, что и объекты Video.
Использование функциональности JS из Kotlin
Браузеры уже поставляются с большим набором веб-API. Вы также можете использовать их из Kotlin/JS, так как он включает в себя обёртки для этих API по умолчанию. Одним из примеров является API fetch, который используется для выполнения HTTP-запросов.
Первая потенциальная проблема заключается в том, что браузерные API, такие как fetch(), используют обратные вызовы для выполнения асинхронных операций. Когда несколько обратных вызовов должны выполняться один за другим, они должны быть вложены. Естественно, код сильно отступывается, и всё больше и больше функциональных элементов размещаются друг внутри друга, что усложняет чтение.
Чтобы преодолеть это, вы можете использовать сопрограммы Kotlin, лучший подход для такой функциональности.
Вторая проблема возникает из-за динамически типизированной природы JavaScript. Нет гарантий относительно типа данных, возвращаемых внешним API. Чтобы решить это, вы можете использовать библиотеку kotlinx.serialization.
Проверьте файл build.gradle.kts. Соответствующий фрагмент кода уже должен существовать:
dependencies {
// . . .
// Coroutines & serialization
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
}
Добавление сериализации
При вызове внешнего API вы получаете текст в формате JSON, который ещё необходимо преобразовать в объект Kotlin, с которым можно работать.
kotlinx.serialization — это библиотека, которая позволяет выполнять такие преобразования из строк JSON в объекты Kotlin.
-
Проверьте файл
build.gradle.kts. Соответствующий фрагмент кода уже должен существовать:plugins { // . . . kotlin("plugin.serialization") version "1.9.0" } dependencies { // . . . // Serialization implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") } -
В качестве подготовки к извлечению первого видео необходимо сообщить библиотеке сериализации о классе
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/dist. Они включают 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