LoginSignup
18
16

More than 3 years have passed since last update.

Scala.jsことはじめ

Last updated at Posted at 2018-11-22

Scala.js

AltJSというと、TypeScriptなんかが一番の流行で、それは認めるところなんだけれど、Webpackなどを含む環境のセットアップがどうしても馴染めません。Scala.jsというのはそういう人のための(?)、最適なツールのように思えます。歴史は結構古いみたいで、いまだに開発は止まっていないようです。この時点の最新版は1.0.0-M6でした。
ドキュメントにはなんだか、日本で人気とあります。本当だろうか。

準備

クライアントだけの単独プロジェクト

ScalaとSBTとNodejsがインストールされている環境を前提としています。まずは以下のようにプロジェクトのひな型を準備します。

 sbt new scala/scala-seed.g8
project/plugins.sbt
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.0.0-M6")
// 外部のJavascript参照のためのプラグイン(0.6系では不要)
addSbtPlugin("org.scala-js" % "sbt-jsdependencies" % "1.0.0-M6")
build.sbt
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として出力されます。

簡単なアプリを書いてみます(これは、ほとんどテンプレのままですが)

src/main/scala/example/Hello.scala
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というフォルダを作って、両方のから参照できるようにしています。詳細は上記のテンプレートで確認してください。

project/plugins.sbt
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")
build.sbt
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からサンプルを引いてみます。

threejs.scala
@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といったテンプレートエンジンにつなぐものです。

build.sbt
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を呼び出しています。

ScalaJSExample.scala
@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およびその依存関係にあるコードをロードする

18
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
16