Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save okunokentaro/aac5918dc0dd2539966e545f9b2b4f05 to your computer and use it in GitHub Desktop.
Save okunokentaro/aac5918dc0dd2539966e545f9b2b4f05 to your computer and use it in GitHub Desktop.
Scala.jsの出力をCommonJSモジュールとして扱う
2015/07/22 にQiitaに投稿した記事のアーカイブです
---
どうも、@armorik83です。先日ひとつの案件が終わり、次の案件まで少しばかり休暇があったので、その隙に[Scala.js](http://www.scala-js.org/)の学習を進めていました。[導入については前回の記事を参照](http://qiita.com/armorik83/items/b6528fffb705fc041a51)。
本稿では、前回の記事以降に解決したいくつかの疑問についてまとめます。
# 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から呼ばれることを想定している場合は、必須ではありません。
> http://www.scala-js.org/doc/export-to-javascript.html
```scala
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モジュールとして扱うには?
```Point.scala
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ソースにて一旦ラップします。
```js:point.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。
```js:es5.js
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](http://mochajs.org/)などを使います。
簡単に試してみただけの印象ですが、`@JSExport`漏れで動かないケースがしばしばあったので、二度手間でもJS側でのテストは必須でしょう。ただしAPIとして公開している部分の検証のみで済むので、隠蔽できるロジックはScalaの単体テストのみでかまいません。
---
以上、しばらく遊んでます。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment