使用 React 和 Kotlin/JS 构建 Web 应用程序 — 教程
本教程将教你如何使用 Kotlin/JS 和 React 框架构建浏览器应用程序。你将:
- 完成构建典型
React
应用程序的常见任务。 - 探索
Kotlin
的 DSL 如何用于简洁统一地表达概念,同时不牺牲可读性,让你能够完全使用Kotlin
编写成熟的应用程序。 - 学习如何使用现成的
npm
组件、使用外部库以及发布最终应用程序。
最终输出将是一个 KotlinConf Explorer Web
应用,专用于 KotlinConf 活动,其中包含会议讲座的链接。用户将能够在一个页面上观看所有讲座,并将其标记为已观看或未观看。
本教程假设你已具备 Kotlin
的先验知识以及 HTML
和 CSS
的基本知识。理解 React
背后的基本概念可能有助于你理解一些示例代码,但并非严格要求。
你可以在此处获取最终应用程序。
开始之前
下载并安装最新版本的 IntelliJ IDEA。
克隆项目模板并在
IntelliJ IDEA
中打开它。该模板包含一个基本的Kotlin Multiplatform Gradle
项目,其中包含所有必需的配置和依赖项。build.gradle.kts
文件中的依赖项和任务:kotlindependencies { // 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") }
src/jsMain/resources/index.html
中的HTML
模板页面,用于插入本教程中将使用的JavaScript
代码:html<!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 惯例,body
的内容(包括root
div)会首先加载,以确保浏览器在脚本加载之前加载所有页面元素。
src/jsMain/kotlin/Main.kt
中的代码片段:kotlinimport kotlinx.browser.document fun main() { document.bgColor = "red" }
运行开发服务器
默认情况下,Kotlin Multiplatform Gradle
插件支持嵌入式 webpack-dev-server
,允许你无需手动设置任何服务器即可从 IDE
运行应用程序。
要测试程序是否成功在浏览器中运行,请在 IntelliJ IDEA
的 Gradle
工具窗口中调用 run
或 browserDevelopmentRun
任务(可在 other
或 kotlin browser
目录中找到)来启动开发服务器:
要从终端运行程序,请使用 ./gradlew run
。
当项目编译并打包后,浏览器窗口中将出现一个空白的红色页面:
启用热重载 / 持续模式
配置 持续编译 模式,这样你就不必每次更改时都手动编译和执行项目。请务必在继续之前停止所有正在运行的开发服务器实例。
编辑
IntelliJ IDEA
在首次运行Gradle run
任务后自动生成的运行配置:在 Run/Debug Configurations 对话框中,为运行配置的实参添加
--continuous
选项:应用更改后,你可以使用
IntelliJ IDEA
内的 运行 按钮重新启动开发服务器。要从终端运行持续的Gradle
构建,请使用./gradlew run --continuous
。要测试此特性,请在
Gradle
任务运行时更改Main.kt
文件中的页面颜色为蓝色:kotlindocument.bgColor = "blue"
项目将重新编译,重新加载后浏览器页面将显示新颜色。
你可以在开发过程中保持开发服务器以持续模式运行。当你进行更改时,它会自动重建并重新加载页面。
你可以在
master
分支此处找到该项目状态。
创建 Web
应用草稿
使用 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
是一个接受lambda
形参的函数。当你在字符串字面量前添加+
号时,实际上是使用操作符重载调用了unaryPlus()
函数。它将字符串附加到封闭的HTML
元素中。
当项目重新编译时,浏览器将显示此 HTML
页面:
将 HTML
转换为 Kotlin
的类型安全 HTML DSL
React
的 Kotlin
封装器 带有一个领域特定语言 (DSL),使得用纯 Kotlin
代码编写 HTML
成为可能。这样,它类似于 JavaScript
中的 JSX。然而,由于这种标记是 Kotlin
,你将获得静态类型语言的所有优势,例如自动补全或类型检查。
比较你的未来 Web
应用的经典 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
代码并更新 main()
函数内的 Fragment.create()
函数调用,替换先前的 h1
标签。
等待浏览器重新加载。页面现在应该如下所示:
使用 Kotlin
结构在标记中添加视频
使用这种 DSL
在 Kotlin
中编写 HTML
有一些优势。你可以使用常规 Kotlin
结构(如循环、条件、集合和字符串内插)来操作你的应用。
现在,你可以将硬编码的视频列表替换为 Kotlin
对象列表:
在
Main.kt
中,创建一个Video
数据类 来将所有视频属性放在一个地方:kotlindata class Video( val id: Int, val title: String, val speaker: String, val videoUrl: String )
分别填充两个列表:未观看视频和已观看视频。将这些声明添加到
Main.kt
的文件级别:kotlinval 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
对象的集合。将“Videos to watch”下的三个p
标签替换为以下代码片段:kotlinfor (video in unwatchedVideos) { p { +"${video.speaker}: ${video.title}" } }
对“Videos watched”后的单个标签应用相同的过程:
kotlinfor (video in watchedVideos) { p { +"${video.speaker}: ${video.title}" } }
等待浏览器重新加载。布局应与之前保持不变。你可以向列表中添加更多视频以确保循环正常工作。
使用类型安全的 CSS
添加样式
kotlin-emotion 是 Emotion 库的封装器,它使得在 JavaScript
中可以直接与 HTML
一起指定 CSS
属性(甚至是动态属性)。概念上,这类似于 CSS-in-JS——但用于 Kotlin
。使用 DSL
的好处是你可以使用 Kotlin
代码结构来表达格式规则。
本教程的模板项目已经包含了使用 kotlin-emotion
所需的依赖项:
dependencies {
// ...
// Kotlin React Emotion (CSS) (chapter 3)
implementation("org.jetbrains.kotlin-wrappers:kotlin-emotion")
// ...
}
使用 kotlin-emotion
,你可以在 HTML
元素 div
和 h3
中指定一个 css
代码块,你可以在其中定义样式。
要将视频播放器移动到页面的右上角,请使用 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
或为 UI
添加一些 color
。
设计应用程序组件
React
中的基本构建块称为 组件。组件本身也可以由其他更小的组件组成。通过组合组件,你可以构建应用程序。如果你将组件结构设计成通用且可重用的,你将能够在应用程序的多个部分中使用它们,而无需重复代码或逻辑。
render()
函数的内容通常描述一个基本组件。你当前应用程序的布局如下所示:
如果你将应用程序分解为单个组件,你将得到一个更结构化的布局,其中每个组件处理其职责:
组件封装了特定的功能。使用组件可以缩短源代码,使其更易于阅读和理解。
添加主组件
要开始创建应用程序的结构,首先明确指定 App
,即渲染到 root
元素的主组件:
在
src/jsMain/kotlin
文件夹中创建新文件App.kt
。在该文件中,添加以下代码片段,并将
Main.kt
中的类型安全HTML
移入其中:kotlinimport 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()
函数:kotlinfun 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
列表中的代码。
在
src/jsMain/kotlin
文件夹中创建新文件VideoList.kt
并添加以下代码:kotlinimport 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
组件:kotlin// . . . div { h3 { +"Videos to watch" } VideoList() h3 { +"Videos watched" } VideoList() } // . . .
目前,
App
组件无法控制VideoList
组件显示的内容。它是硬编码的,所以你看到的是两次相同的列表。
添加 props
以在组件之间传递数据
由于你将重用 VideoList
组件,因此你需要能够用不同的内容填充它。你可以添加将项目列表作为属性传递给组件的功能。在 React
中,这些属性称为 props
。当 React
中组件的 props
发生更改时,框架会自动重新渲染组件。
对于 VideoList
,你需要一个包含要显示的视频列表的 prop
。定义一个包含可以传递给 VideoList
组件的所有 props
的接口:
将以下定义添加到
VideoList.kt
文件中:kotlinexternal interface VideoListProps : Props { var videos: List<Video> }
external
修饰符 告诉编译器该接口的实现是在外部提供的,因此它不会尝试从声明生成JavaScript
代码。调整
VideoList
的类定义,使其利用作为形参传递到FC
代码块中的props
:kotlinval VideoList = FC<VideoListProps> { props -> for (video in props.videos) { p { key = video.id.toString() +"${video.speaker}: ${video.title}" } } }
key
属性帮助React
渲染器在props.videos
的值更改时确定该做什么。它使用key
来确定列表的哪些部分需要刷新,哪些保持不变。你可以在 React 指南中找到有关列表和key
的更多信息。在
App
组件中,确保子组件使用适当的属性进行实例化。在App.kt
中,用VideoList
的调用以及unwatchedVideos
和watchedVideos
的属性替换h3
元素下方的两个循环。 在Kotlin DSL
中,你在VideoList
组件所属的块内为其赋值:kotlinh3 { +"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}"
}
// . . .
如果你点击浏览器窗口中的一个列表项,你将在警报窗口中获取有关视频的信息,如下所示:
直接将
onClick
函数定义为lambda
简洁且对于原型开发非常有用。然而,由于Kotlin/JS
中当前相等性的工作方式,从性能方面来看,这不是传递点击处理程序的最优化方式。如果你想优化渲染性能,请考虑将函数存储在变量中并传递它们。
添加状态以保留值
除了仅仅提醒用户之外,你还可以添加一些功能,用于用 ▶ 三角形突出显示选定的视频。为此,引入该组件特有的 状态。
状态是 React
中的核心概念之一。在现代 React
(使用所谓的 Hooks API)中,状态使用 useState
钩子 表示。
将以下代码添加到
VideoList
声明的顶部:kotlinval VideoList = FC<VideoListProps> { props -> var selectedVideo: Video? by useState(null) // . . .
VideoList
函数式组件保持状态(一个独立于当前函数调用的值)。状态是可空的,类型为Video?
。其默认值为null
。React
中的useState()
函数指示框架在函数的多次调用中跟踪状态。例如,即使你指定了默认值,React
也会确保默认值仅在开始时赋值。当状态改变时,组件将根据新状态重新渲染。by
关键字表示useState()
作为委托属性工作。与任何其他变量一样,你可以读取和写入值。useState()
背后的实现负责使状态工作所需的机制。
要了解有关状态钩子的更多信息,请查阅 React 文档。
更改
onClick
处理程序和VideoList
组件中的文本,使其如下所示:kotlinval 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 常见问题 中找到有关状态管理的更多详细信息。
检查浏览器并点击列表中的一个项目,确保一切正常工作。
组合组件
目前,两个视频列表独立工作,这意味着每个列表都跟踪一个选定的视频。用户可以同时选择两个视频,一个在未观看列表中,一个在已观看列表中,即使只有一个播放器:
一个列表不能同时跟踪自身内部和兄弟列表内部选择的视频。原因在于所选视频不属于 列表 状态,而是属于 应用程序 状态。这意味着你需要将状态从单个组件中 提升 出来。
状态提升
React
确保 props
只能从父组件传递给其子组件。这可以防止组件之间被硬连线在一起。
如果一个组件想要改变兄弟组件的状态,它需要通过其父组件来完成。此时,状态也不再属于任何子组件,而是属于整体父组件。
将状态从组件迁移到其父组件的过程称为 状态提升。对于你的应用,将 currentVideo
作为状态添加到 App
组件中:
在
App.kt
中,将以下内容和useState()
调用添加到App
组件的顶部:kotlinval App = FC<Props> { var currentVideo: Video? by useState(null) // . . . }
VideoList
组件不再需要跟踪状态。它将接收当前视频作为prop
。删除
VideoList.kt
中的useState()
调用。准备
VideoList
组件以接收选定的视频作为prop
。为此,扩展VideoListProps
接口以包含selectedVideo
:kotlinexternal interface VideoListProps : Props { var videos: List<Video> var selectedVideo: Video? }
更改三角形的条件,使其使用
props
而不是状态:kotlinif (video == props.selectedVideo) { +"▶ " }
传递处理程序
目前,没有办法为 prop
赋值,因此 onClick
函数将无法按当前设置的方式工作。要更改父组件的状态,你需要再次提升状态。
在 React
中,状态总是从父组件流向子组件。因此,要从一个子组件更改 应用程序 状态,你需要将处理用户交互的逻辑移动到父组件,然后将其作为 prop
传递。请记住,在 Kotlin
中,变量可以具有函数类型。
再次扩展
VideoListProps
接口,使其包含一个变量onSelectVideo
,该变量是一个接受Video
并返回Unit
的函数:kotlinexternal interface VideoListProps : Props { // ... var onSelectVideo: (Video) -> Unit }
在
VideoList
组件中,在onClick
处理程序中使用新的prop
:kotlinonClick = { props.onSelectVideo(video) }
你现在可以从
VideoList
组件中删除selectedVideo
变量。回到
App
组件,并为两个视频列表分别传递selectedVideo
和onSelectVideo
的处理程序:kotlinVideoList { videos = unwatchedVideos // and watchedVideos respectively selectedVideo = currentVideo onSelectVideo = { video -> currentVideo = video } }
对已观看视频列表重复上一步。
切换回浏览器,并确保在选择视频时,选择在两个列表之间跳转而不会重复。
添加更多组件
提取视频播放器组件
你现在可以创建另一个自包含的组件,一个视频播放器,它目前是一个占位图片。你的视频播放器需要知道讲座标题、讲座作者和视频链接。这些信息都已包含在每个 Video
对象中,因此你可以将其作为 prop
传递并访问其属性。
创建一个新文件
VideoPlayer.kt
并添加VideoPlayer
组件的以下实现:kotlinimport 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
中,将先前的视频播放器div
片段替换为以下内容:kotlincurrentVideo?.let { curr -> VideoPlayer { video = curr } }
let
作用域函数 确保仅当state.currentVideo
不为null
时才添加VideoPlayer
组件。
现在,点击列表中的条目将弹出视频播放器,并用点击条目的信息填充它。
添加按钮并连接它
为了让用户能够将视频标记为已观看或未观看,并在两个列表之间移动它,请在 VideoPlayer
组件中添加一个按钮。
由于此按钮将在两个不同的列表之间移动视频,因此处理状态更改的逻辑需要从 VideoPlayer
中 提升 出来,并从父组件作为 prop
传递。该按钮应根据视频是否已观看而显示不同的外观。这也是你需要作为 prop
传递的信息。
扩展
VideoPlayerProps
接口在VideoPlayer.kt
中以包含这两种情况的属性:kotlinexternal interface VideoPlayerProps : Props { var video: Video var onWatchedButtonPressed: (Video) -> Unit var unwatchedVideo: Boolean }
你现在可以将按钮添加到实际组件中。将以下代码片段复制到
VideoPlayer
组件的主体中,位于h3
和img
标签之间:kotlinbutton { 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
表达式更改按钮的颜色。
将视频列表移动到应用程序状态
现在是时候调整 App
组件中 VideoPlayer
的使用位置了。当按钮被点击时,视频应该从未观看列表移动到已观看列表,反之亦然。由于这些列表现在实际上可以更改,请将它们移动到应用程序状态中:
在
App.kt
中,将以下属性和useState()
调用添加到App
组件的顶部:kotlinval App = FC<Props> { var currentVideo: Video? by useState(null) var unwatchedVideos: List<Video> by useState(emptyList()) var watchedVideos: List<Video> by useState(emptyList()) // . . . }
由于所有演示数据都直接包含在
watchedVideos
和unwatchedVideos
的默认值中,因此你不再需要文件级声明。在Main.kt
中,删除watchedVideos
和unwatchedVideos
的声明。更改
App
组件中属于视频播放器的VideoPlayer
的调用点,使其如下所示:kotlinVideoPlayer { 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
播放器,请使用 npm
中的 react-player
包。它可以播放视频并允许你控制播放器的外观。
有关组件文档和 API
描述,请参阅其 GitHub
上的 README。
检查
build.gradle.kts
文件。react-player
包应该已经包含在内:kotlindependencies { // ... // Video Player implementation(npm("react-player", "2.12.0")) // ... }
如你所见,通过在构建文件的
dependencies
代码块中使用npm()
函数,可以将npm
依赖项添加到Kotlin/JS
项目中。然后,Gradle
插件会为你下载和安装这些依赖项。为此,它使用自己的捆绑安装的 Yarn 包管理器。要在
React
应用程序内部使用JavaScript
包,需要通过提供外部声明来告知Kotlin
编译器预期内容。创建一个新文件
ReactYouTube.kt
并添加以下内容:kotlin@file:JsModule("react-player") @file:JsNonModule import react.* @JsName("default") external val ReactPlayer: ComponentClass<dynamic>
当编译器看到像
ReactPlayer
这样的外部声明时,它会假定相应类的实现由依赖项提供,并且不会为其生成代码。最后两行等效于
JavaScript
导入,例如require("react-player").default;
。它们告诉编译器,在运行时某个组件肯定会符合ComponentClass<dynamic>
。
然而,在此配置中,ReactPlayer
接受的 props
的泛型类型设置为 dynamic
。这意味着编译器将接受任何代码,但存在运行时中断的风险。
一个更好的替代方案是创建一个 external interface
,它指定了此外部组件的 props
应该具有哪些属性。你可以在组件的 README 中了解 props
接口。在本例中,使用 url
和 controls
props
:
通过用外部接口替换
dynamic
来调整ReactYouTube.kt
的内容:kotlin@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
标签替换为以下代码片段:kotlinReactPlayer { url = props.video.videoUrl controls = true }
添加社交分享按钮
分享应用程序内容的简单方法是为即时通讯工具和电子邮件提供社交分享按钮。你也可以为此使用现成的 React
组件,例如 react-share:
检查
build.gradle.kts
文件。此npm
库应该已经包含在内:kotlindependencies { // ... // Share Buttons implementation(npm("react-share", "4.4.1")) // ... }
要从
Kotlin
使用react-share
,你需要编写更多基本的外部声明。GitHub
上的示例 表明分享按钮由两个React
组件组成:例如EmailShareButton
和EmailIcon
。不同类型的分享按钮和图标都具有相同的接口。 你将以与视频播放器相同的方式为每个组件创建外部声明。将以下代码添加到新的
ReactShare.kt
文件中:kotlin@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
中,在ReactPlayer
的使用位置上方添加两个分享按钮到div
中:kotlin// . . . 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
对象的结构相同。
从 Kotlin
使用 JS
功能
浏览器本身就附带了各种各样的 Web API。你也可以从 Kotlin/JS
中使用它们,因为它开箱即用地包含了这些 API
的封装器。一个例子是 fetch API,它用于发出 HTTP
请求。
第一个潜在问题是像 fetch()
这样的浏览器 API
使用回调来执行非阻塞操作。当多个回调应该一个接一个地运行时,它们需要嵌套。自然地,代码会深度缩进,越来越多的功能块相互堆叠,这使得阅读变得困难。
为了克服这个问题,你可以使用 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
文件。相应的代码片段应该已经存在:kotlinplugins { // . . . kotlin("plugin.serialization") version "2.2.10" } dependencies { // . . . // Serialization implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") }
为了获取第一个视频做准备,需要告知序列化库关于
Video
类的信息。在Main.kt
中,在其定义中添加@Serializable
注解:kotlin@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()
从API
获取给定id
的视频。此响应可能需要一段时间,因此你await()
结果。接下来,text()
(它使用回调)从响应中读取主体。然后你await()
其完成。 - 在返回函数的值之前,你将其传递给
Json.decodeFromString
,这是kotlinx.coroutines
中的一个函数。它将你从请求中收到的JSON
文本转换为具有适当字段的Kotlin
对象。 window.fetch
函数调用返回一个Promise
对象。你通常需要定义一个回调处理程序,一旦Promise
被解析且结果可用,该处理程序就会被调用。然而,有了协程,你可以await()
这些Promise
。每当调用像await()
这样的函数时,该方法就会停止(挂起)其执行。一旦Promise
可以被解析,它的执行就会继续。
为了向用户提供视频选择,定义 fetchVideos()
函数,它将从上述相同的 API
获取 25 个视频。为了并发运行所有请求,使用 Kotlin
协程提供的 async
功能:
将以下实现添加到你的
App.kt
:kotlinsuspend fun fetchVideos(): List<Video> = coroutineScope { (1..25).map { id -> async { fetchVideo(id) } }.awaitAll() }
遵循结构化并发的原则,实现被封装在一个
coroutineScope
中。然后你可以启动 25 个异步任务(每个请求一个)并等待它们全部完成。你现在可以将数据添加到你的应用程序中。添加
mainScope
的定义,并更改你的App
组件,使其以以下代码片段开始。不要忘记也将演示值替换为emptyLists
实例:kotlinval 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
代码块将_不会_第二次运行。
如果你想深入了解协程的工作原理,请查阅这篇协程教程。
部署到生产环境和云端
现在是时候将应用程序发布到云端,并使其可供其他人访问了。
打包生产构建
要以生产模式打包所有资产,请通过 IntelliJ IDEA
中的工具窗口或运行 ./gradlew build
来运行 Gradle
中的 build
任务。这会生成一个优化后的项目构建,应用各种改进,例如 DCE
(无用代码消除)。
构建完成后,你可以在 /build/dist
中找到部署所需的所有文件。它们包括 JavaScript
文件、HTML
文件和运行应用程序所需的其他资源。你可以将它们放在静态 HTTP
服务器上,使用 GitHub Pages
提供服务,或者将它们托管在你选择的云提供商上。
部署到 Heroku
Heroku
可以非常简单地启动一个可在其自己的域名下访问的应用程序。他们的免费套餐应该足以满足开发目的。
创建帐户。
在项目根目录中的终端中运行以下命令,创建
Git
仓库并附加Heroku
应用:bashgit init heroku create git add . git commit -m "initial commit"
与在
Heroku
上运行的常规JVM
应用程序(例如,用Ktor
或Spring Boot
编写的应用程序)不同,你的应用程序生成静态HTML
页面和JavaScript
文件,需要相应地提供服务。你可以调整所需的构建包以正确提供程序服务:bashheroku buildpacks:set heroku/gradle heroku buildpacks:add https://github.com/heroku/heroku-buildpack-static.git
为了让
heroku/gradle
构建包正常运行,build.gradle.kts
文件中需要有一个stage
任务。此任务等效于build
任务,相应的别名已包含在文件底部:kotlin// Heroku Deployment tasks.register("stage") { dependsOn("build") }
在项目根目录中添加一个新文件
static.json
来配置buildpack-static
。在文件内部添加
root
属性:xml{ "root": "build/distributions" }
你现在可以触发部署,例如,通过运行以下命令:
bashgit add -A git commit -m "add stage task and static content root configuration" git push heroku master
如果你从非
main
分支推送,请调整命令以推送到main
远程,例如git push heroku feature-branch:main
。
如果部署成功,你将看到人们可以用来访问互联网上应用程序的 URL
。
你可以在
finished
分支此处找到该项目的状态。
接下来
添加更多特性
你可以将生成的应用程序作为起点,探索 React
、Kotlin/JS
等领域中更高级的主题。
- 搜索。你可以添加一个搜索字段来过滤讲座列表——例如,按标题或作者。了解 HTML 表单元素在
React
中如何工作。 - 持久化。目前,每当页面重新加载时,应用程序都会丢失查看者的观看列表。考虑构建你自己的后端,使用
Kotlin
可用的Web
框架之一(例如 Ktor)。或者,研究在客户端存储信息的方法。 - 复杂
API
。有许多数据集和API
可用。你可以将各种数据拉入你的应用程序中。例如,你可以为猫咪照片或免版税图库照片API
构建一个可视化工具。
改进样式:响应式和网格
应用程序设计仍然非常简单,在移动设备或窄窗口中看起来不会很好。探索更多 CSS DSL
,使应用程序更具可访问性。
加入社区并获取帮助
报告问题和获取帮助的最佳方式是 kotlin-wrappers 问题追踪器。如果你找不到你的问题对应的工单,请随时提交一个新工单。你也可以加入 Kotlin Slack
官方频道:https://surveys.jetbrains.com/s3/kotlin-slack-sign-up。那里有 #javascript
和 #react
频道。
了解更多关于协程的信息
如果你对了解如何编写并发代码感兴趣,请查阅协程教程。
了解更多关于 React
的信息
现在你已经了解了基本的 React
概念以及它们如何转换为 Kotlin
,你可以将 React 文档 中概述的一些其他概念转换为 Kotlin
。