Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

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

はじめに

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

環境

Windows10
Intellij IDEA Ultimate(Communityでも問題なし)

AirFrameとは

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

プロジェクト作成

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

image.png

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

image.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"
)

こんにちわ世界

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

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
<!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で前もってタグを用意しておく形でも大丈夫です。

タグを書く

  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

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

Rxっぽいやつ

  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

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

おわりに

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

Setz
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away