ts2kt を Gradle で実行する
概要
TypeScript の型定義を Kotlin ファイルに変換する Node.js スクリプトである ts2kt を Gradle で実行できるようにします。
2017/8/31 現在、ts2kt は使える状態ではありません。 生成された Kotlin ファイルでは多数のエラーが発生します。
目次
確認環境
- IntelliJ IDEA Community 2017.2
- Kotlin 1.1.4-2
- Gradle 3.5
- Groovy 2.4.10
参考情報
- gradle-node-plugin/docs at master · srs/gradle-node-plugin · GitHub
- gradle-node-plugin の文書
- GitHub - Kotlin/ts2kt: Converter of TypeScript definition files to Kotlin declarations (stubs)
- ts2kt の文書
解説
例として Vue.js の型情報 (*.d.ts
) を Kotlin ファイル (*.kt
) に変換する。
手順としては以下の通り。
- node をプロジェクト用にダウンロード
- npm を使い ts2kt と vue をインストール
- ts2kt で
*.d.ts
を*.kt
に変換
まずは build.gradle
を以下の通りに記述する。
buildscript { ext.gradle_node_version = '1.2.0' repositories { jcenter() maven { // gradle-node-plugin url "https://plugins.gradle.org/m2/" } } dependencies { classpath "com.moowork.gradle:gradle-node-plugin:$gradle_node_version" } } apply plugin: 'com.moowork.node' node { // node をプロジェクト用にダウンロード download = true } task ts2kt(type: NodeTask) { String nodeDir = "${project.projectDir}/node_modules" script = file("$nodeDir/ts2kt/ts2kt.js") String destDir = 'src/main/kotlin/org/vuejs' file(destDir).mkdir() args = ['-d', destDir] + file("$nodeDir/vue/types").listFiles().collect { it.absolutePath } }
npm の設定として pakage.json
を以下の通りに記述する。
{ "name": "Vue.kt", "version": "2.4.2", "description": "Vue.kt that is Vue.js in Kotlin", "devDependencies": { "ts2kt": "0.0.14", "vue": "2.4.2" } }
設定が完了したら以下のコマンドを実行する。
$ ./gradlew npmInstall ts2kt
src/main/kotlin/org/vuejs
ディレクトリに Kotlin ファイルが生成される。
例えば vue.kt
の一部。
external interface `T$0` { @nativeInvoke operator fun invoke(): VNode @nativeInvoke operator fun invoke(tag: String, children: VNodeChildren): VNode @nativeInvoke operator fun invoke(tag: String, data: VNodeData? = definedExternally /* null */, children: VNodeChildren? = definedExternally /* null */): VNode @nativeInvoke operator fun invoke(tag: Component, children: VNodeChildren): VNode @nativeInvoke operator fun invoke(tag: Component, data: VNodeData? = definedExternally /* null */, children: VNodeChildren? = definedExternally /* null */): VNode @nativeInvoke operator fun invoke(tag: AsyncComponent, children: VNodeChildren): VNode @nativeInvoke operator fun invoke(tag: AsyncComponent, data: VNodeData? = definedExternally /* null */, children: VNodeChildren? = definedExternally /* null */): VNode } ... <snip> ... open fun <T> `$watch`(expOrFn: (this: Vue /* this */) -> T, callback: WatchHandler<Vue /* this */, T>, options: WatchOptions? = definedExternally /* null */): () -> Unit = definedExternally ... <snip> ...
元は vue.d.ts
のこの部分。
export type CreateElement = { // empty node (): VNode; // element or component name (tag: string, children: VNodeChildren): VNode; (tag: string, data?: VNodeData, children?: VNodeChildren): VNode; // component constructor or options (tag: Component, children: VNodeChildren): VNode; (tag: Component, data?: VNodeData, children?: VNodeChildren): VNode; // async component (tag: AsyncComponent, children: VNodeChildren): VNode; (tag: AsyncComponent, data?: VNodeData, children?: VNodeChildren): VNode; } ... <snip> ... $watch<T>( expOrFn: (this: this) => T, callback: WatchHandler<this, T>, options?: WatchOptions ): (() => void); ... <snip> ...
生成された Kotlin ファイルにはいくつか問題がある。
T$0
という名前が同一パッケージ内で複数定義されることになり名前が衝突する。@nativeInvoke
は DeprecatedVNodeChildren
,Component
,AsyncComponent
などの型が定義できていない- Union Type に対応できていないため型を定義できない
- パラメータ名に
this
(Kotlin のキーワード) が使われている
Union Type に関する議論はされている様子。
既存の JavaScript ライブラリを使うのであれば ts2kt による型情報を補足が欲しいところ。
Kotlin/JS TIPS - this の扱い
概要
JavaScript の関数中の this は関数を保持しているオブジェクトによって決定されます。 この振る舞いが Kotlin/JS を使う上での問題になります。 this を扱ういくつかの方法を説明します。 最適な方法とは言えないので、より良い方法があれば教えてください。
ソースコードは前回の記事の続きになっています。 詳しくは前回の記事を参照してください。
目次
確認環境
- IntelliJ IDEA Community 2017.2
- Kotlin 1.1.3-2
- Gradle 3.5
- Groovy 1.4.10
参考情報
- 算出プロパティとウォッチャ - Vue.js
- Vue.js の算出プロパティ
解説
this の問題点
以下の JavaScript のコードは 算出プロパティとウォッチャ - Vue.js から引用したコードに少しだけ手を加えたコードである。firstName
と lastName
に値を設定すると fullName
が算出されるサンプルである。
var vm = new Vue({ el: '#example', data: { firstName: 'Foo', lastName: 'Bar' }, computed: { fullName: function () { return this.firstName + ' ' + this.lastName } } }) vm.firstName = 'Taro' vm.lastName = 'Yamada'
この処理を Kotlin/JS で書いてみる。SampleClient.kt
を以下に変更する。
@JsNonModule @JsModule("vue") external open class Vue(option: VueOption) external interface VueOption : Json { var el: Any var data: Json var computed: Json } fun <T : Json> Json(): T = js("({})") fun <T : Json> Json(init: T.() -> Unit): T = Json<T>().apply(init) fun Vue(init: VueOption.() -> Unit): Vue = Vue(Json<VueOption>().apply(init)) external interface Model : Json { var firstName: String var lastName: String } fun run() { val vm: dynamic = Vue { el = "#example" data = Json<Model> { firstName = "Foo" lastName = "Bar" } computed = Json { // this は Json クラスのインスタンス this["fullName"] = { // this は Vue クラスのインスタンス this as Model // firstName, lastName を参照したいのでキャスト "${this.firstName} ${this.lastName}" } } } vm.firstName = "Taro" vm.lastName = "Yamada" }
sample-client/index.html
には以下を記述して、ブラウザで開いてみる。
<div id="example">{{ fullName }}</div>
ブラウザに表示される内容は undefined undefined
である。
残念ながら上記のコードではやりたいことは行えない。
${this.firstName} ${this.lastName}
の this
に問題がある。
JavaScript と Kotlin での this の違い
先の例では、JavaScript の関数中の this
は関数を保持しているオブジェクトを示している。
実行時に決定されるので、this
は Vue クラスのインスタンスを示すことになる。
一方 Kotlin の例では、Kotlin のラムダ中の this
はそのラムダの外側のスコープの this
になっている。
つまり、Json クラスのインスタンスを示すことになる。
レシーバ付き関数リテラル
実行時に this
を決定したいのであれば、レシーバ付き関数リテラルを使用する。
SampleClient.kt
を以下に変更する。
fun run() { val vm: dynamic = Vue { el = "#example" data = Json<Model> { firstName = "Foo" lastName = "Bar" } computed = Json { // this は Json クラスのインスタンス this["fullName"] = fun Model.(): String { // this は実行時に決定される return "${this.firstName} ${this.lastName}" } } } vm.firstName = "Taro" vm.lastName = "Yamada" }
external との併用における問題点
external interface
を使うことで、firstName =
, lastName =
の様な簡潔な記述を実現している。
fullName
も同様に簡潔な記述にするために以下のコードを追加したくなる。
external interface Computed : Json { var fullName: Model.() -> String }
残念ながらコンパイルエラーとなる。
external
宣言中ではレシーバ付き関数型は禁止されているとエラーメッセージが表示される。
(禁止している理由を知っている方がいたら教えてください。)
external との併用
レシーバ付き関数リテラルを使えば this
の扱いを動的に変更できるが、
external
宣言と併せて使うことができない。
external
宣言を使う場合にはレシーバなしの関数リテラルにする必要がある。
例えば、以下のとおり。
external interface Computed : Json { var fullName: () -> String }
そうすると最初の問題に戻ってしまう。
回避するための苦肉の策として js("this")
を使う方法がある。
コード生成時に JavaScript の this
をそのまま出力させる。
以下がその例である。
inline fun <T> thisAs(): T = js("this") fun run() { val vm: dynamic = Vue { el = "#example" data = Json<Model> { firstName = "Foo" lastName = "Bar" } computed = Json<Computed> { // this は Json クラスのインスタンス fullName = { val self = thisAs<Model>() // self は実行時に決定される "${self.firstName} ${self.lastName}" } } } vm.firstName = "Taro" vm.lastName = "Yamada" }
inline
を付けないと thisAs
関数が生成され、thisAs
関数を保持しているオブジェクトが存在しないために thisAs
関数の結果は undefined
になる。
ソースコード
Kotlin/JS TIPS - JavaScript ライブラリへの型情報の付与
概要
JavaScript ライブラリを使用する際に、型情報を付与する方法について説明します。 前回の記事で JavaScript ライブラリの使用方法を説明しましたが、 静的型付けによる恩恵を得ずに JavaScript の柔軟性を重視した使用方法としていました。 型情報を付与することによって、以下の恩恵を得られる様にしていきます。
ライブラリ開発者の手間を増やして、ライブラリ利用者の手間を減らしていきます。
前回の記事:
目次
確認環境
- IntelliJ IDEA Community 2017.2
- Kotlin 1.1.3-2
- Gradle 3.5
- Groovy 1.4.10
参考情報
- kotlin.js - Kotlin Programming Language
- Kotlin/JS 標準ライブラリ
- Calling JavaScript from Kotlin - Kotlin Programming Language
external interface
の使い方
解説
json 関数と Json インターフェース
JavaScript では {}
によってオブジェクト生成を行う。
Kotlin/JS では json 関数によってオブジェクトを生成できる。
生成されたオブジェクトは Json インターフェース を実装したオブジェクトであり、
[]
による操作を行える。
以下は SampleClient.kt
の一部である。
fun run() { val option = json() option["el"] = "#example" option["data"] = json("message" to "Hello World") }
生成される JavaScript は以下の様になっている。
function run() { var option = json([]); option['el'] = '#example'; option['data'] = json([to('message', 'Hello World')]); }
json 関数のソースコード (Kotlin) は以下になっており、js("({})")
でオブジェクトを生成していることがわかる。
public fun json(vararg pairs: Pair<String, Any?>): Json { val res: dynamic = js("({})") for ((name, value) in pairs) { res[name] = value } return res }
また、Json インターフェースのソースコード (Kotlin) は以下になっている。
public external interface Json { operator fun get(propertyName: String): Any? operator fun set(propertyName: String, value: Any?): Unit }
external
を使うことで JavaScript のコードをそのまま使用することができる。
get/set
を定義することで []
による参照/代入を可能にしている。
型情報の付与
json 関数の仕組みを応用して以下のとおりに記述することで、Json コンストラクタに見せかけることができる。
fun Json(): Json = js("({})") fun run() { val option = Json() option["el"] = "#example" }
さらに、external
を使うことで JavaScript のコードをそのまま使用できる性質を利用して、
[]
を使わずにオブジェクトに値を設定できる様にする。
例えば、以下の様に。
fun <T : Json> Json(): T = js("({})") external interface VueOption : Json { var el: Any var data: Json } external interface Model : Json { var message: String } fun run() { val option = Json<VueOption>() option.el = "#example" option.data = Json<Model>().apply { message = "Hello World" } option["computed"] = Json() // VueOption は Json インターフェースを実装しているので [] を使える }
生成された JavaScript は以下のとおり。
external
を使用した部分は簡潔なコードとなっている。
コード上に VueOption
や Model
インターフェースは現れない。
function Json() { return {}; } function run() { var option = Json(); option.el = '#example'; var $receiver = Json(); $receiver.message = 'Hello World'; option.data = $receiver; option['computed'] = Json(); }
さらに、初期化処理を行う関数を引数 (init
) にとる Json
関数を追加することで、
()
や apply
を省略できる。
fun <T : Json> Json(init: T.() -> Unit): T = Json<T>().apply(init) fun run() { val option = Json<VueOption> { el = "#example" data = Json<Model> { message = "Hello World" } this["computed"] = Json() }
external interface
を使うことで型情報を付与し、
補完機能の恩恵を得えつつ、
誤った型の値を代入する危険性を減らすことができる。
Vue.js に適用する
さらに、Vue
関数を SampleClient.kt
に追加すると、Vue の初期化が簡潔になる。
@JsNonModule @JsModule("vue") external class Vue(option: VueOption) fun <T : Json> Json(): T = js("({})") fun <T : Json> Json(init: T.() -> Unit): T = Json<T>().apply(init) external interface VueOption : Json { var el: Any var data: Json } external interface Model : Json { var message: String } fun Vue(init: VueOption.() -> Unit): Vue = Vue(Json<VueOption>().apply(init)) fun run() { val vm: dynamic = Vue { el = "#example" data = Json<Model> { message = "Hello World" } } vm.message = "Hello Kotlin World" }
前回の記事と比べると随分と簡潔になったと思う。
しかし、検討すべき課題は残っている。
補完機能の恩恵は受けられるようになったが、型安全という面では十分とは言えない。
上記の例で言えば、el
は String
もしくは HTMLElement
であるため、Any
にしている。
また、vm.message
を参照するために dynamic
にしている。
JavaScript の動的な側面を扱う方法については検討が必要である。
ソースコード
Kotlin/JS で JavaScript ライブラリを使用する
概要
Kotlin/JS から JavaScript ライブラリを使用する方法を説明します。 AMD に対応している JavaScript ライブラリを前提としています。 例として Lodash, Vue.js を使います。 作成するコードでは型付けは弱いままとして、JavaScript の柔軟性を活かした例としています。 Kotlin による静的型付けの恩恵は得られません。 静的型付けを活かした書き方は別途記事を書きます。
前回の記事の続きになっているので、必要に応じて参照してください。
ソースコード : Release 2017-07-30-211651 · nosix/kotlin-js · GitHub
目次
確認環境
- IntelliJ IDEA Community 2017.2
- Kotlin 1.1.3-2
- Gradle 3.5
- Groovy 1.4.10
参考情報
- RequireJS API
- RequireJS 公式
- Calling JavaScript from Kotlin - Kotlin Programming Language
- Kotlin から JavaScript を呼ぶ方法
解説
JavaScript ライブラリを使用する
前回の記事では、 RequireJS を使用して JavaScript の依存関係を管理できるようにした。 RequireJS は AMD に対応している JavaScript ライブラリを管理するだけでなく、AMD に対応していないライブラリも管理できる。
本記事では、AMD に対応していないライブラリについては記載しない。
AMD に対応していないライブラリでは試行していないためである。
RequireJS にて AMD に対応していないライブラリを使用する場合には config で shim
を使えば良いらしい。
(参照 : RequireJS使い方メモ - Qiita)
以降では、AMD に対応しているライブラリについて説明する。
JavaScript ライブラリを CDN 経由で使用する
ライブラリをダウンロードせず、CDN (Content Delivery Network) を使用して手軽に試したい場合がある。
例えば、Lodash を使う場合、以下のサイトで配信されており、URL が記載されている。
lodash free CDN links by jsDelivr - A super-fast CDN for developers and webmasters
RequireJS で使用するには、前回の記事で作成した require-config.js
に上記サイトに記載されている URL を追記すればよい。
以下は require-config.js
の内容である。
var require = { baseUrl: 'build/js', // 追加 paths: { lodash: 'https://cdn.jsdelivr.net/lodash/4.17.4/lodash', // 拡張子の.jsは不要 }, // ^^^ enforceDefine: true, };
次に、Kotlin/JS のコードを記述する。 型チェックが弱いままで使用する方法である。 JavaScript のコードをほぼそのまま記述するため記述の手間は少ないが、型チェックや補完機能の恩恵を得られない方法である。
SampleClient.kt
を以下に変更する。
external val lodash: dynamic fun run() { println(lodash.capitalize("hello world")) }
external val lodash: dynamic
とすると、JavaScript の lodash 変数を参照できる。
external
は外部、すなわち JavaScript のコードを示す。
dynamic
は変数の型が動的であることを示す。
ビルドで生成される JavaScript のコードは以下になる。(一部抜粋)
function run() { println(lodash.capitalize('hello world')); }
このコードでは lodash
変数が見つからずにエラーとなる。
また、生成された sample-client.js
では lodash
への依存関係が定義されない。
エラーを解決するために SampleClient.kt
を以下に変更する。
@JsNonModule @JsModule("lodash") external val lodash: dynamic fun run() { println(lodash.capitalize("hello world")) }
依存関係を定義させるためには @JsModule
を使用して、モジュール名(ライブラリ名)を指定する。
@JsModule
を使用しており、かつ build.gradle
で moduleKind = "plain"
か moduleKind = "umd"
とした場合には、ビルドエラーとなる。
モジュールシステムを使用しない場合のために @JsNonModule
を指定する必要がある。
前回の記事に引き続き moduleKind = "umd"
としているため、
@JsModule
と @JsNonModule
を指定している。
@JsModule
を指定した場合には、生成される JavaScript コードは以下になる。(一部抜粋)
function run() { println($module$lodash.capitalize('hello world')); }
以上でエラーは解消され、ブラウザのコンソールに Hello world
が表示される。
Lodash を使う場合の注意点
Lodash を moduleKind = "plain"
で使う場合には注意が必要である。
Lodash や Underscore.js では Global スコープの _
を使用する。
そのため、SampleClient.kt
のコードは以下にする。
external val `_`: dynamic fun run() { println(`_`.capitalize("hello world")) }
Kotlin/JS が生成するコードでは、Closure スコープに _
を使用する。
このため、Global スコープの _
を隠蔽してしまいエラーとなる。
sample-client.js
を読み込む前に _
を lodash
に代入して、変数名も lodash
にすれば対応できる。
しかし、モジュールシステムを使用した方が簡潔である。
JavaScript ライブラリをダウンロードして使用する
Gradle では JavaScript ライブラリとの依存関係定義のために WebJars を使用することができる。 前回の記事でも RequireJS との依存関係定義に使用した。
例として、Vue.js の依存関係を追加する。
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' compile 'org.webjars:vue:2.1.3' // 追加 }
次に、Vue.js の使い方に従い sample-client/index.html
に以下を追記する。
<div id="example">{{ message }}</div>
message
を表示するように SampleClient.kt
を変更する。
@JsNonModule @JsModule("vue") external class Vue(option: Json) fun run() { val vm = Vue(json( "el" to "#example", "data" to json( "message" to "Hello World" ) )) }
比較のため JavaScript で記述する場合を記載しておく。
var vm = new Vue({ el: '#example', data: { message: 'Hello World' } })
Kotlin と JavaScript の対応は以下になっている。
Kotlin | JavaScript |
---|---|
Vue() |
new Vue() |
json() |
{} |
"key" to value |
key: value |
Lodash の時との違いとしては、external val Vue: dynamic
ではなく external class Vue(option: Json)
としている。
Vue.js の場合にはモジュールオブジェクトは関数オブジェクトになっており、その関数はインスタンス生成に使用される。
val Vue
とした上で Vue(...)
とすると関数実行になるが、
class Vue
とした上で Vue(...)
とすればインスタンス生成になる。
client-sample/index.html
をブラウザで表示すれば、Hello World
と表示される。
しかし、vm
変数の型は dynamic
ではなく Vue
となる。
これにより、Vue
クラスに定義されていないプロパティを参照できなくなる。
例えば、Vue.js では JavaScript の場合には以下のように書くことができる。
var vm = new Vue({ el: '#example', data: { message: 'Hello World' } }) vm.message = 'Hello Kotlin World'
Kotlin/JS では class Vue
にプロパティを定義しなければならない。
しかし、以下の様に dynamic
型を使えばプロパティに定義しなくとも参照できる。
fun run() { val vm: dynamic = Vue(json( "el" to "#example", "data" to json( "message" to "Hello World" ) )) vm.message = "Hello Kotlin World" }
ソースコード
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>
ソースコード
Google App Engine スタンダード環境で Kotlin + Spring Boot を動かす
概要
2017.6.28 に Google App Engine スタンダード環境が Java8 をサポートしたと告知されました。 告知記事を読んでいると Kotlin + Spring Boot で動かすサンプルが目にとまったので、動かしてみました。
確認環境
- macOS 10.12.5
- Google Cloud SDK 161.0.0
- 158.0.0 でインストール後にアップデート
- App Engine Java 1.9.54
- IntelliJ IDEA Community 2017.1.4
参考情報
- Google Cloud Platform Blog: Google App Engine standard now supports Java 8
- 告知記事
- getting-started-java/appengine-standard-java8/kotlin-springboot-appengine-standard at master · GoogleCloudPlatform/getting-started-java · GitHub
- Google App Engine + Kotlin + Spring Boot のサンプル
- Gradle と App Engine プラグインを使用する | Java の App Engine スタンダード環境 | Google Cloud Platform
- Gradle で環境構築するための方法
解説
Google Cloud Platform
- プロジェクトを作成
(設定手順をメモしていませんでした…。)
Google Cloud SDK
手順は以下の通り。
ダウンロード後、ファイルの SHA256 チェックサムを検証してからファイルを展開。
install.sh を実行する前に好みのディレクトリに配置する。
筆者はユーザーのホームディレクトリに置いている (/Users/user_name/google-cloud-sdk
) 。
install.sh を実行することで、インストール済みコンポーネントを確認でき、コンポーネントのインストール/アンインストール方法を知れる。
また、環境変数 PATH
の自動設定を行える。
$ echo "6572e086d94ab2a6b3242a966b688a382a80a417101315d59d38ac804b5a366e google-cloud-sdk-158.0.0-darwin-x86_64.tar.gz" | shasum -a 256 -c google-cloud-sdk-158.0.0-darwin-x86_64.tar.gz: OK $ tar xvfz google-cloud-sdk-158.0.0-darwin-x86_64.tar.gz $ cd google-cloud-sdk $ ./install.sh ... Do you want to help improve the Google Cloud SDK (Y/n)? ... Modify profile to update your $PATH and enable shell command completion? Do you want to continue (Y/n)? ... [/Users/user_name/.bash_profile]: ...
途中の Y/n は全て初期設定 (Enter
) を選択。~/.bash_profile
にて PATH
の設定が追加される。
Terminal を起ち上げ直して、以下を続行。 Java Extension コンポーネントをインストールして、SDK をアップデートする。 その後にアカウントを初期化する。
$ gcloud components install app-engine-java $ gcloud components update $ gcloud init ... You must log in to continue. Would you like to log in (Y/n)? ... Please enter numeric choice or text value (must exactly match list item): google_cloud_platform_project_id ... API [compute-component.googleapis.com] not enabled on project [xxxxxxxxxxx]. Would you like to enable and retry? (Y/n)? ...
途中の Y/n は初期設定 (Enter
) を選択。ブラウザが起動して認証を求められる。
SDK の設定諸々は以下のコマンドで確認できる。
$ gcloud info ... Installation Root: [/Users/user_name/google-cloud-sdk] ...
IntelliJ IDEA でプロジェクト作成
- Gradle Project を作成
- appengine-web.xml 作成
- Java8 スタンダード環境を指定
- build.gradle 作成
- Application.kt 作成
- JsonObjectMapper の設定
- TemplateEngine の設定
- JsonObjectMapper 作成
- Controller, Template などを作成 (省略)
- appengineRun タスクで実行
- ローカル環境で動かす
- appengineDeploy タスクで配置
- Google の環境で動かす
appengine-web.xml, build.gradle, Application.kt, JsonObjectMapper は以下の Gist を参照のこと。
Google App Engine Standard Environment in Java8 wi …
Serializer/Deserializer クラスは ObjectMapper の addSerializer/addDesirializer メソッドに型を合わせるために用意している。
IntelliJ IDEA の Terminal にて、以下を実行すればローカル環境で試せる。
$ ./gradlew appengineRun ... 04, 2017 4:56:51 午前 com.google.appengine.tools.development.AbstractModule startup 情報: Module instance default is running at http://localhost:8080/ 7 04, 2017 4:56:51 午前 com.google.appengine.tools.development.AbstractModule startup 情報: The admin console is running at http://localhost:8080/_ah/admin 7 04, 2017 1:56:51 午後 com.google.appengine.tools.development.DevAppServerImpl doStart 情報: Dev App Server is now running
http://localhost:8080/
をダブルクリックで選択し、コンテキストメニューの Open as URL
を選択するとブラウザで開ける。
(もっと楽に起動できる方法は?)
同じく、以下を実行すれば Google Cloud Platform 環境に配置される。
$ ./gradlew appengineDeploy ...
配置後、gcloud app browser
とすることでデフォルトブラウザが開くが、デフォルトブラウザの設定が不明。
OS のデフォルトは Chrome に設定されているのに、Firefox が開く。
(どこに設定が?)
他の設定もしていた build.gradle から余計な設定を削除したため、もしかしたら削除し過ぎていて問題が起こるかも。 IntelliJ IDEA に Google Cloud Tools plugin をインストールしているが、無くてもよいはずなので記載していない。 Google Cloud Tools を使うことで App Engine に配置できるはずが機能しない。 (何か設定しないとだめ?)
IntelliJ IDEA で Emacs 風キーマップを作る
概要
IntelliJ IDEA の keymap 設定を Emacs にしても、使いたい機能のショートカットが複雑で覚えられません。 そこで、Emacs 風のショートカットに設定を変更します。
筆者は最初に覚えたエディタが Emacs でしたが、その後は Emacs のライトユーザーです。 ヘビーユーザーからしたら Emacs 風ではないと思われるかもしれません。
確認環境
- IntelliJ IDEA Community 2017.1.4
参考情報
解説
Emacs と言えば、ESC
キーと Ctrl
キーから連なるショートカットが特徴的かと思います。
IntelliJ IDEA ではショートカットキーに Second stroke を設定することにより、
Emacs 風のショートカットキーを設定できます。
私は、Ctrl-[
を ESC
キーとして使用しています(Emacs 標準のはず)。
そのため、Ctrl-[
, Ctrl-x
, Ctrl-c
, Ctrl-h
には Second stroke を設定しています。
Ctrl-x
と Ctrl-c
で使い分けがあったかもしれませんが無視することにして設定しています。
Ctrl-h
はヘルプ系のアクションを割り当てています。
また、Emacs とは異なりますが、Navigation 系のアクションを Ctrl-j
(Jump のつもり) に割り当てています。
基本的には、アクションの意味を表すようにキーを選択しているつもりです(Project ならば p
, Intention なら i
とか)。
使用頻度の低いと思われるアクションは Ctrl-[ x
で Find Action を起動して実行する方針です。
設定した内容を整理したスプレッドシートを公開しています。 設定を変更していないアクションについては記載していません。
アクションの一覧は intellJ IDEA Community のソースコードから取得しています。
intellij-community/ActionsBundle.properties at master · JetBrains/intellij-community · GitHub
IntellJ IDEA の便利機能をまだまだ知らないので、こんな便利機能があるよ!という話があれば伺いたいです。 また、Emacs の標準的なショートカットキーで、このキー設定は鉄板!という話があれば伺いたいです。