Scala.js
AltJSというと、TypeScriptなんかが一番の流行で、それは認めるところなんだけれど、Webpackなどを含む環境のセットアップがどうしても馴染めません。Scala.jsというのはそういう人のための(?)、最適なツールのように思えます。歴史は結構古いみたいで、いまだに開発は止まっていないようです。この時点の最新版は1.0.0-M6でした。
ドキュメントにはなんだか、日本で人気とあります。本当だろうか。
準備
クライアントだけの単独プロジェクト
ScalaとSBTとNodejsがインストールされている環境を前提としています。まずは以下のようにプロジェクトのひな型を準備します。
sbt new scala/scala-seed.g8
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.0.0-M6")
// 外部のJavascript参照のためのプラグイン(0.6系では不要)
addSbtPlugin("org.scala-js" % "sbt-jsdependencies" % "1.0.0-M6")
import Dependencies._
lazy val root = (project in file("."))
.enablePlugins(ScalaJSPlugin, JSDependenciesPlugin)
.settings(
inThisBuild(List(
organization := "com.example",
scalaVersion := "2.12.7",
version := "0.1.0-SNAPSHOT"
)),
name := "Tutrial"
)
scalaJSUseMainModuleInitializer := true
これでビルド環境の準備は完了です。
build.sbt
の最後のscalaJSUseMainModuleInitializer
は、当該オブジェクトをApplicationとして出力するためのもので、具体的には出力された-fastopt.js
の最後で、引数なしでMainメソッドが呼ばれるようになります。要するにエントリーポイントです。falseの場合はLibraryとして出力されます。
簡単なアプリを書いてみます(これは、ほとんどテンプレのままですが)
package example
object Hello extends Greeting {
def main(args: Array[String]): Unit = {
println(greeting)
}
}
trait Greeting {
lazy val greeting: String = "hello"
}
普通にSBTのコマンド実行で、Javascriptが出力され、runで実行も可能です。普通にScalaで実行しているように見えて、実は出力されたJavascriptをNode.js経由で実行しているのがミソです。ちなみにsbt fullOptJS
とするとGoogle Closure Compilerを使ったさらに最適化されたJavascriptコードが出力されます。
sbt fastOptJS
sbt run
ここでScalaTagsなどを使うことによって、動的にHTMLやJavascriptを生成することができます。サーバサイドを含めて簡易的に実行するためには、Workbenchプラグインを使うとよいでしょう。
Cross-Building環境
Workbenchを使ってもよいのですが、Scala.jsなんかを使う人は、サーバーサイドもScalaで書こうという言う人が多いのではないかと思われます。例えば、PlayとかAkka-Httpとかです。これについては、すでにgilter8でテンプレートが用意されています。
Scala.js 1.0以降はsbt.crossprojectを使って、サーバー、クライアントそれぞれをコンパイルするのがベストプラクティスのようです。
ちなみにウェブサーバーと一緒に使うためのプラグインとしては、sbt-web-scalajsというのを使うのが一般的のようです。
具体的には以下のようにserverと、clientを分離して、build.sbtに記載します。この際、同名のフォルダをプロジェクト内に作っておきます。ついでにsharedというフォルダを作って、両方のから参照できるようにしています。詳細は上記のテンプレートで確認してください。
addSbtPlugin("com.vmunier" % "sbt-web-scalajs" % "1.0.8-0.6")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.25")
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.6.0")
lazy val server = project.settings(
scalaJSProjects := Seq(client),
pipelineStages in Assets := Seq(scalaJSPipeline)
).enablePlugins(SbtWeb)
lazy val client = project.enablePlugins(ScalaJSPlugin, ScalaJSWeb)
Javascript Libraryの利用
Typescriptと同じように、既往のJavascriptライブラリを使ってScala.jsを安全にコンパイルする際には、型情報が必要になります。有名どころについては、Facadeが提供されていて、それを依存ライブラリに加えればよいのですが、1.0系用になっていなかったり、整備されていなかったりします。Typescriptの型情報をScala.jsに変換するscala-js-ts-importerというのもあるので、適宜使ってみるとよいでしょう。
とにかく、コンパイルするソースコードから、型情報を記載したscalaファイルが参照可能になっていることが肝要です。書き方については非常に分かりにくいのですが、ThreeJSのFacadeからサンプルを引いてみます。
@js.native
@JSGlobal("THREE.Camera")
class Camera extends Object3D {
var matrixWorldInverse: Matrix4 = js.native
var projectionMatrix: Matrix4 = js.native
override def lookAt(vector: Vector3): Unit = js.native
def clone(camera: Camera): Camera = js.native
}
上記のコードがあると、Scala.jsのコードからval camera = new Camera()
という感じでJavascriptのTHREE.Cameraクラスが扱えます。@JSGlobal("THREE.Camera")
というところが、それらを結びつけるアノテーションです(0.6以前は@JSName)。
Playによるサンプルアプリの作成
それでは、Playを使って簡単なThreeJSのアプリを作ってみます。以下のようにテンプレートからプロジェクトを生成します。ライブラリの依存関係が厳しそうなので、極力ライブラリのバージョンはいじらないほうが良いと思います。
sbt new vmunier/play-scalajs.g8
上述のように、内部にはclient, server, sharedというフォルダが生成されています。server以下は見慣れたPlayのフォルダ構成が見て取れますbuild.sbt
で参照されているScala.js-Scriptsがclientとserverのコードをつないでくれます。Scala.js-Scriptsは、具体的にはScala.jsの出力をTwirlやScalatagsといったテンプレートエンジンにつなぐものです。
import sbtcrossproject.{crossProject, CrossType}
lazy val server = (project in file("server"))
.settings(commonSettings)
.settings(
scalaJSProjects := Seq(client),
pipelineStages in Assets := Seq(scalaJSPipeline),
pipelineStages := Seq(digest, gzip),
// triggers scalaJSPipeline when using compile or continuous compilation
compile in Compile := ((compile in Compile) dependsOn scalaJSPipeline).value,
libraryDependencies ++= Seq(
"com.vmunier" %% "scalajs-scripts" % "1.1.2",
guice,
specs2 % Test
),
)
.enablePlugins(PlayScala)
.dependsOn(sharedJvm)
lazy val client = (project in file("client"))
.settings(commonSettings)
.settings(
scalaJSUseMainModuleInitializer := true,
libraryDependencies ++= Seq(
"org.scala-js" %%% "scalajs-dom" % "0.9.6"
),
jsDependencies ++= Seq(
"org.webjars" % "three.js" % "r88" / "r88/three.js"
)
)
.enablePlugins(ScalaJSPlugin, ScalaJSWeb)
.dependsOn(sharedJs)
lazy val shared = crossProject(JSPlatform, JVMPlatform)
.crossType(CrossType.Pure)
.in(file("shared"))
.settings(commonSettings)
lazy val sharedJvm = shared.jvm
lazy val sharedJs = shared.js
lazy val commonSettings = Seq(
scalaVersion := "2.12.7",
organization := "jp.ad.wide.hongo.kasuya"
)
// loads the server project at sbt startup
onLoad in Global := (onLoad in Global).value andThen {s: State => "project server" :: s}
ThreeJSの読み込みのためにjsDependenciesに追記をしています。この参照はWebJarsからとってきています。ここで追加されたファイルは、依存ライブラリとしてまとめてコンパイルされます。この例では、client/target/scala2.12/client-jsdeps.js
にまとめられます。
ThreeJS型情報のインポート
ThreeJSのFacadeを読み込もうと思ったら、リポジトリが死んでいたので、ソース丸ごとclient側のプロジェクトにインポートしました。あまりいいやり方ではないと思いますが、client/src/main/scala
フォルダに、対象コードをコピーします。ちなみにコンパイルの際には、@JSName
を@JSGlobal
に直せという警告がたくさん出ます。1.0系の場合はそれらを実際に変換して直さないとエラーで停止します。将来のためには、きちんと整備したほうが良いでしょう。
プログラムの動作原理
テンプレートで実行できるアプリケーションは、Playが分かれば簡単に読み解けると思います。MVCでいうところのControllerであるScalaJSExample.scala
が、ViewにあるTwirlテンプレートのviews.html.index
を呼び出しています。
@Singleton
class Application @Inject()(cc: ControllerComponents) extends AbstractController(cc) {
def index = Action {
Ok(views.html.index(SharedMessages.itWorks))
}
}
views.html.index
はその中で、main.scala.html
を呼び出しています。この中の@scalajs.html.scripts
ディレクティブが重要な要素で、生成されたJavaScriptおよびその依存関係にあるコードをロードする