メモ2ブログ

メモtoウェブログ。旧ブログはこちら。 http://sakebook.blogspot.jp/

Kotlin/JSことはじめ

なんでもKotlinで解決したいと思いがちの筆者です。

Kotlin/JSを触ってみたので動かし方などまとめます。

環境は

  • Kotlin 1.3.61
  • Node 13.3.0

です

Setup

Gradle Plugin

kotlin js プラグインを使います。

  • build.gradle.kts
plugins {
    kotlin("js") version "1.3.61"
}

kotlin jsプラグインを使えば、settings.gradleでのpluginManagementは不要です。

Kotlin関連のプラグインやライブラリでは kotliin("XXX") という書き方をすることで記述をスッキリできます。中身はただの拡張関数です

Dependencies

kotlin stdlib-js ライブラリを使います。

  • build.gradle.kts
dependencies {
    implementation(kotlin("stdlib-js"))
}

Build configuration

フロント(Browser)向けのJSと、サーバ(Node)向けのJSがあるので、どちらでも動かせるようなJSを吐き出すようにします。

targetブロックではビルドする対象を指定し、tasksブロックでは既存taskのプロパティの変更を行っています。

  • build.gradle.kts
kotlin.target {
    nodejs()
    browser()
}

tasks {
    compileKotlinJs {
        kotlinOptions {
            moduleKind = "umd"
        }
    }
}

moduleKindに設定できる値は amd, commonjs , umd , plain とありますが、デフォルトは plain です。

フロントで使いたければ amd, サーバで使いたければ commonjs, 両方で使いたければ umd を選択する感じです。

今回はどちらでも動かすので umdを選択します。

main.kt

src フォルダに作成します。 - main.kt

fun main() {
    println("Hello Kotlin/JS!!")
}

Build

次のコマンドを実行します

$ ./gradlew build

build フォルダが生成され、次のような構成になります。

.
└── build
    ├── js
    │   ├── node_modules/
    │   ├── node_modules.state
    │   ├── package.json
    │   ├── packages/
    │   ├── packages_imported/
    │   └── yarn.lock
    ├── kotlin
    │   ├── compileKotlinJs/
    │   └── sessions/
    ├── libs
    │   ├── kotlin-js-example-0.0.1.jar
    │   └── kotlin-js-example-js-0.0.1-sources.jar
    ├── reports
    │   └── tests/
    └── tmp
        ├── JsJar/
        ├── expandedArchives/
        └── kotlinSourcesJar/

jsは、JavaScriptのルートフォルダのようなものです。js/packages にトランスパイルされたJSファイルが含まれています。js/packages_importedにはトランスパイルされたJSファイルが依存しているpackageが含まれています。

Run

Browser

動作確認用に適当なhtmlファイルを作成します。

プロジェクトルートに次のhtmlを作成します。

  • index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <script src="build/js/packages_imported/kotlin/1.3.61/kotlin.js"></script>
    <script src="build/js/packages/kotlin-js-example/kotlin/kotlin-js-example.js"></script>
</head>

<body>
<p>Kotlin/JS example</p>
</body>
</html>

生成したJSファイルはkotlin.jsに依存しているので、kotlin.jsを読み込んだ後に生成した ${module}.jsを読み込みます。

ファイル名はプロジェクトのものが適応されます。

f:id:sakebook:20200218015239p:plain

ちなみに今回は詳しく取り上げませんが、 divコードから動的に生成できます。

Node

nodeコマンドで実行します。

$ node build/js/packages/kotlin-js-example/kotlin/kotlin-js-example.js
Hello Kotlin/JS!!

これでどちらでも動作確認ができました。

JavaScriptライブラリの追加

kotlinブロックの中で定義します。さらにどのsourceフォルダで利用するかも指定します。 このあたりはMPPが意識されてる感じがしますね。

次の例では dayjs を追加してます。

  • build.gradle.kts
kotlin {
    sourceSets["main"].dependencies {
        implementation(npm("dayjs", "^1.8.20"))
    }
}

npm 関数を使います。 これで build/js/node_modules/ 内にDLされます。

ライブラリの利用(Node)

ソースを次のように変更します。

  • main.kt
external fun require(module: String): dynamic

fun main() {
    val dayjs = require("dayjs")
    println("Hello Kotlin/JS!! ${dayjs()}")
}

これをnodeで実行します。

$ node build/js/packages/kotlin-js-example/kotlin/kotlin-js-example.js
Hello Kotlin/JS!! Sat, 08 Feb 2020 10:56:59 GMT

少し解説します。

external

external 修飾子を付けたものはJavaScriptからも呼び出しできるようにトランスパイルされます。そのままの名前で変換されます。

nodeで require が定義されているため今回は実行できています。これを別名にするとエラーになります。

  • main.kt
external fun req(module: String): dynamic

fun main() {
    val dayjs = req("dayjs")
    println("Hello Kotlin/JS!! ${dayjs()}")
}
$ node build/js/packages/kotlin-js-example/kotlin/kotlin-js-example.js
/USER_PATH/build/js/packages/kotlin-js-example/kotlin/kotlin-js-example.js:35
    var dayjs = req('dayjs');
                ^

ReferenceError: req is not defined

自由に命名したい場合JsNameアノテーションをつけてあげれば解決します。

  • main.kt
@JsName("require")
external fun req(module: String): dynamic

dynamic

JavaScriptの世界の型を受け止めるための便宜的な型です。

dynamicはどんな変数や関数も代入でき、実行可能です。

関数はdynamic型を返します。

未定義の関数は実行時にTypeErrorになります。未定義の変数はundefinedになります。

  • main.kt
