Gradle で Kotlin/JS ライブラリを開発する
概要
Gradle を使用して Kotlin/JS の開発環境を構築し、Hello World を作成します。 更に、ライブラリとして配布可能にする方法、そのライブラリを使用する方法を説明します。 JavaScript のライブラリを使用する方法は別の記事で説明します。
なお、筆者は JavaScript 界については詳しくないので、誤りがあれば優しく教えてください。
目次
確認環境
- IntelliJ IDEA Community 2017.2
- Kotlin 1.1.3-2
- Gradle 3.5
- Groovy 2.4.10
参考情報
- Kotlin to JavaScript - Kotlin Programming Language
- 公式チュートリアル
- Dynamic Type - Kotlin Programming Language
- 公式リファレンス
- kotlin.js - Kotlin Programming Language
- 公式API
解説
環境構築
Gradle の kotlin2js plugin を使用して Kotlin/JS のコードをビルドする環境を構築する。
モジュール単位に js ファイルが生成されるため、複数のモジュールを作成する前提で構築する。
手順としては、空プロジェクトを作成し、モジュールを追加する。
モジュール名は sample
として作成した。
初期設定
生成された sample/build.gradle
は以下の通り。
group 'com.example' version '1.0-SNAPSHOT' buildscript { ext.kotlin_version = '1.1.3-2' repositories { mavenCentral() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } apply plugin: 'kotlin2js' repositories { mavenCentral() } dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" }
Sample.kt
を src/main/kotlin
に作成する。
fun main(args: Array<String>) { println("Hello World") }
module root (例では sample
ディレクトリ) にてビルドを実行。
$ ./gradlew build
HTML (sample/index.html
) を作成する。HTML ではビルドにより生成された JavaScript を読み込む。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>サンプル</title> </head> <body> <script src="build/classes/main/sample_main.js"></script> </body> </html>
ここまでに作成、および生成したファイルは以下の通り。(重要な部分のみ記載。)
sample <- module root build classes main sample_main root-package.kjsm sample_main.js sample_main.meta.js src main java kotlin Sample.kt resources test java kotlin resources build.gradle index.html
ブラウザで表示すると kotlin
モジュールが見つからずにエラーとなる。
kotlin.js の抽出
Gradle を使う場合には jar ファイルから js ファイルを抽出する必要がある。
sample/build.gradle
に以下を追記する。
(参照 : http://kotlinlang.org/docs/tutorials/javascript/getting-started-gradle/getting-started-with-gradle.html)
build.doLast() { // Copy *.js from jar into build directory configurations.compile.each { File file -> copy { includeEmptyDirs = false from zipTree(file.absolutePath) into 'build/classes/main' include '**/*.js' eachFile { details -> def names = details.path.split('/') details.path = names.getAt(names.length - 1) } } } }
HTML では kotlin.js
を読み込ませる記述を sample_main.js
の前に追記。
<script src="build/classes/main/kotlin.js"></script> <script src="build/classes/main/sample_main.js"></script>
以上でブラウザのコンソールに Hello World
が表示される。
パスの変更
ソースコードを配置するディレクトリと生成される JavaScript が配置されるディレクトリは階層が深い。 階層を減らすことで何らかの支障をきたすかもしれないが、 今回のサンプルの範囲では問題ないため、 配置するディレクトリを変更する。 (今回のサンプルでは、ソースセットには Kotlin ソースコード以外は置かず、テストを作成していないため問題ない。)
sample/build.gradle
に以下を追記する。
sourceSets { main.kotlin.srcDirs += "src/main" test.kotlin.srcDirs += "src/test" } def outputDir = "${projectDir}/build/js" compileKotlin2Js { kotlinOptions { outputFile = "${outputDir}/sample.js" } } build.doLast() { // Copy *.js from jar into build directory configurations.compile.each { File file -> copy { includeEmptyDirs = false from zipTree(file.absolutePath) into outputDir // 変更 include '**/*.js' eachFile { details -> def names = details.path.split('/') details.path = names[names.length - 1] } } } }
ファイルの配置は以下に変更される。
sample <- module root build js sample root-package.kjsm kotlin.js kotlin.meta.js sample.js sample.meta.js src main Sample.kt test build.gradle index.html
JavaScript の配置が変更されたので HTML (sample/index.html
) の内容も変更する。
<script src="build/js/kotlin.js"></script> <script src="build/js/sample.js"></script>
生成ファイル
ビルドすることで生成されるファイルは 3 種類ある。
*.js
- モジュールの JavaScript ファイル
*.meta.js
- Kotlin to JavaScript - Kotlin Programming Language
- リフレクションやその他の機能に使用されるメタファイル
- Kotlin to JavaScript - Kotlin Programming Language
*.kjsm
- javascript - Kotlin: What is a kjsm file? - Stack Overflow
- Kotlin JavaScript Meta ファイル
- IDE で型チェックをするときに使用
- javascript - Kotlin: What is a kjsm file? - Stack Overflow
ライブラリ化
ビルドすることで jar ファイルが生成される。 jar ファイルには 3 種類のファイル (js, meta.js, kjsm) が全て含まれている。 kjsm ファイルが含まれていれば、IDE の型チェックが有効となる。
例えば、sample
モジュールの main
関数を sayHello
関数に変更して、他のモジュール (sample-client
モジュールを作成) から使用する。
まず、sample
モジュールの Sample.kt
を以下に変更する。
fun sayHello() { println("Hello World") }
ライブラリ化する際には、依存している js ファイルを抽出する必要はない。
sample/build.gradle
から以下の記述を無効化、もしくは削除する。
//build.doLast() { // // Copy *.js from jar into build directory // configurations.compile.each { File file -> // copy { // includeEmptyDirs = false // // from zipTree(file.absolutePath) // into outputDir // include '**/*.js' // eachFile { details -> // def names = details.path.split('/') // details.path = names[names.length - 1] // } // } // } //}
ビルドすると build/libs/sample-1.0-SNAPSHOT.jar
が生成される。
次に、sample-client
モジュールを作成し、sample
モジュールと同様に設定する。
jar の依存関係を追加すると sample-client/build.gradle
は以下の通りになる。
buildscript { ext.kotlin_version = '1.1.3-2' repositories { mavenCentral() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } apply plugin: 'kotlin2js' repositories { mavenCentral() // 追加 flatDir { dirs '../sample/build/libs' } // ^^^ } dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" compile "com.example:sample:1.0-SNAPSHOT" // 追加 //compile 'com.example:sample:1.0-SNAPSHOT' // error! } sourceSets { main.kotlin.srcDirs += "src/main" test.kotlin.srcDirs += "src/test" } def outputDir = "${projectDir}/build/js" compileKotlin2Js { kotlinOptions { outputFile = "${outputDir}/sample-client.js" // 変更 } } build.doLast() { // Copy *.js from jar into build directory configurations.compile.each { File file -> copy { includeEmptyDirs = false from zipTree(file.absolutePath) into outputDir include '**/*.js' eachFile { details -> def names = details.path.split('/') details.path = names[names.length - 1] } } } }
SampleClient.kt
を以下の内容で作成する。
build/libs/sample-1.0-SNAPSHOT.jar
に含まれる kjsm ファイルのおかげで補完機能が有効になっている。
fun main(args: Array<String>) {
sayHello()
}
HTML ファイル (sample-client/index.html
) を作成して JavaScript ファイルを読み込む。
sample-client.js
の前に kotlin.js
と sample.js
を読み込む必要がある。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>サンプル</title> </head> <body> <script src="build/js/kotlin.js"></script> <script src="build/js/sample.js"></script> <script src="build/js/sample-client.js"></script> </body> </html>
ここまでに作成/生成したファイルを整理すると以下になる。
sample-client <- module root build js sample-client root-package.kjsm kotlin.js kotlin.meta.js sample.js sample.meta.js sample-client.js sample-client.meta.js src main SampleClient.kt test build.gradle index.html
sample-client/index.html
をブラウザで開くと、ブラウザのコンソールに Hello World
が表示される。
依存関係管理
HTML に script タグを並べることで各種のライブラリを使用できるが、記述順を気にする必要がある。
依存関係の管理を楽にするために、JavaScript のモジュールシステムを使用する。
Kotlin/JS が対応している JavaScript モジュールシステム (moduleKind
) は以下の 4 つ。
(参照 : JavaScript Modules - Kotlin Programming Language)
- plain
- モジュールシステムを使わない
- amd
- Asynchronous Module Definition (AMD) に対応させる
- require.js ライブラリで使用する
- commonjs
- CommonJS に対応させる
- node.js/npm で使用する
- umd
ライブラリに JavaScript モジュールシステムを適用する
UMD を使用すればいずれのモジュールシステムにも対応できるので、UMD を指定してビルドする。
sample
と sample-client
の両方の build.gradle
に moduleKind
を追加する。
compileKotlin2Js { kotlinOptions { outputFile = "${outputDir}/sample-client.js" moduleKind = "umd" // 追加 } }
ビルドして生成された JavaScript ファイル (sample.js
, sample-client.js
) の冒頭には以下の様な記述が加えられる。
moduleKind = "plain"
の場合 :
if (typeof kotlin === 'undefined') { throw new Error("Error loading module 'sample-client'. Its dependency 'kotlin' was not found. Please, check whether 'kotlin' is loaded prior to 'sample-client'."); } if (typeof sample === 'undefined') { throw new Error("Error loading module 'sample-client'. Its dependency 'sample' was not found. Please, check whether 'sample' is loaded prior to 'sample-client'."); } this['sample-client'] = function (_, Kotlin, $module$sample) { ...snip... return _; }(typeof this['sample-client'] === 'undefined' ? {} : this['sample-client'], kotlin, sample);
Global オブジェクトに kotlin
, sample
, sample-client
を登録している。
moduleKind = "umd"
の場合 :
(function (root, factory) { if (typeof define === 'function' && define.amd) define(['exports', 'kotlin', 'sample'], factory); else if (typeof exports === 'object') factory(module.exports, require('kotlin'), require('sample')); else { if (typeof kotlin === 'undefined') { throw new Error("Error loading module 'sample-client'. Its dependency 'kotlin' was not found. Please, check whether 'kotlin' is loaded prior to 'sample-client'."); } if (typeof sample === 'undefined') { throw new Error("Error loading module 'sample-client'. Its dependency 'sample' was not found. Please, check whether 'sample' is loaded prior to 'sample-client'."); } root['sample-client'] = factory(typeof this['sample-client'] === 'undefined' ? {} : this['sample-client'], kotlin, sample); } }(this, function (_, Kotlin, $module$sample) { ...snip... return _; }));
typeof define === 'function' && define.amd
に該当すれば AMD、
typeof exports === 'object'
に該当すれば CommonJS、
それ以外であればモジュールシステム無しと判断している。
JavaScript モジュールシステムを使い、読み込む
AMD を使用して依存関係を管理する。
AMD を使用するために require.js
を導入する。
(CommonJS も少しだけ調べたけれどブラウザで使用するには複雑に見えたので、AMD だけを記載。)
sample-client/build.gradle
に以下を追記する。
dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" compile "com.example:sample:1.0-SNAPSHOT" compile 'org.webjars:requirejs:2.3.3' // 追加 }
ビルドすることで build/js/require.js
が展開される。
require.js
を使うにあたって設定ファイル (require-config.js
) を作成しておく。
(参照 : RequireJS API)
var require = { baseUrl: 'build/js', enforceDefine: true, };
設定の内容は以下のとおり。
build/js
以下にある JavaScript ファイルを読み込む (baseUrl
)- JavaScript の読み込みに失敗したときはエラーとする (
enforceDefine
)
次に、require.js
を使用するように HTML (sample-client/index.html
) を変更する。
<script src="require-config.js"></script> <script src="build/js/require.js"></script> <script>require(['sample-client']);</script>
sample-client
を指定することで、sample-client
が依存している kotlin
と sample
が読み込まれる。
読み込み先は require-config.js
で指定されている。
sample
, sample-client
のいずれでも依存関係は Gradle に集約できている点が嬉しい。
最後に、ブラウザで開くとブラウザのコンソールに Hello World
が表示される。
おまけ
sample-client
は読み込むだけで main
関数が実行されてしまう。
再利用性の観点からは望ましくない。
SampleClient.kt
は以下に変更する。
fun run() {
sayHello()
}
sample-client/index.html
は以下に変更する。
<script src="require-config.js"></script> <script src="build/js/require.js"></script> <script> require(['sample-client'], function(client) { client.run(); }); </script>