KotlinJSを動かす
エンジニアの原です。
先日Kotlin1.2がリリースされました。
バックエンド、Webフロントエンド、Androidでコードの共通化ができるようになったとこのことです。
今回はその機能は触らないんですが、前にリリースされたKotlinJSとKotlinNativeには興味がありました。
で最近はずっとReactNative, ReactSPAばっかりやってて、ふと思いました。
React NativeのAndroidのコードはKotlin化は余裕
KotlinJSでKotlinでJSが吐き出せる
KotlinNativeでiOSが動かせる
「これはReactNativeはKotlinだけで動かせるのでは?」
ということで、色々試してみます。
KotlinJSを動かす
まずここからです。先は長い。
とりあえずハロワ
プロジェクト作成
エントリポイントを作成
fun main(args: Array<String>) { console.log("Hello, World") }
⌘9でビルド
outディレクトリにコンパイル結果が入ってます。エラーは無かったようです。
HelloWorld.js
HelloWorld.js見てみましょう
if (typeof kotlin === 'undefined') { throw new Error("Error loading module 'HelloWorld'. Its dependency 'kotlin' was not found. Please, check whether 'kotlin' is loaded prior to 'HelloWorld'."); } var HelloWorld = function (_, Kotlin) { 'use strict'; function main(args) { console.log('Hello, World'); } _.main_kand9s$ = main; main([]); Kotlin.defineModule('HelloWorld', _); return _; }(typeof HelloWorld === 'undefined' ? {} : HelloWorld, kotlin);
Kotlinというグローバルオブジェクトが無いと駄目のようです。
kotlin.js
outディレクトリにlibというディレクトリもできており、そのなかにkotlin.jsというのがあります。
これをグローバルオブジェクトとして定義するとHelloWorld.jsが動きそうです。
実行
> const kotlin = require('./out/production/HelloWorld/lib/kotlin'); undefined > require('./out/production/HelloWorld/HelloWorld'); Hello, World {}
動きました。
他に何ができるかドキュメントを読んでみます。
JSのコードをKotlin上で書く
Calling JavaScript from Kotlin - Kotlin Programming Language
js()を使うとJSのコードが動くみたいです。ちょっと試してみましょう。
fun main(args: Array<String>) { val time: String = js(" new Date().toString()") console.log(time) }
コンパイル結果
if (typeof kotlin === 'undefined') { throw new Error("Error loading module 'HelloWorld'. Its dependency 'kotlin' was not found. Please, check whether 'kotlin' is loaded prior to 'HelloWorld'."); } var HelloWorld = function (_, Kotlin) { 'use strict'; function main(args) { var time = (new Date()).toString(); // 新しいコード console.log(time); } _.main_kand9s$ = main; main([]); Kotlin.defineModule('HelloWorld', _); return _; }(typeof HelloWorld === 'undefined' ? {} : HelloWorld, kotlin);
> const kotlin = require('./out/production/HelloWorld/lib/kotlin'); undefined > require('./out/production/HelloWorld/HelloWorld'); Wed Dec 06 2017 10:27:43 GMT+0900 (JST) {}
動きました。
nodeで動かす前提でkotlin.jsをrequireするコードも追加してみましょう。
fun main(args: Array<String>) { js("require('./out/production/HelloWorld/lib/kotlin')") val time: String = js(" new Date().toString()") console.log(time) }
コンパイル結果
if (typeof kotlin === 'undefined') { throw new Error("Error loading module 'HelloWorld'. Its dependency 'kotlin' was not found. Please, check whether 'kotlin' is loaded prior to 'HelloWorld'."); } var HelloWorld = function (_, Kotlin) { 'use strict'; function main(args) { require('./out/production/HelloWorld/lib/kotlin'); var time = (new Date()).toString(); console.log(time); } _.main_kand9s$ = main; main([]); Kotlin.defineModule('HelloWorld', _); return _;a }(typeof HelloWorld === 'undefined' ? {} : HelloWorld, kotlin);
> const kotlin = require('./out/production/HelloWorld/lib/kotlin'); undefined > require('./out/production/HelloWorld/HelloWorld'); Wed Dec 06 2017 10:37:43 GMT+0900 (JST) {}
動いたけど…違う。自動でrequireして欲しい…
調べてみるとKotlin Compilerの設定でCommonJSの形にもできるようです。
早速CommonJSにしてコンパイルしてみます。
(function (_, Kotlin) { 'use strict'; function main(args) { console.log('Hello, World'); } _.main_kand9s$ = main; main([]); Kotlin.defineModule('HelloWorld', _); return _; }(module.exports, require('kotlin')));
おっ、requireしてくれてる。 しかしデフォルトではlibにkotlin.jsが吐かれるのでこれだとエラーが出ます。
そこで先程のKotlin Compilerの設定でDestination directoryという設定がありましたが、これをnode_modulesに変更します。
コンパイルすると
(function (root, factory) { if (typeof define === 'function' && define.amd) define(['exports', 'kotlin'], factory); else if (typeof exports === 'object') factory(module.exports, require('kotlin')); else { if (typeof kotlin === 'undefined') { throw new Error("Error loading module 'HelloWorld'. Its dependency 'kotlin' was not found. Please, check whether 'kotlin' is loaded prior to 'HelloWorld'."); } root.HelloWorld = factory(typeof HelloWorld === 'undefined' ? {} : HelloWorld, kotlin); } }(this, function (_, Kotlin) { 'use strict'; function main(args) { console.log('Hello, World'); } _.main_kand9s$ = main; main([]); Kotlin.defineModule('HelloWorld', _); return _; }));
大分変わりましたね。
ちゃんとnode_modulesにコンパイル結果が入ってます。
実行してみます。
% node out/production/HelloWorld/HelloWorld.js Hello, World
やったぜ。
KotlinでNodeのサーバー立てる
JSのライブラリをKotlin側から使うのはどうするのでしょうか。
とりあえず雰囲気で書いてみる
interface Http { fun createServer(onRequest: (req: Any, res: Res) -> Unit): Proxy interface Proxy { fun listen(port: Int, callback: () -> Unit) } interface Res { fun writeHead(statusCode: Int) fun end(message: String) } } fun main(args: Array<String>) { val http: Http = js("require('http')") http.createServer({ _, res -> res.writeHead(200) res.end("Hello World") }).listen(8080, { console.log(it)}) }
コンパイルは通る。
実行してみる。
% node out/production/HelloWorld/HelloWorld.js /Users/Ryohlan/dev/kotlin/HelloWorld/out/production/HelloWorld/HelloWorld.js:37 http.createServer_jnnk04$(main$lambda).listen_n53o35$(8080, main$lambda_0); ^ TypeError: http.createServer_jnnk04$ is not a function at main (/Users/Ryohlan/dev/kotlin/HelloWorld/out/production/HelloWorld/HelloWorld.js:37:10) at /Users/Ryohlan/dev/kotlin/HelloWorld/out/production/HelloWorld/HelloWorld.js:43:3 at Object.<anonymous> (/Users/Ryohlan/dev/kotlin/HelloWorld/out/production/HelloWorld/HelloWorld.js:46:2) at Module._compile (module.js:635:30) at Object.Module._extensions..js (module.js:646:10) at Module.load (module.js:554:32) at tryModuleLoad (module.js:497:12) at Function.Module._load (module.js:489:3) at Function.Module.runMain (module.js:676:10) at startup (bootstrap_node.js:187:16)
ですよねー。
そもそもcreateServer_jnnk04$ってなってしまってるのでなんとかしないと
external
これはKotlinコンパイラにそれが生のJSインターフェースだと伝える手段です。
これをHttpにつけてコンパイルしてみます。
external interface Http { fun createServer(onRequest: (req: Any, res: Res) -> Unit): Proxy interface Proxy { fun listen(port: Int, callback: () -> Unit) } interface Res { fun writeHead(statusCode: Int) fun end(message: String) } } fun main(args: Array<String>) { val http: Http = js("require('http')") http.createServer({ _, res -> res.writeHead(200) res.end("Hello World") }).listen(8080, { console.log("localhost:8080")}) }
% node out/production/HelloWorld/HelloWorld.js localhost:8080
やったぜ。
ついでにrequireもexternalにします。
しかし問題が。
requireは読み込むモジュールごとに返り値が違うので通常だとコンパイルが通りません。
Dynamic Type
これはKotlinJSのための型定義です。
dynamic型を使うとその型はKotlinのタイプチェックから無視されるのでコンパイルが通ります。
今回だと
external fun require(path:String):dynamic fun main(args: Array<String>) { val http = require("http") http.createServer({ _, res -> res.writeHead(200) ...
このようにして使うとcreateServerは補完には出ませんがタイプチェックを無視されるのでコンパイルは通ります。
補完を効かせるためにhttpの型をつけて最終的には以下のようになります。
external interface Http { fun createServer(onRequest: (req: Any, res: Res) -> Unit): Proxy interface Proxy { fun listen(port: Int, callback: (message: String) -> Unit) } interface Res { fun writeHead(statusCode: Int) fun end(message: String) } } external fun require(path:String):dynamic fun main(args: Array<String>) { val http: Http = require("http") http.createServer({ _, res -> res.writeHead(200) res.end("Hello World") }).listen(8080, { console.log("localhost:8080")}) }
追記
@JsModuleでrequireいらず
ドキュメント見てたら@JsModuleを使うとrequire要らずということが分かりました。
なので更にスマートに
@JsModule("http") external object Http { // interfaceだと実装がないのでコンパイル通らない。 fun createServer(onRequest: (req: Any, res: Res) -> Unit): Proxy interface Proxy { fun listen(port: Int, callback: (message: String) -> Unit) } interface Res { fun writeHead(statusCode: Int) fun end(message: String) } } fun main(args: Array<String>) = Http.createServer{ _, res -> res.writeHead(200) res.end("Hello World") }.listen(8080, { console.log("localhost:8080")})
(function (_, Kotlin, $module$http) { 'use strict'; var Unit = Kotlin.kotlin.Unit; function main$lambda(f, res) { res.writeHead(200); res.end('Hello World'); return Unit; } function main$lambda_0(it) { console.log('localhost:8080'); return Unit; } function main(args) { $module$http.createServer(main$lambda).listen(8080, main$lambda_0); } _.main_kand9s$ = main; main([]); Kotlin.defineModule('HelloWorld', _); return _; }(module.exports, require('kotlin'), require('http')));
まとめ
思ってたより全然簡単でした。
JSのライブラリをKotlin側から呼ぶにはすべてのインターフェースを定義する必要があるので依存ライブラリが増えると大変そう…
次回はReactNativeのJSコードをKotlin書いてみたいと思います。