ぶらっ記ぃ

日本語の練習をしています

Scala.jsでReactアプリケーションを作って公開したはなし

モチベーション

最近、流星のロックマン3というゲームに再燃しています。
Wi-Fiを使ってネット対戦を行うことができるのですが、任天堂公式のWi-Fiサービス終了により長らく対戦を行えない状況でした。
しかし、近年、有志の方がWi-Fiサーバをエミュレートするサーバを立てたことにより、再びネット対戦をすることが可能になりました。

さて、流星3には「シークレットサテライトサーバ」という、対戦中に使える特別なカードがあり、決められたカードテーブルからある程度狙ったカードを引くことができます。
しかし、カードテーブルの内容を全て記憶するのは困難であり、いちいち調べるのも面倒でした。

最近、Scala.jsに興味があることから、Webアプリケーションとしてこのシークレットサテライトサーバの早見表を作ってみようというのがきっかけです。

作ったもの

こちらで公開しています。

mmsf-hub.nomadblacky.dev

アーキテクチャの話

ソースコードも公開しています。

github.com

Scala.js

www.scala-js.org

いわゆるTypeScriptのようなAltJSの一種で、ScalaコードをJavaScriptにトランスパイルすることで、Scalaの強力な言語機能や静的型付けなど、その書き心地をそのままWebアプリケーションなどに持ち込むことができます。
2020年2月には待望のメジャーバージョンである v1.x 系がリリースされたりと、開発もそこそこ活発なのではないかと思います。
詳しい説明は色々な記事があると思うのでここでは語りません。

ちなみに、Scala.jsの概要を知るのにこのページが(面白くて)おすすめです。

tototoshi.github.io

Slinky

slinky.dev

Scala.jsでReact.jsを扱うためのライブラリです。
似たライブラリに scalajs-react があります。

Slinkyを採用した理由としては、Webpackを用いた開発からパッケージングまでサポートされたgiter8テンプレートがありとっつきやすかったことです。

github.com

書き味としてはReact.js色が強いので予めReact.js公式のチュートリアルを触っておくと実装の感覚をつかみやすいと思います。

reactjs.org

以下はソースコードの一部です。雰囲気を感じていただければと。

@react class CardTableComponent extends StatelessComponent {
  case class Props(cardTable: CardTable, selectedCardIndexes: Set[Int])

  private type IndexInCardTable = Int
  private type CardWithIndex    = (BattleCard, IndexInCardTable)

  def render(): ReactElement =
    div(className := "table")(
      props.cardTable.cards.zipWithIndex
        .grouped(5)
        .zipWithIndex
        .map { case (row, i) => renderRows(row, i) }
        .toSeq
    )

  private def renderRows(row: Iterable[CardWithIndex], rowIndex: Int): ReactElement =
    div(key := rowIndex.toString, className := "row")(
      row.map { case (card, cId) => renderCard(card, cId) }
    )

  private def renderCard(card: BattleCard, index: IndexInCardTable): ReactElement = {
    val bg = if (props.selectedCardIndexes.contains(index)) "lightgreen" else "inherit"
    div(
      key := index.toString,
      id := index.toString,
      className := "card",
      style := js.Dynamic.literal(background = bg)
    )(card.name)
  }
}

scala-js-bundler

scalacenter.github.io

Scala.js向けにWebpackを扱うためのsbtプラグインです。
Webpackについてはここでは語りません(語れません)

npmのライブラリ管理や、Webpackを使ったパッケージングなどを行ってくれます。

以下はScala.jsアプリケーションをパッケージングする例です。
./build ディレクトリに吐き出されるので、これをWebサーバなどに配置すればアプリケーションを公開できます。

sbt:mmsf-hub> fullOptJS::webpack
[info] Full optimizing /home/blacky/projects/scala/mmsf-hub/target/scala-2.13/scalajs-bundler/main/mmsf-hub-opt.js
[info] Closure: 0 error(s), 0 warning(s)
[info] Writing scalajs.webpack.config.js
[info] Bundling the application with its NPM dependencies
[info] ℹ 「wdm」: Compiling...
[info] ℹ 「wdm」: Hash: 0b2fc06d929fe96b0158
[info] Version: webpack 4.43.0
[info] Time: 100ms
[info] Built at: 2020-07-24 0:36:07
[info]      Asset      Size  Chunks             Chunk Names
[info] index.html  1.49 KiB          [emitted]
[info]  + 1 hidden asset
[info] Entrypoint mmsf-hub-fastopt = mmsf-hub-fastopt.js
[info] [./scalajs-entry.js] 435 bytes {mmsf-hub-fastopt} [built]
[info]     + 231 hidden modules
[info] ℹ 「wdm」: Compiled successfully.
[warn] asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
[warn] This can impact web performance.
[warn] Assets:
[warn]   mmsf-hub-opt.js (439 KiB)
[warn] entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
[warn] Entrypoints:
[warn]   mmsf-hub-opt (439 KiB)
[warn]       mmsf-hub-opt.js
[warn] webpack performance recommendations:
[warn] You can limit the size of your bundles by using import() or require.ensure to lazy load some parts of your application.
[warn] For more info visit https://webpack.js.org/guides/code-splitting/
[info] Version: 4.43.0
[info] Hash: 90553944aab77f6c1bf7
[info] Time: 6835ms
[info] Path: /home/blacky/projects/scala/mmsf-hub/build
[info] Built at 2020-07-24T00:36:15.132342
[info]               Asset      Size                       Chunks
[info]    data/servers.csv     51562   [emitted]               []
[info]         favicon.ico    143480   [emitted]               []
[info]           index.css       725   [emitted]               []
[info]          index.html       502   [emitted]               []
[info]       manifest.json       315   [emitted]               []
[info]     mmsf-hub-opt.js    449853   [emitted]   [mmsf-hub-opt]
[info] mmsf-hub-opt.js.map   1299361   [emitted]   [mmsf-hub-opt]
[success] Total time: 15 s, completed 2020/07/24 0:36:15

