概要
この記事では、KotlinでReactアプリを書くときに使うライブラリの実装についてみていきます。Reactについては詳しく説明しません。
実装を読む気持ちとしては、React用のKotlinDSLがどのように実装されているか気になる、Kotlinのコードを読んでKotlinに関する知見を深めるといったことがあります。
はじめに
はじめてKotlinでReactアプリを書く場合、create-react-kotlin-appというnodeモジュールを利用することで簡単にサンプルプロジェクトを作成できます。
kotlin-wrappersの中にある、kotlin-reactとkotlin-react-domというKotlinのライブラリを使うことで、Kotlinらしい書き方ができます。
以下は、create-react-kotlin-appで作成したサンプルプロジェクトのソースコードの抜粋です。(紙面の都合上、複数ファイルに書いてあるものを一箇所に書いています)
fun main(args: Array<String>) {
render(document.getElementById("root")) { // -- (1)
app()
}
}
class App : RComponent<RProps, RState>() {
override fun RBuilder.render() {
div("App-header") { // -- (2)
key = "header"
logo()
h2 {
+"Welcome to React with Kotlin" // -- (3)
}
}
p("App-intro") {
key = "intro"
+"To get started, edit "
code { +"app/App.kt" }
+" and save to reload."
}
p("App-ticker") {
key = "ticker"
ticker()
}
}
}
fun RBuilder.app() = child(App::class) {}
JavascriptでReactアプリを実装する場合は、render関数内でJSXというHTMLのようなXMLを記述しますが、Kotlinの場合は、すべてKotlinのDSLで記述できていることが分かりますね。
このようなDSLで記述にするためにライブラリの中はどのような実装になっているのでしょうか?
この記事では、以下の三点について見ていきます。
(1) main関数内のrender関数は何をやってるの?
(2) div関数の中はどうなっているの?
(3) 文字列の前についている+
ってなに?
ライブラリのソースを読む
登場人物
ソースを読む前に、よく出てくるクラスを列挙しておきます。
クラス名 | 役割 |
---|---|
RBuilder | ビルダー用の関数群をもつ。 |
RDOMBuilder | React用DOMのビルダー。RBuilderを継承している。createするとReactElementが取れる。 |
ReactElement | Reactの要素。external修飾子付き。 |
(1) main関数内のrender関数は何をやってるの?
はじめにrender関数です。
fun render(container: Element?, handler: RBuilder.() -> Unit) = render(buildElements(handler), container)
第一引数にJavaScriptのElementを、第二引数にRBuilderの関数を受け取ります。
受け取った引数を別のrender関数に渡しています。
buildElements関数は、elementを作って返却しています。
@file:JsModule("react-dom")
external fun render(element: dynamic, container: Element?, callback: () -> Unit = definedExternally)
こちらのrender関数には、external修飾子がついています。
react-dom
というJavaScriptのモジュールで実装されていて、Kotlinでの実装は存在しないです。実行時は、JavaScriptのReactDOM.renderが呼ばれます。
render関数についてまとめると、elementを作って、ReactDOM.renderに渡しています。
ReactDOM.renderにはexternal修飾子がついていて、JavaScript側の実装で処理されます。
(2) div関数の中はどうなっているの?
続いてdivです。
inline fun RBuilder.div(classes: String? = null, block: RDOMBuilder<DIV>.() -> Unit): ReactElement =
tag(block) { DIV(attributesMapOf("class", classes), it) }
第一引数にクラス名を、第二引数にRDOMBuilderの関数を受け取り、ReactElementを返却します。
実装は、tag関数にRDOMBuilder.() -> Unit
と{ DIV(...) }
を渡しています。
inline fun <T : Tag> RBuilder.tag(block: RDOMBuilder<T>.() -> Unit, noinline factory: (TagConsumer<Unit>) -> T): ReactElement =
child(RDOMBuilder(factory).apply { block() }.create())
tag関数ですが、RDOMBuilderにfactoryを渡して、block(RDOMBuilder.() -> Unit
)を実行したものを、createして子要素として追加しています。
以下のコードの場合、"Hello"
がh2の子要素になり、h2がdivの子要素となることが分かると思います。
div("App-header") {
h2 { +"Hello" }
}
さて、まだ<div>
というHTMLの要素が出てきてないですね。
RBuilder.divはどのように<div>
になるのでしょうか。
open fun create(): ReactElement =
React.createElement(attrs.tagName, props, childList.toTypedArray())
登場人物のところで、RDOMBuilderをcreateするとReactElementが取れると書きましたが、実装上は、上記のようにReact.createElement
を呼ぶ実装になっています。
React.createElement
はexternal修飾子がついた関数で、以下のような仕様です。
React.createElement(
type,
[props],
[...children]
)
引用元: https://reactjs.org/docs/react-api.html#createelement
typeにはdiv
やspan
のようなタグ名が入るとあるので、attrs.tagName
にdiv
が入っていると想像がつきます。そして、ReactJS側で<div>
になるはずです。
open class DIV(initialAttributes : Map<String, String>, override val consumer : TagConsumer<*>)
: HTMLTag("div", consumer, initialAttributes, null, false, false), HtmlBlockTag { }
open class HTMLTag(
override val tagName : String,
override val consumer : TagConsumer<*>,
initialAttributes : Map<String, String>,
override val namespace : String? = null,
override val inlineTag: Boolean,
override val emptyTag: Boolean
) : Tag {
override val attributes : DelegatingMap = DelegatingMap(initialAttributes, this) { consumer }
override val attributesEntries: Collection<Map.Entry<String, String>>
get() = attributes.immutableEntries
}
実際にコードを読むと、DIVというクラスが、HTMLTagのtagNameにdiv
を入れたクラスになっていて、RDOMBuilderにtagNameが渡っていることが分かります。
ここでは、div関数の実装について見てきました。
RBuilderの関数が、RDOMBuilder<T>.() -> Unit
を引数に取るようになっていて、HTMLの構造をラムダでうまく表現できるようになっていました。
(3) 文字列の前についている+
ってなに?
最後に小ネタですが、なぜ文字列の前に+
をつけているのでしょうか。
operator fun String.unaryPlus() {
childList.add(this)
}
operator overloadingによって、Stringの前に+
をつけると、子要素として追加される実装になっています。
operator overloadingは全く使ったことがなかったですが、実際に使われているのを見ると書いてみたくなりますね。
まとめ
この記事では、KotlinのReact用wrapperの実装をみてきました。
ライブラリの実装を読むことで、「Kotlinでこんなこともできるんだ!」というような新たな発見があって面白いです。
Kotlinの仕様を読めば書いてあることばかりですが、実際にどういう場面で使われるのかが分かるとさらにKotlinらしいコードが書けるようになる手助けになることでしょう。