この記事はtraP夏のブログリレー 9月20日の記事です。
こんにちは, RLookと申します。
夏休みも終りが近いですが, 夏休みどう過ごされましたか?
ぼくはだいたいKotlinを書いていたような気がします。KotlinFest 2019にも行ってきたので今年の夏はKotlinまみれでした。
そんな夏を締めくくるべく, 今日はKotlin + GradleでのReactアプリの作り方について簡単に説明します。
先行例
Kotlin + GradleでReactアプリを作った例としては, 次が一番いいのではないでしょうか。
rivasdiaz/react-tutorial-kotlin: An implementation of the React Tutorial using Kotlin
Playing with Kotlin React official wrapper - Ramon Rivas - Medium
実装の内容としては, Reactの公式のチュートリアルとなっています。
ただし, これはKotlin 1.1
時代のものですから, まずはKotlin 1.3
に上げるなどライブラリの更新と, Gradle Kotlin DSL
を導入します。
その前にこの例で使われているGradleプラグインについてもさらっと見ておきます。
Kotlin2JS
その名の通りKotlinをJavaScriptへとトランスパイルするために必要なプラグインです。
本当はorg.jetbrains.kotlin.js
を使ってみたかったのですが, 後述するkotlin-frontend-plugin
との競合が発生していたため, 今回はこちらで済ませます。
kotlin-frontend-plugin
Kotlin/kotlin-frontend-plugin: Gradle Kotlin (http://kotlinlang.org) plugin for frontend development
README.mdには,
The plugin provides an easy way to gather Maven and npm dependencies, pack bundles (via webpack) and test a frontend application using Karma. By default the plugin generates all required configs for webpack, karma and manages the corresponding daemons.
とあります。すなわちnpmの依存関係をまとめることができ, かつWebpack等を用いてbundle化するのを助けてくれるプラグインのようです。
また, Reactのアプリを開発する上で次のライブラリが重要となります。
Kotlin Wrappers
JetBrains/kotlin-wrappers: Kotlin wrappers for popular JavaScript libraries
このライブラリは名前の通りReactのラッパーです。README.mdに書いてあるとおり, React以外にも, ReduxやReact-Routerなどもあります。
build.gradle.kts
の記述
さて先述したように, まずはgroovyで書かれているbuild.gradle
を, Kotlin DSLを用いて書き直します。それに併せて, 各ライブラリを更新します。
次に変更例を示します。
import org.jetbrains.kotlin.gradle.frontend.webpack.WebPackExtension
import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile
val reactVersion = "16.9.0"
val reactWrapperVersion = "16.9.0-pre.82-kotlin-1.3.41"
buildscript {
val kotlinVersion by extra("1.3.50")
val kotlinFrontendVersion by extra("0.0.45")
repositories {
mavenCentral()
jcenter()
maven(url="https://dl.bintray.com/kotlin/kotlin-eap")
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
classpath("org.jetbrains.kotlin:kotlin-frontend-plugin:$kotlinFrontendVersion")
}
}
plugins {
id("kotlin2js") version "1.3.50"
id("org.jetbrains.kotlin.frontend") version "0.0.45"
}
repositories {
mavenCentral()
jcenter()
maven(url = "https://dl.bintray.com/kotlin/kotlin-js-wrappers")
}
dependencies {
implementation(kotlin("stdlib-js"))
implementation("org.jetbrains:kotlin-react:$reactWrapperVersion")
implementation("org.jetbrains:kotlin-react-dom:$reactWrapperVersion")
}
kotlinFrontend {
downloadNodeJsVersion = "latest"
npm {
dependency("react")
dependency("react-dom")
}
bundle<WebPackExtension>("webpack") {
this as WebPackExtension
bundleName = "tictactoe"
contentPath = file("src/main/web")
}
}
gradle.projectsEvaluated {
tasks.withType(Kotlin2JsCompile::class) {
kotlinOptions {
moduleKind = "commonjs"
}
}
}
ポイントとしては, メソッドwebpackBundle
が存在しないため, 代わりにbundle
を使う必要があることです。ただし, このときラムダ式内のthis
はWebPackExtension
のスーパークラスであるBundleConfig
となっているため, キャストを挟む必要があります。今回はラムダ式内1行目にて一見無駄なキャストを挟むことで, スマートキャストを働かせています。
このように変更してから, Gradleタスクrun
を実行すると, 次のエラーが発生します。
FAILURE: Build failed with an exception.
* What went wrong:
A problem was found with the configuration of task ':webpack-config'.
> Directory '$PROJECT_DIR\react-tutorial-kotlin\webpack.config.d' specified for property '$1' does not exist.
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
* Get more help at https://help.gradle.org
ここでは, プロジェクトのルートにwebpack.config.d
という名のディレクトリを追加するとエラーを解消することができます。
この後もう一度run
をすると, ようやく開発用のサーバが立ち上がります。
今回はReactチュートリアルではおなじみのマルバツゲームが立ち上がるはずです。
KotlinでのReactアプリの実装
簡単にJavaScriptによる実装と比較しながら, KotlinでのReactの書き方を確認しましょう。
コンポーネントの宣言
JSでの実装は通常次のようになります。
class Hoge extends React.Component {
render() {
return (...)
}
foo() {
this.setState({
x: 30
})
}
}
一方でkotlinでは次のようになります。
class Hoge: RComponent<Props, Status>() {
override fun RBuilder.render() {
...
}
private fun foo() {
setState {
x = 30
}
}
}
interface Props: RProps { ... }
interface Status: RState { ... }
Kotlinによる実装では, propsとstateがどのインターフェースであるかを宣言する必要があります。これはTypeScriptによる実装に近いですが, 違いとしてはpropsとして使うインターフェースはRProps
を実装する必要があり, 同様にstateとして使うインターフェースはRState
を実装する必要があります。
propsやstateを使わない場合にもこれらを指定する必要があり, この場合先行例ではそれぞれRProps
とRState
をそのまま指定していますが, Nothing
で代えることもできます。
またrender
メソッドに関しては, KotlinではRBuilder
の拡張関数として宣言されています。
setState
メソッドに関しては, ラムダ式中で各フィールドに代入するように書くことができ, Kotlinとして自然に書くことができます。
コンポーネントの呼び出し
例えばGame
のrender
では次のように書かれています。
<Board
squares={current.squares}
onClick={i => this.handleClick(i)}
/>
一方で, KotlinではJSXのようにはいかないため, 次のように書く必要があります。
class Hoge: RComponent<...> {
override fun RBuilder.render() {
board(current.squares) {
handleClick(it)
}
}
}
fun RBuilder.board(squares: Array<String?>, onClickFunction: (Int) -> Unit) = child(Board::class) {
attrs.squares = squares
attrs.onClickFunction = onClickFunction
}
このようにコンポーネントを用意するたびに, 適当な拡張関数をもってpropsが渡るように書く必要が生じます。
エントリポイント
チュートリアル中では次のように書かれています。
ReactDOM.render(<Game />, document.getElementById("root"));
一方でKotlinでの実装は次のようになります。
fun main() {
window.onload = {
document.getElementById("root")?.let {
render(it) { child(Game::class) {} }
}
}
}
このようにしてReactのアプリを構築していきます。
おわりに
簡単にではありましたが, KotlinでReactのアプリを作る手順を示しました。
現状ではまだまだ難しい部分が多いですが, Kotlin-WrappersはJetBrainsから(Team Projectではあるが)出ていますので, これからの発展が望まれるところです。
明日の担当はHinaruhiさんです。お楽しみに。