Scala.js の出力を CommonJS モジュールとして扱う
どうも、@armorik83 です。先日ひとつの案件が終わり、次の案件まで少しばかり休暇があったので、その隙にScala.jsの学習を進めていました。導入については前回の記事を参照。
本稿では、前回の記事以降に解決したいくつかの疑問についてまとめます。
Q. Scala.js が出力する JS はとても読めない?
sbt fastOptJS
これで解決しました。前回はsbt fullOptJS
の結果だけで判断してしまいましたが、あれは minify したのと同様にかなり圧縮されていただけで、デバッグ中はfastOptJS
を使えばよさそう。同時に sourcemap も出力されますが、デバッガが変数の中身を表示しなくなるので個人的には使っていません。
10 行にも満たない.Scala
が 2000 行を超える.js
として出力されますが、ランタイムは気合があれば読めます。
Q. JSApp を使わなければならない?
これは誤解でした。import scala.scalajs.js.JSApp
して、JSApp
を継承したobject
内のmain()
がブラウザでのロードに合わせて呼び出されるというだけで、ライブラリのように他の JS から呼ばれることを想定している場合は、必須ではありません。
package example
import scala.scalajs.js
import js.annotation.JSExport
@JSExport
object HelloWorld {
@JSExport
def main(): Unit = {
println("Hello world!")
}
}
このように@JSExport
アノテーションを与えることで JS 側から呼び出せるコードとして出力されます。
Q. JSExport を CommonJS モジュールとして扱うには?
package pointPackage
import scala.scalajs.js
import js.annotation.JSExport
import scala.annotation.meta.field
@JSExport
class Point(
@(JSExport @field) var x: Int,
@(JSExport @field) var y: Int) {
@JSExport
def move(dx: Int, dy: Int) = {
x += dx
y += dy
}
}
上のような(あまりよい例とは言えない)Scala ソースがあるとします。package 名は JS 変換時の対照を分かりやすくするためで、これもよい命名ではない。
これを次の JS ソースにて一旦ラップします。
var vm = require('vm');
var fs = require('fs');
var code = fs.readFileSync(__dirname + '/target/scala-2.11/point-fastopt.js').toString();
var sandbox = {};
vm.runInNewContext(code, sandbox, 'point-fastopt.js');
module.exports = sandbox.__ScalaJSExportsNamespace.pointPackage;
Scala.js の@JSExport
は問答無用でglobal
に出力されます。この出力先を制御するために__ScalaJSEnv
を参照するようランタイムが組まれているのですが、その__ScalaJSEnv
自体もグローバル変数のため、万が一 Scala.js 由来の JS ライブラリが混ざってしまったときに汚染が起こる恐れがあります。
vm.runInNewContext
によって安全なスコープ内でグローバル展開ができるので、そこで Scala.js の Export を受け取り、sandbox.__ScalaJSExportsNamespace.pointPackage
の箇所でその中身を CommonJS として Export しています。ES 2015 module として Export したければ、そのように書き換えるとよいでしょう。なお、このアイデアは mizchi 氏のものを拝借しています。(thx @mizchi!)
使うときは普通に npm モジュールを扱う感覚で OK。
var Point = require('./point').Point;
var p = new Point(1, 2);
p.move(3, -5);
console.log('x:', p.x, 'y:', p.y);
// x: 4 y: -3
そりゃこれだけだったら全部 JS で書いたほうが早い。
Q. テストはどうするの?
テストは二種類実施するのがよいと捉えています。一つは Scala ソースとしてのテスト、これは JS 出力をせずに他の Scala と同じように行います。この辺のテスト事情はまだ探っている最中なので、本稿では解説しません。
もうひとつは e2e テスト。ここでいう e2e は DOM やブラウザを用いるテストではなく、プロダクトがきちんと JS 環境で動くかどうかのテストという意味です。ここでは JS の領域なのでMochaなどを使います。
簡単に試してみただけの印象ですが、@JSExport
漏れで動かないケースがしばしばあったので、二度手間でも JS 側でのテストは必須でしょう。ただし API として公開している部分の検証のみで済むので、隠蔽できるロジックは Scala の単体テストのみでかまいません。
以上、しばらく遊んでます。
Discussion