ScalablyTyped

scalablytyped.org

TypeScriptのコードをScala.jsで使えるよう変換をしてくれるとても意欲的なsbtプラグインです。
Scala.jsでJavaScriptのライブラリを型安全に扱うにはFacadeというScalaJavaScriptの世界を繋ぐものが必要になるのですが、ScalabyTypeはそのFacedeの作成を自動で行います。

以下は使用例です。(この csv-parser の例だとあまり型安全感は出ていませんが…)

build.sbt

enablePlugins(ScalaJSBundlerPlugin, ScalablyTypedConverterPlugin)

npmDependencies in Compile ++= Seq(
  // ...
  "csv-parse"   -> "4.11.1"
)

Main.scala

import typings.csvParse.mod.Options
import typings.csvParse.{libSyncMod => parseCsv}

// ...

object Main {
  // ...
  private def loadApp(xhr: XMLHttpRequest, container: Element): Unit = {
    val rawServers =
      parseCsv(
        xhr.responseText,
        js.Dynamic.literal(columns = true).asInstanceOf[Options]
      ).asInstanceOf[js.Array[js.Dictionary[String]]]

    val servers = rawServers.map { dict =>
      Server(
        id = dict("id").toInt,
        serverType = ServerType.values(dict("type").toInt),
        level = dict("level").toInt,
        name = dict("name"),
        cardTable = CardTable(parseCards(dict("cards")))
      )
    }.toSeq

    ReactDOM.render(SSSViewer(servers), container)
  }
}

ちなみに、このScalablyTypedを使ってSlackのBoltアプリケーションをScala.jsで作れたりしました。
ScalaJavaScriptの世界を縮めてくれる、なかなか面白いツールだと思うので今後の動向に注目したいところです。

actions-gh-pages

github.com

GitHub Actions を使ってアプリケーションをGitHub Pagesに公開するのに使いました。 使い方は簡単で、アプリケーションのルートになるディレクトリを指定するだけです。gh-pagesブランチへのコミットはなどを自動で行ってくれます。

今回は、scala-js-bundler のパッケージング結果である ./build を指定しました。

name: GitHub pages

on:
  push:
    branches:
      - master

jobs:
  deploy_to_gh-pages:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - uses: actions/setup-java@v1
        with:
          java-version: '11'

      - uses: actions/setup-node@v2-beta
        with:
          node-version: '12'

      - name: Cache Coursier and node_modules
        uses: actions/cache@v2
        with:
          path: |
            ~/.cache/coursier
            .target/scala-2.13/scalajs-bundler/main/node_modules
          key: ${{ runner.os }}-cache-${{ github.run_id }}
          restore-keys: |
            ${{ runner.os }}-cache-
      - name: Build App
        run: ./sbt fullOptJS::webpack

      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./build

最後に

最近Scala.jsを触れた身ではありますが、簡単なWebアプリケーションを作って公開するところまでできました。
エコシステム周りもだいぶ充実してきている印象で、Scalaに書き慣れた方はWebアプリケーションを実装するひとつの選択肢として考えても良いのではないでしょうか。
Scala.jsで実装する利点のひとつとして、サーバとクライアントでScalaコードを共有できる点があると思いますが、これはまだ試したことがないのでやってみたいところです。

気になるところとしてはやはり、ScalaJavaScriptを繋ぐ点がどうしても黒魔術感が出てしまうところでしょうか。
ScalaでもJavaScriptでもない、「Scala.jsの知識」というものはどうしても必要になってきます。(まぁ当たり前といえばそうかも)

Scala.jsの世界に触れてみたい、という方はぜひ shadaj/create-react-scala-app.g8 から触れてみることをおすすめします。

$ sbt new shadaj/create-react-scala-app.g8

今回、アプリケーションに使うデータを作成するにあたり、攻略サテライト様のデータを使わせていただきました。この場を借りてお礼申し上げます。

wily.xrea.jp