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

参考情報

解説

環境構築

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.ktsrc/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 種類ある。

ライブラリ化

ビルドすることで 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.jssample.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
    • Unified Module Definitions (UMD) に対応させる
    • AMD, CommonJS, モジュールシステム無しの全てに対応させる
    • AMD, CommonJS の順に適用され、いずれも使用していなければモジュールシステム無しの扱いになる

ライブラリに JavaScript モジュールシステムを適用する

UMD を使用すればいずれのモジュールシステムにも対応できるので、UMD を指定してビルドする。

samplesample-client の両方の build.gradlemoduleKind を追加する。

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 に該当すれば AMDtypeof 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 が依存している kotlinsample が読み込まれる。 読み込み先は 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>

ソースコード

Release 2017-07-30-211651 · nosix/kotlin-js · GitHub