Scala.jsでReactアプリケーションを作って公開したはなし
モチベーション
最近、流星のロックマン3というゲームに再燃しています。
Wi-Fiを使ってネット対戦を行うことができるのですが、任天堂公式のWi-Fiサービス終了により長らく対戦を行えない状況でした。
しかし、近年、有志の方がWi-Fiサーバをエミュレートするサーバを立てたことにより、再びネット対戦をすることが可能になりました。
さて、流星3には「シークレットサテライトサーバ」という、対戦中に使える特別なカードがあり、決められたカードテーブルからある程度狙ったカードを引くことができます。
しかし、カードテーブルの内容を全て記憶するのは困難であり、いちいち調べるのも面倒でした。
最近、Scala.jsに興味があることから、Webアプリケーションとしてこのシークレットサテライトサーバの早見表を作ってみようというのがきっかけです。
作ったもの
こちらで公開しています。
SSSのデータを読み込む部分の実装ができた。最低限使えるレベルにはなったのではなかろうか。 pic.twitter.com/ec7uX5ZkBx
— nomadblacky (@nomadblacky) July 23, 2020
アーキテクチャの話
ソースコードも公開しています。
Scala.js
いわゆるTypeScriptのようなAltJSの一種で、ScalaコードをJavaScriptにトランスパイルすることで、Scalaの強力な言語機能や静的型付けなど、その書き心地をそのままWebアプリケーションなどに持ち込むことができます。
2020年2月には待望のメジャーバージョンである v1.x 系がリリースされたりと、開発もそこそこ活発なのではないかと思います。
詳しい説明は色々な記事があると思うのでここでは語りません。
ちなみに、Scala.jsの概要を知るのにこのページが(面白くて)おすすめです。
Slinky
Scala.jsでReact.jsを扱うためのライブラリです。
似たライブラリに scalajs-react があります。
Slinkyを採用した理由としては、Webpackを用いた開発からパッケージングまでサポートされたgiter8テンプレートがありとっつきやすかったことです。
書き味としてはReact.js色が強いので予めReact.js公式のチュートリアルを触っておくと実装の感覚をつかみやすいと思います。
以下はソースコードの一部です。雰囲気を感じていただければと。
@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
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
TypeScriptのコードをScala.jsで使えるよう変換をしてくれるとても意欲的なsbtプラグインです。
Scala.jsでJavaScriptのライブラリを型安全に扱うにはFacadeというScalaとJavaScriptの世界を繋ぐものが必要になるのですが、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で作れたりしました。
ScalaとJavaScriptの世界を縮めてくれる、なかなか面白いツールだと思うので今後の動向に注目したいところです。
コードがお察しだしバグってるけどScala.jsでSlackのBoltアプリがなんか動いてしまった😂 pic.twitter.com/NvhGhCdM4o
— nomadblacky (@nomadblacky) July 1, 2020
actions-gh-pages
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コードを共有できる点があると思いますが、これはまだ試したことがないのでやってみたいところです。
気になるところとしてはやはり、ScalaとJavaScriptを繋ぐ点がどうしても黒魔術感が出てしまうところでしょうか。
ScalaでもJavaScriptでもない、「Scala.jsの知識」というものはどうしても必要になってきます。(まぁ当たり前といえばそうかも)
Scala.jsの世界に触れてみたい、という方はぜひ shadaj/create-react-scala-app.g8 から触れてみることをおすすめします。
$ sbt new shadaj/create-react-scala-app.g8
今回、アプリケーションに使うデータを作成するにあたり、攻略サテライト様のデータを使わせていただきました。この場を借りてお礼申し上げます。