Scala.jsとairframe-rxでさくさくWebフロントエンドを書く

airframe-rxが思った以上に良かったので紹介してみる。

# はじめに

当方Webフロントエンドは専門外で、今回使用するairframe-rxについても理解が十分とは言えません。マサカリお待ちしております。


# 環境

Windows10
Intellij IDEA Ultimate（Communityでも問題なし）


# AirFrameとは

公式サイトは[ここ](https://wvlet.org/airframe/)
「Essential Building Blocks For Scala」ということでAirFrameの名を冠したライブラリ群。
DI、RPC、REST、テスト用のライブラリを始めとして色々いっぱいあります。
今回は、その中でもRXのためのライブラリであるairframe-rx、およびairframe-rx-htmlを使ってWebフロントエンドを書いていきます。


# プロジェクト作成

まずは適当にScala.jsのプロジェクトを作成します。
Intellij IDEAでsbtの新規プロジェクトを作成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144437/63d83040-45d2-b3fe-ca20-f9d7fb67dd1b.png)

JDKとScalaのバージョンはとりあえず最新に。
sbtのバージョンは1.4系だとビルド時のhttpアクセスによるエラーの回避方法が分からなかったため、1.3系にしておきます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144437/acd4fc7a-270c-f0f1-6d1a-6c0790d115a5.png)

projectディレクトリ配下に下記のファイルを作成

```:plugins.sbt
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.3.0")
```

build.sbtファイルの末尾に空行を追加したあと、下記内容を追加

```
enablePlugins(ScalaJSPlugin)

scalaJSUseMainModuleInitializer := true

libraryDependencies ++= Seq(
  "org.wvlet.airframe" %%% "airframe-rx" % "20.10.3",
  "org.wvlet.airframe" %%% "airframe-rx-html" % "20.10.3"
)
```


# こんにちわ世界

下記ファイルを作成します

```scala:Main.scala
import org.scalajs.dom
import org.scalajs.dom.raw.Element
import wvlet.airframe.rx.html.DOMRenderer
import wvlet.airframe.rx.html.all.div

object Main {
  def main(args: Array[String]): Unit = {
    // 描画先のdivを作る
    val mainDiv: Element = dom.document.createElement("div")
    mainDiv.setAttribute("id", "main")
    dom.document.body.appendChild(mainDiv)

    DOMRenderer.renderTo(mainDiv, div("Hello world!"))
  }
}
```

で、下記コマンドを実行してコンパイル

```
sbt fastOptJS
```

すると、めでたくtargetディレクトリ配下に色々生成されます。
最終成果物は```target/scala-2.13/【プロジェクト名】-fastopt.js```です。
適当なところに下記のようなhtmlを作成

```html:html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>airframe rx sample</title>
</head>
<body>
<script type="text/javascript" src="【上記最終成果物のパス】"></script>
</body>
</html>
```

あとはこのhtmlをブラウザで表示すれば、めでたくHello world完了です。
Main.scalaの「描画先のdivを作る」の部分は、htmlで前もってタグを用意しておく形でも大丈夫です。


# タグを書く

```scala
  def main(args: Array[String]): Unit = {
    val mainDiv: Element = dom.document.createElement("div")
    mainDiv.setAttribute("id", "main")
    dom.document.body.appendChild(mainDiv)

    val contents = List(
      h1(
        cls -> "top",
        "適当なホームページ"
      ),
      ul(
        style -> "color: red",
        (1 to 3).map(i => li(s"項目$i"))
      )
    )

    DOMRenderer.renderTo(mainDiv, contents)
  }
```

これで

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144437/253ec6fd-dbe1-e869-cb71-2b81bcc10102.png)

こうなります。
clsでCSSクラスの指定、styleでスタイルの直接指定ですね。
リストだろうが単体のタグだろうがごちゃまぜでも対応してくれるので、かなりフレキシブルに書けます。


# Rxっぽいやつ

```scala
  def main(args: Array[String]): Unit = {
    val mainDiv: Element = dom.document.createElement("div")
    mainDiv.setAttribute("id", "main")
    dom.document.body.appendChild(mainDiv)

    val counter: RxVar[Int] = Rx.variable(0)

    val contents: Rx[RxElement] = counter.map { n =>
      List(
        button(
          onclick -> { _: dom.MouseEvent => counter := counter.get + 1 },
          "押した数をカウントするよ"),
        div(s"押された数：$n")
      )
    }

    DOMRenderer.renderTo(mainDiv, contents)
  }
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144437/2dbf795b-a717-0f00-d253-2c14b07bd449.png)

最初に見たときはどういうこと…？と思いましたが、counter.mapで定義しているブロックはcounterのリスナーとなっており、counterの値が変化するたびに再計算されるようです。
今回はクライアントに閉じた処理ですが、Airframe RPCを組み合わせることで、サーバとのやり取りに応じて表示を変更することも可能ですね（というか、それが本筋と思われる）


# おわりに

大体、上記のコードの組み合わせで必要な事はできるのではないかなあと思われます。リッチな表現などはまた別のライブラリ等に任せる話だと思うので。
他ライブラリもいくつか試してみたのですが、環境構築等で引っかかる点が多かったり、タグの記述が面倒だったり、Rxとかデータバインディング的な機能は持たなかったり…という感じで、airframe-rxが一番シンプルにやりたいことを達成できるのではないかというのが個人的な意見です。