fun main() {
    val dyn: dynamic = object{}
    println(dyn.some)
    dyn.foo = "foo"
    println(dyn.foo)
    dyn.bar = {
        println("bar")
    }
    dyn.bar()
    println(dyn.bar)
    dyn.some()
}
$ node build/js/packages/kotlin-js-example/kotlin/kotlin-js-example.js
undefined
foo
bar
function main$lambda() {
    println('bar');
    return Unit;
  }
/USER_PATH/build/js/packages/kotlin-js-example/kotlin/kotlin-js-example.js:35
    dyn.some();
        ^

TypeError: dyn.some is not a function

dynamic型の仕様についてはこちらにドキュメントがあります。

github.com

ライブラリの利用(Browser)

Browserの場合は require で読み込みは行わないのでエラーになります。

f:id:sakebook:20200218021013p:plain

Browserの場合は、 require で読み込むのをやめて追加したライブラリをscriptタグで読み込んであげると実行できます。

  • main.kt
fun main() {
    println("Hello Kotlin/JS!! ${js("dayjs()")}")
}
  • index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <script src="build/js/packages_imported/kotlin/1.3.61/kotlin.js"></script>
    <script src="build/js/node_modules/dayjs/dayjs.min.js"></script>
    <script src="build/js/packages/kotlin-js-example/kotlin/kotlin-js-example.js"></script>
</head>

<body>
<p>Kotlin/JS example</p>
</body>
</html>

f:id:sakebook:20200218021305p:plain

js

js 関数の引数に渡した文字列は、そのままJavaScriptのコードとして実行されます。

この方法だとBrowserとNodeのどちらでも動くJavaScriptが作成できません。なので、ライブラリをModule化します。

ライブラリのModule化

external修飾子をつけているものに、更にJSModuleアノテーションを付与します。

引数にはnpmなどでインストールする名前と同じものを使います。

umd 形式の場合、JSNonModuleアノテーションもセットで必要です。

  • main.kt
@JsModule("dayjs")
@JsNonModule
external fun dayjs(): dynamic

fun main() {
    println("Hello Kotlin/JS!! ${dayjs()}")
}

これで実行可能な形式でJSファイルが生成されているので、どちらの形式でも実行可能になりました。

実際のライブラリに則している形であれば定義方法は自由です。例えば次のように定義することもできます。

  • main.kt
@JsModule("dayjs")
@JsNonModule
@JsName("dayjs")
external class DayJs(any: Any? = definedExternally) {
    fun format(): String
}

fun main() {
    println("Hello Kotlin/JS!! ${DayJs().asDynamic()}")
    println("this year         ${DayJs().asDynamic().year()}")
    println("Valentine         ${DayJs("2020-02-14").asDynamic()}")
    println("Formatted date    ${DayJs().format()}")
}
$ node build/js/packages/kotlin-js-example/kotlin/kotlin-js-example.js
Hello Kotlin/JS!! Sat, 15 Feb 2020 10:47:09 GMT
this year         2020
Valentine         Thu, 13 Feb 2020 15:00:00 GMT
Formatted date    2020-02-15T19:47:09+09:00

definedExternallyプレースホルダのようなものです。JSで定義されていてKotlin側からはわからないものがあるときに使います。

yearは実際にdayjsライブラリで定義されている関数を、dynamicから直接呼び出しています。なので型はdynamicで返ってきます。

formatも実際にdayjsライブラリで定義されているの関数なのですが、再定義することで型情報を与えることができます。

Kotlinで気持ちよくJSのライブラリを利用するには、このようにモジュールを作成して必要な関数たちを再定義してあげる必要があります。

JetBrains公式からReactのラッパーモジュールが提供されています。

github.com

Webpack

先程buildコマンドで作成したBrowser用のJSファイルは、複数必要でした。これだと、ライブラリを追加するたびにhtmlも変更しなければならないため、手間です。

Webpackを利用してビルドすることで、1つのJSファイルにまとめることができます。

次のコマンドで作成できます。このコマンドはtargetブロックで browser を宣言していないと使えないので注意が必要です。

$ ./gradlew browserWebpack

作成されたJSファイルは build/distributions/ に吐き出されます。

htmlを変更します。

  • index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <script src="build/distributions/kotlin-js-example.js"></script>
</head>

<body>
<p>Kotlin/JS example</p>
</body>
</html>

読み込むのは1つのファイルですが、無事実行できました。

f:id:sakebook:20200218021707p:plain

このJSファイルはnodeからも実行できます。

$ node build/distributions/kotlin-js-example.js 
Hello Kotlin/JS!! Sat, 15 Feb 2020 11:36:33 GMT
this year         2020
Valentine         Thu, 13 Feb 2020 15:00:00 GMT
Formatted date    2020-02-15T20:36:33+09:00

browserRun というコマンドもあるのですが、まだバグがあり正常に終了しないのでJSファイルが作成できません。ちなみにbrowserRunで立ち上がるサーバは、 src/main/resrouces/index.html を見ます。

まとめ

今回はKotlin/JSを一から、 umd 形式でアウトプットして動かしてみました。

実際は commonjs 形式で利用することが多いと思いますが、このように、どうしてそうなるのかをきちんと理解しておけば、困ったときにデバッグしやすいと思います。

ネットで見つかるサンプルも、古いものや解説がないものばかりなのでこの記事は参考になると思います。

今回検証したリポジトリはこちらです。

github.com

参考

Kotlin to JavaScript - Kotlin Programming Language

https://youtrack.jetbrains.com/issue/KT-27679

GitHub - Kotlin/kotlinx.html: Kotlin DSL for HTML

GitHub - iamkun/dayjs: ⏰ Day.js 2KB immutable date library alternative to Moment.js with the same modern API

Calling JavaScript from Kotlin - Kotlin Programming Language