翻訳者から。
ねえ!ステレオタイプがありますについてKotlinそれが唯一のAndroidのための開発のための言語です。実際、これはまったく当てはまりません。この言語は、いくつかのプラットフォーム(JVM、JS、Native)を公式にサポートしており、他の言語で記述されたこれらのプラットフォームのライブラリを操作する方法も知っています。このような「マルチプラットフォーム性」のサポートにより、あらゆる種類のプロジェクトを1つの言語で単一の形式で作成できるだけでなく、異なるプラットフォーム用に1つのプロジェクトを作成するときにコードを再利用することもできます。
この記事では、KotlinでWebサイトを構築するための公式のKotlinハンズオンチュートリアルを翻訳しています。Kotlin / JSプログラミングの多くの側面をカバーし、純粋なDOM以上のものを操作する方法を理解します。主にReactJSについて説明しますが、NPMからの依存関係を使用し、REST APIを呼び出し、Herokuにデプロイし、最終的にビデオプレーヤーアプリケーションを作成するGradleビルドシステムについても触れます。
このテキストは、Kotlinを少し知っていて、Reactを知らないかほとんど知らない人を対象としています。これらの問題についてより経験がある場合は、チュートリアルの一部が過度に噛まれているように見える場合があります。
1.
, Kotlin/JS React . React , . , .
React , - . JavaScript.
Kotlin/JS React, Gradle org.jetbrains.kotlin.js
. , React .
, - (DSL) , . , , .
, , HTML CSS. , .
KotlinConf , . KotlinConf 2018 - 1300 . YouTube, – "". – KotlinConf Explorer (. ).
, .
2.
, , . , – IntelliJ IDEA ( 2020.3
, Community Edition) (1.4.30
) – . , ( Windows, MacOS Linux).
, .
GitHub IntelliJ IDEA (, File | New | Project from Version Control... Git | Clone...).
Kotlin/JS Gradle , - . Gradle , .
, , .
: , , Gradle , – .
Gradle
React, , . Gradle , .
, build.gradle.kts
repositories
. .
dependencies
:
dependencies {
// React, React DOM + Wrappers ( 3)
implementation("org.jetbrains:kotlin-react:17.0.1-pre.148-kotlin-1.4.21")
implementation("org.jetbrains:kotlin-react-dom:17.0.1-pre.148-kotlin-1.4.21")
implementation(npm("react", "17.0.1"))
implementation(npm("react-dom", "17.0.1"))
// Kotlin Styled ( 3)
implementation("org.jetbrains:kotlin-styled:5.2.1-pre.148-kotlin-1.4.21")
implementation(npm("styled-components", "~5.2.1"))
// Video Player ( 7)
implementation(npm("react-youtube-lite", "1.0.1"))
// Share Buttons ( 7)
implementation(npm("react-share", "~4.2.1"))
// Coroutines ( 8)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3")
}
, IDEA Gradle . , Reimport All Gradle Projects - Gradle ( ).
HTML
JavaScript , JS HTML , . src/main/resources/index.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 Gradle , ("") JavaScript , . HTML confexplorer.js
(, , , followingAlong
, followingAlong.js
).
JavaScript, ( #root
) . , , .
: HTML, , onLoad
body
. Kotlin/JS body
.
"Hello, World" , – , . , . src/main/kotlin/Main.kt
:
import kotlinx.browser.document
fun main() {
document.bgColor = "red"
}
.
Kotlin/JS Gradle webpack-dev-server, IDE .
, run
browserDevelopmentRun
- Gradle. other
( ), kotlin browser
:
IDE, , ./gradlew run
( Windows Gradle -: .\gradlew.bat run
).
, , , :
(hot reload) a.k.a.
, – Kotlin/JS . run
Gradle.
, ( IDE – Stop; – Ctrl+C
).
IDEA, . IDEA , Gradle , :
Run/Debug Configurations --continuous
:
Run (|>
) .
, : ./gradlew run --continuous
.
, Gradle . , :
document.bgColor = "blue"
, , – .
. . , .
, , , . -, , - . -, , . -, - , , Gradle , – - .
, Kotlin/JS, . , : HTML . browserDevelopmentWebpack
, build/distributions
build/developmentExecutable
. index.html
, .
, ...
Kotlin/JS , . !
master
.
3. –
Hello, World. .
src/main/kotlin/Main.kt
:
import react.dom.*
import kotlinx.browser.document
fun main() {
render(document.getElementById("root")) {
h1 {
+"Hello, React+Kotlin/JS!"
}
}
}
:
, ! , . render
kotlin-react-dom ( ) . , src/main/resources/index.html
ID root
, . – . , HTML , DSL.
HTML
kotlin-react DSL, HTML . , DSL .
, . , - , , !
+
:
– +
. . h1
– , . +
, unaryPlus
, HTML .
, +
" ".
HTML
, , () HTML. HTML, . , HTML:
<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>
Kotlin DSL. . , , :
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 {
attrs {
src = "https://via.placeholder.com/640x360.png?text=Video+Player+Placeholder"
}
}
}
render
. IntelliJ IDEA , (quick-fixes) Alt+Enter
. , :
HTML DSL HTML. – , . , , , – HTML DSL , .
- . KotlinVideo
, ( Main.kt
, – ), external
– , API:
external interface Video {
val id: Int
val title: String
val speaker: String
val videoUrl: String
}
data class KotlinVideo(
override val id: Int,
override val title: String,
override val speaker: String,
override val videoUrl: String
) : Video
: . Main.kt
:
val unwatchedVideos = listOf(
KotlinVideo(1, "Building and breaking things", "John Doe", "https://youtu.be/PsaFVLr8t4E"),
KotlinVideo(2, "The development process", "Jane Smith", "https://youtu.be/PsaFVLr8t4E"),
KotlinVideo(3, "The Web 7.0", "Matt Miller", "https://youtu.be/PsaFVLr8t4E")
)
val watchedVideos = listOf(
KotlinVideo(4, "Mouseless development", "Tom Jerry", "https://youtu.be/PsaFVLr8t4E")
)
HTML, , ! HTML . p
, :
for (video in unwatchedVideos) {
p {
+"${video.speaker}: ${video.title}"
}
}
watchedVideos
. , . , , , , .
CSS
, , : , . - .css
index.html
, , Kotlin DSL – CSS.
kotlin-styled styled-components , . CSS-in-JS. , , .
CSS DSL, Gradle. :
dependencies {
//...
// Kotlin Styled ( 3)
implementation("org.jetbrains:kotlin-styled:5.2.1-pre.148-kotlin-1.4.21")
implementation(npm("styled-components", "~5.2.1"))
//...
}
div
h3
styled
, , styledDiv
styledH3
. css
. , , :
styledDiv {
css {
position = Position.absolute
top = 10.px
right = 10.px
}
h3 {
+"John Doe: Building and breaking things"
}
img {
attrs {
src = "https://via.placeholder.com/640x360.png?text=Video+Player+Placeholder"
}
}
}
, IDEA . , :
import kotlinx.css.*
import styled.*
Alt+Enter
.
. – , . CSS Grids, ( ). ( fontFamily
) ( sans-serif
), , , ( color
).
step-02-first-static-page
.
4. React – .
. , , . , , / .
, . , :
, , :
. App
, . App.kt
src/main/kotlin
. App
, RComponent
( React Component). (RProps
RState
), :
import react.*
@JsExport
class App : RComponent<RProps, RState>() {
override fun RBuilder.render() {
// HTML!
}
}
HTML render
. . main
- App
. : App
, child
:
fun main() {
render(document.getElementById("root")) {
child(App::class) {}
}
}
? , – . , , .
VideoList.kt
. App
, VideoList
, RComponent
HTML DSL unwatchedVideos
:
import react.*
import react.dom.*
@JsExport
class VideoList : RComponent<RProps, RState>() {
override fun RBuilder.render() {
for (video in unwatchedVideos) {
p {
+"${video.speaker}: ${video.title}"
}
}
}
}
App
:
div {
h3 {
+"Videos to watch"
}
child(VideoList::class) {}
h3 {
+"Videos watched"
}
child(VideoList::class) {}
}
: App
. . , .
, - . , , . props
. , .
, . . VideoList.kt
:
external interface VideoListProps : RProps {
var videos: List<Video>
}
VideoList
, :
@JsExport
class VideoList : RComponent<VideoListProps, RState>() {
override fun RBuilder.render() {
for (video in props.videos) {
p {
key = video.id.toString()
+"${video.speaker}: ${video.title}"
}
}
}
}
, VideoList
( App
) . unwatchedVideos
watchedVideos
:
child(VideoList::class) {
attrs.videos = unwatchedVideos
}
, , . , . , .
fun RBuilder.videoList(handler: VideoListProps.() -> Unit): ReactElement {
return child(VideoList::class) {
attrs.handler()
}
}
, : videoList
RBuilder
. handler
– - VideoListProps
, Unit
. child
( VideoList
), handler
attrs
.
– :
videoList { videos = unwatchedVideos }
, child
, class
attrs
, . , . ! App
.
- – . , . : alert
.
VideoList.render
. , p
:
p {
key = video.id.toString()
attrs {
onClickFunction = {
window.alert("Clicked $video!")
}
}
+"${video.speaker}: ${video.title}"
}
IntelliJ IDEA , Alt+Enter
. :
import kotlinx.html.js.onClickFunction
import kotlinx.browser.window
:
onClickFunction
, . Kotlin/JS . . ,onClickFunction
.
?
. (|>
). – . – :
external interface VideoListState : RState {
var selectedVideo: Video?
}
:
-
VideoList
,VideoListState
–RComponent<..., VideoListState>
. - .
-
onClickFunction
selectedVideo
, . ,setState
.
, :
@JsExport
class VideoList : RComponent<VideoListProps, VideoListState>() {
override fun RBuilder.render() {
for (video in props.videos) {
p {
key = video.id.toString()
attrs {
onClickFunction = {
setState {
selectedVideo = video
}
}
}
if (video == state.selectedVideo) {
+"|> "
}
+"${video.speaker}: ${video.title}"
}
}
}
}
setState
. UI .
, React FAQ.
step-03-first-component
.
5. .
. , , . , :)
-, – , . ( ) . ( , "" ).
-, : . , . , , . . ! , App
. , VideoList
.
:
external interface AppState : RState {
var currentVideo: Video?
}
App
:
@JsExport
class App : RComponent<RProps, AppState>()
VideoListState
, . , , :
@JsExport
class VideoList : RComponent<VideoListProps, RState>()
App
VideoList
. VideoListProps
, :
external interface VideoListProps : RProps {
var videos: List<Video>
var selectedVideo: Video?
}
, :
if (video == props.selectedVideo) {
+"|> "
}
, : , setState
onClickFunction
. , - .
, , . -: . , ? – , Video
Unit
:
external interface VideoListProps : RProps {
var videos: List<Video>
var selectedVideo: Video?
var onSelectVideo: (Video) -> Unit
}
onClickFunction
:
onClickFunction = { props.onSelectVideo(video) }
, . , . videoList
:
videoList { videos = unwatchedVideos selectedVideo = state.currentVideo onSelectVideo = { video -> setState { currentVideo = video } } }
watchedVideos
.
, : , , . , , .
step-04-composing-components
.
6. !
, . .
, – ( -). , : , . , Video
, . VideoPlayer
VideoPlayer.kt
:
import kotlinx.css.*
import kotlinx.html.js.onClickFunction
import react.*
import react.dom.*
import styled.*
external interface VideoPlayerProps : RProps {
var video: Video
}
@JsExport
class VideoPlayer : RComponent<VideoPlayerProps, RState>() {
override fun RBuilder.render() {
styledDiv {
css {
position = Position.absolute
top = 10.px
right = 10.px
}
h3 {
+"${props.video.speaker}: ${props.video.title}"
}
img {
attrs {
src = "https://via.placeholder.com/640x360.png?text=Video+Player+Placeholder"
}
}
}
}
}
fun RBuilder.videoPlayer(handler: VideoPlayerProps.() -> Unit): ReactElement {
return child(VideoPlayer::class) {
this.attrs(handler)
}
}
styledDiv
( App.kt
) . , - – let
, let
, currentVideo
null
:
state.currentVideo?.let { currentVideo -> videoPlayer { video = currentVideo } }
. VideoPlayer
.
, VideoPlayer
. , .
-. , , , , . .
VideoPlayerProps
:
external interface VideoPlayerProps : RProps {
var video: Video
var onWatchedButtonPressed: (Video) -> Unit
var unwatchedVideo: Boolean
}
, . CSS : . HTML DSL render
, h3
img
:
styledButton {
css {
display = Display.block
backgroundColor = if (props.unwatchedVideo) Color.lightGreen else Color.red
}
attrs {
onClickFunction = {
props.onWatchedButtonPressed(props.video)
}
}
if (props.unwatchedVideo) {
+"Mark as watched"
} else {
+"Mark as unwatched"
}
}
VideoPlayer
, .
unwatched
watched
, .
. ! :
external interface AppState : RState {
var currentVideo: Video?
var unwatchedVideos: List<Video>
var watchedVideos: List<Video>
}
init
. , App
:
override fun AppState.init() {
unwatchedVideos = listOf(
KotlinVideo(1, "Building and breaking things", "John Doe", "https://youtu.be/PsaFVLr8t4E"),
KotlinVideo(2, "The development process", "Jane Smith", "https://youtu.be/PsaFVLr8t4E"),
KotlinVideo(3, "The Web 7.0", "Matt Miller", "https://youtu.be/PsaFVLr8t4E")
)
watchedVideos = listOf(
KotlinVideo(4, "Mouseless development", "Tom Jerry", "https://youtu.be/PsaFVLr8t4E")
)
}
unwatchedVideos
watchedVideos
Main.kt
, Main.kt
(un
)watchedVideos
, IDE , state.
(un
)watchedVideos
.
, . :
videoPlayer {
video = currentVideo
unwatchedVideo = currentVideo in state.unwatchedVideos
onWatchedButtonPressed = {
if (video in state.unwatchedVideos) {
setState {
unwatchedVideos -= video
watchedVideos += video
}
} else {
setState {
watchedVideos -= video
unwatchedVideos += video
}
}
}
}
, , , .
, . , , , . !
. .
step-05-more-components
.
7. NPM
, . , , . , , .
– .
, . react-youtube-lite
. API README.
. react-youtube-lite
, Gradle. :
dependencies {
// ...
// Video Player ( 7)
implementation(npm("react-youtube-lite", "1.0.1"))
// ...
}
– NPM Gradle npm
. yarn
, Kotlin/JS Gradle , , .
NPM , : , . , IDE . . ReactYouTube.kt
:
@file:JsModule("react-youtube-lite")
@file:JsNonModule
import react.*
@JsName("ReactYouTubeLite")
external val reactPlayer: RClass<dynamic>
JavaScript – , , . – require("react-youtube-lite").default
JS. : " , , RClass<dynamic>
".
, , . dynamic
, . , , - (, ).
, , ( external
), README . – . , – . :
@file:JsModule("react-youtube-lite")
@file:JsNonModule
import react.*
@JsName("ReactYouTubeLite")
external val reactPlayer: RClass<ReactYouTubeProps>
external interface ReactYouTubeProps : RProps {
var url: String
}
VideoPlayer
! img
:
reactPlayer { attrs.url = props.video.videoUrl }
KotlinConf ( ). – . , , . , , react-share. Gradle:
dependencies {
// ...
// Share Buttons ( 7)
implementation(npm("react-share", "~4.2.1"))
// ...
}
. , , : , EmailShareButton
EmailIcon
. . ; ReactShare.kt
:
@file:JsModule("react-share")
@file:JsNonModule
import react.RClass
import react.RProps
@JsName("EmailIcon")
external val emailIcon: RClass<IconProps>
@JsName("EmailShareButton")
external val emailShareButton: RClass<ShareButtonProps>
@JsName("TelegramIcon")
external val telegramIcon: RClass<IconProps>
@JsName("TelegramShareButton")
external val telegramShareButton: RClass<ShareButtonProps>
external interface ShareButtonProps : RProps {
var url: String
}
external interface IconProps : RProps {
var size: Int
var round: Boolean
}
. reactPlayer
( styledDiv
, ):
styledDiv {
css {
display = Display.flex
marginBottom = 10.px
}
emailShareButton {
attrs.url = props.video.videoUrl
emailIcon {
attrs.size = 32
attrs.round = true
}
}
telegramShareButton {
attrs.url = props.video.videoUrl
telegramIcon {
attrs.size = 32
attrs.round = true
}
}
}
, . , . , , .
, .
step-06-packages-from-npm
.
8. REST API
, . , REST API.
API, https://my-json-server.typicode.com/kotlin-hands-on/kotlinconf-json/videos/1. API – videos
, . API . , , Video
( ;)
). , .
JS
. Kotlin/JS , . Fetch API, HTTP REST API.
JavaScript – . , , . , - . , , , . , :
window.fetch("https://url...").then {
it.json().then {
it.unsafeCast<Video>()
//...
}
}
. , .
(structured concurrency) – . , . . .
, Gradle :
dependencies {
//...
// Coroutines ( 8)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9")
}
, !
App.kt
, , REST API:
suspend fun fetchVideo(id: Int): Video {
val response = window
.fetch("https://my-json-server.typicode.com/kotlin-hands-on/kotlinconf-json/videos/$id")
.await()
.json()
.await()
return response as Video
}
. :
import kotlinx.browser.window
import kotlinx.coroutines.*
, suspend . fetch
, id
API. (await
), JSON, . external interface Video
. , IDE – JavaScript fetch
: , Video
. . : , @Suppress
, unsafeCast
(response.unsafeCast<Video>()
).
. window.fetch
json
. , . , (await
) . , , . await
, ( suspend
). , .
suspend
, , 25. fetchVideos
, 25 . , suspend – async
. :
suspend fun fetchVideos(): List<Video> = coroutineScope {
(1..25).map { id ->
async {
fetchVideo(id)
}
}.awaitAll()
}
. , init
App
:
override fun AppState.init() {
unwatchedVideos = listOf()
watchedVideos = listOf()
val mainScope = MainScope()
mainScope.launch {
val videos = fetchVideos()
setState {
unwatchedVideos = videos
}
}
}
, init
, setState
unwatchedVideos
. - , , , unwatchedVideos
. setState
, , .
:
. , "Hello, World" .
, , , , .
step-07-using-external-rest-api
.
9.
.
, Gradle build
- IntelliJ IDEA ./gradlew build
. , , , DCE (dead code elimination – ).
, build/distributions
. JS , HTML , . , , , HTTP , GitHub Pages .
Heroku
Heroku . ; , .
git init heroku create git add . git commit -m "initial commit"
JVM , Heroku (, Ktor Spring Boot), , . Heroku:
heroku buildpacks:set heroku/gradle heroku buildpacks:add https://github.com/heroku/heroku-buildpack-static.git
heroku/gradle
stage
Gradle . , build
, , :
// Heroku Deployment ( 9)
tasks.register("stage") {
dependsOn("build")
}
buildpack-static
, static.json
. root
:
{
"root": "build/distributions"
}
, , :
git add -A git commit -m "add stage task and static content root configuration" git push heroku master
master (, ,step*
), , master Heroku (, :git push heroku step-08-deploying-to-production:master
).
, , !
final
.
10. :
- , .
, , – state effect. , .
, . , . – this
. , , :
external interface WelcomeProps : RProps {
var name: String
}
val welcome = functionalComponent<WelcomeProps> { props ->
h1 {
+"Hello, ${props.name}"
}
}
, external interface
. . functionalComponent
render
.
: child
:
child(welcome) {
attrs.name = "Kotlin"
}
:
fun RBuilder.welcome(handler: WelcomeProps.() -> Unit) = child(welcome) {
attrs.handler()
}
4. welcome { name = "Kotlin" }
.
, - . .
State
, . :
val counter = functionalComponent<RProps> {
val (count, setCount) = useState(0)
button {
attrs.onClickFunction = { setCount(count + 1) }
+"$count"
}
}
, :
useState
0
–Int
. , , (useState<String?>(null)
).-
useState
, :
- (
count
Int
); - (
setCount
RSetState<Int> /* = (Int) -> Unit */
).
- (
- ,
setState
.
, count
, . , , , .
: useState
– -. , , :
val counter = functionalComponent<RProps> {
var count by useState(0)
button {
attrs.onClickFunction = { ++count }
+"$count"
}
}
Effect
, - – API WebSocket . , h3
:
val randomFact = functionalComponent<RProps> {
val (randomFact, setRandomFact) = useState<String?>(null)
useEffect(emptyList()) {
GlobalScope.launch {
val fortyTwoFact = window.fetch("http://numbersapi.com/42").await().text().await()
setRandomFact(fortyTwoFact)
}
}
h3 { +(randomFact ?: "Fetching...") }
}
, , , . useEffect
, setRandomFact
.
, useEffect
. – , – . , , useEffect
. API . .
, setRandomFact
, .
, - , , videoList
, . useState
, useEffect
API, 8.
, .
11. ?
, . , Kotlin/JS .
. , . , HTML , .
. , -. - , ( Ktor), , . - .
APIs
:
, , . CSS (grids) ( : ).
kotlin-wrappers JS , . ( ):
,
– YouTrack. , . Slack. , #javascript
#react
.
, !
, ! Kotlin/JS , JS, JSX – , , .
, , Kotlin DSL. JSX , Kotlin DSL , . , , , – . , , . Kotlin/JS !