scalajs-reactを使う
ScalaJSでReactを使う場合、scalajs-reactというFacadeを使うのが一般です。残念なことにscalajsのバージョン1.0系以降には対応していないので0.6系を使います。
準備
前のエントリのようにプロジェクトのひな型から作ります。
今回はWorkbenchとscalajs-bundlerのプラグインを使います。
WorkbenchはScalajsのアプリ用にローカルサーバを立ち上げて、HTMLを表示してくれます。scalajs-bundlerを使うと、jsDependencyとwebjarsを使って依存関係を解決するというオールドスクールなやり方をしないで済みます。Webjarsって便利だけど、参照がうまくいかなかったりするのでこの方が便利です。
sbt new scala/scala-seed.g8
// Sbt plugins
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.26")
addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.14.0")
addSbtPlugin("com.lihaoyi" % "workbench" % "0.4.1")
name := "reactjs-test"
version := "0.1"
scalaVersion := "2.12.6"
lazy val root = (project in file("."))
.settings(
scalaJSUseMainModuleInitializer := true,
libraryDependencies ++= Seq(
"org.scala-js" %%% "scalajs-dom" % "0.9.6",
"com.github.japgolly.scalajs-react" %%% "core" % "1.3.1"
),
npmDependencies in Compile ++= Seq(
"react" -> "16.5.1",
"react-dom" -> "16.5.1")
)
.enablePlugins(ScalaJSPlugin, WorkbenchPlugin, ScalaJSBundlerPlugin)
これで準備は完了です。
公式サイトに倣って、簡単なサンプルを実装します。
サンプルアプリ(Timer)
簡単な、というより公式のでもサイトのサンプルをそのまま実装しています。一応、内部でStateで状態管理しているのでReactっぽいコンポーネントなので、どのように実装すればよいかはわかるかと思います。
package scalajs
import org.scalajs.dom
import dom.document
import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.html_<^._
import scala.scalajs.js
object Timer{
def main(args: Array[String]): Unit = {
val Timer = ScalaComponent.builder[Unit]("Timer")
.initialState(State(0))
.renderBackend[Backend]
.componentDidMount(_.backend.start)
.componentWillUnmount(_.backend.clear)
.build
Timer().renderIntoDOM(document.body)
}
case class Point(x: Int, y: Int){
def +(p: Point) = Point(x + p.x, y + p.y)
def /(d: Int) = Point(x / d, y / d)
}
case class State(secondsElapsed: Long)
class Backend($: BackendScope[Unit, State]) {
var interval: js.UndefOr[js.timers.SetIntervalHandle] =
js.undefined
def tick =
$.modState(s => State(s.secondsElapsed + 1))
def start = Callback {
interval = js.timers.setInterval(1000)(tick.runNow())
}
def clear = Callback {
interval foreach js.timers.clearInterval
interval = js.undefined
}
def render(s: State) =
<.div("Seconds elapsed: ", s.secondsElapsed)
}
}
最初のTimerの変数宣言のところで、Reactのコンポーネントを指定しています。最初の値である0をCase ClassのStateとして生成し、initialeStateメソッドの引数としています。次のrenderBackendは下に示しているコンポーネントの動きを記述しているクラスを指定しています。同クラスのrender内に書かれている.<はScalajs-reactのDSLになります。
次のチェーンメソッドのcomponentDidMountとcomponentWillUnmountはReactコンポーネントのライフサイクルのフックになっているようですね。最後にbuildして使えるようにしています。
これをHTML内に挿入するのはrenderIntoDOMメソッドを使います。普通のReactだとReactDOM.render()とかやるところですね。
これを表示するHTMLは以下の感じです。何の変哲もないですが、scalajs-bundlerを使っているので、参照するJSファイルのパスが少し違います。ここは要チェックです。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script type="text/javascript" src="/workbench.js"></script>
<script type="text/javascript" src="./target/scala-2.12/scalajs-bundler/main/reactjs-test-fastopt-bundle.js"></script>
</body>
</html>
scalajs-bundler含めてのコンパイルとしては、以下のコマンドで行います。
fastOptJS::webpack
Workbenchが立ち上がっているので、http://localhost:12345/index-dev.htmlにアクセスすれば、単に時間をカウントするだけのアプリが表示されます。
所感
まぁ、できることはできますが、Scalajsに慣れていないこともあってとにかくとっつきにくかったです。DSLも相まって混乱を誘います。ただ、よくよく見ると合理性のあるつくりになっており、ScalaでPlayとの連携も容易なので、少し大きなプロジェクトだといい感じに使えるのだと信じています。