ScalaJS + PlayFrameworkでチャットを実装した話

 

こんにちは、Webフロントエンドエンジニアの奥山です。

世間のWebフロントの開発ではJavascriptがよく使われていますが、プロトタイプベース・動的型付けなのでとても柔軟に書けて、高速な開発ができるというメリットがある反面、人によるコードの差異が大きく、バグを生みやすい、メンテナンス性が低いといったデメリットに悩まされることが多いと思います。

Scalaはクラスベース・静的片付けの関数型言語なのでバグを生みづらく、またIDEのサポートが充実しているのでリファクタリングも効率良く行えてメンテナンス性が高いので、Scala.jsを使ってJavascriptに潜むデメリットを解決できるかもしれません。

今回は、プロジェクトへの導入の検討を兼ねて、Scalaのみで簡単なチャットが出来るWebアプリケーションを作る過程を通してメリット・デメリットを探ってみました。
それでは、主にScala.jsのメリットやデメリット、どんなプロジェクトに導入できそうかについて書いていきたいと思います。

abc.gif

Scala.jsとは

Scala.jsはAltJSの一種です。要するにScalaでjavascriptが書けます。

開発環境

Scala.jsの開発を始めるためには、JDKとsbtが必要で、またエディタにはIntelliJ IDEAを使うのでこれらをインストールします。
Macでbrew($ brew –version が0.9.5以上)を使っている人は以下の手順で入ります。

$ brew tap caskroom/versions
$ brew cask install java8
$ brew install sbt@1
$ brew cask install intellij-idea-ce

Scalaプラグインの関係で、IntelliJ IDEAは2017.2.7以上のバージョンにアップデートしてください。

sbt(0.13.13以降のバージョン)にはテンプレートからプロジェクトを作成する機能があり、scalajs + playのプロジェクトを作成してくれるテンプレートがあったためそれを利用します。xxxyyyは用途にあわせて変更してください。

$ sbt new vmunier/play-scalajs.g8 --name=xxx --organization=jp.co.yyy

これでできた(build.sbtがある)ディレクトリに移動し、sbtコマンドでsbtを起動し、runコマンドでplayのサーバーが http://localhost:9000 で起動します。
サーバーはEnterキーで停止し、sbtはexitと入力するか、Ctrl+Dで終了します。

今回検証で作ったWebアプリケーションは、ここからコードを追加・修正していきました。

メリット

Scala.jsを使うと、以下のようなメリットがあると思います。

実行時エラーの低減

こんなコードは書かないですが、例としてこういうコードの場合javascriptは実行時になるまでエラーに気づきませんが、Scalaだとコードを書いてるときに発見できます。

// javascript
const something = {
  date: new Date(),
};
something.date = null;
something.date.getTime();  // 実行時にようやくエラーが出てここまでに書いたコードのどこかが間違っていることが判明する
// scala
case class Something(date: Date)
val something = Something(date = new Date())
something.date = null  // 'reassignment to val' というエラーが出るのでこのコードを書いた時点でエラーが判明する
something.date.getTime()

副作用をもたない設計

Scalaのコレクションというのはjavascriptにおいては配列や連想配列のようなものですが、immutable(不変)コレクションはjavascriptにはない仕様で、一度定義したら値を変更できない配列や連想配列のようなものです。
例えば与えられた数字の配列のそれぞれの要素を2倍するメソッドを作るとして、javascriptでは副作用を持った設計にできてしまいます。

// javascript
const numbers = [1, 2, 3];
function multiply2(array) {
  for (var i = 0; i < array.length; i++) {
    array[i] = array[i] * 2;
  }
}
multiple2(numbers);
console.log(numbers);  // -> (3) [2, 4, 6]

このくらいの話であれば、以下のように書く方がいいですが、どちらがいいかという話ではなく、immutableなコレクションでは上のような副作用をもった設計はできないので可読性が上がります。

// javascript
const numbers = [1, 2, 3];
const multiply2 = n => n * 2;
const multiplied = numbers.map(multiply2);
console.log(multiplied);  // -> (3) [2, 4, 6]
// scala
val numbers = List(1, 2, 3)
def multiply2(n: Int) = n * 2
val multiplied = numbers.map(multiply2)
println(multiplied)  // -> List(2, 4, 6)

Scalaの標準ライブラリが便利

例えばJavascriptの連想配列には、自身のキーの配列や値の配列を返すメソッドはありませんが、ScalaはMapのインスタンスに対して.keys.valuesでそれができます。
他にも配列には、.map.foreachはもちろん、配列を頭から、直前の結果を保持しながら順番に計算していく畳み込み.foldLeftが用意されていたり、配列の要素を任意の個数ずつまとめた配列を生成する.grouped、任意の数でスライドしていく.slidingなど、どんな操作をしているのかがわかりやすく、副作用を生まない便利なメソッドが充実しています。
可能な限り行いたい操作に意味が近いもの使い、副作用を起こさないようにコードを書くことで、バグ耐性やメンテナンス性は大きく向上します。

モデル・値の共有

ScalaのみでWebアプリケーションを作る場合は、送受信するデータのモデルが共有できたり、値を簡単に共有することもできます。
共通モデルの定義は開発手順にしたがって作ったものだとsharedプロジェクトに配置することでserver, clientプロジェクト両方から参照できるようになります。

IDEのサポート

静的型付け言語はIDEのサポートが充実していることが多く、例えば複雑なクラスでも使えるフィールド、メソッド、継承関係などがわかりやすく便利です。以下はIntelliJ IDEA CEでScalaを使ってDateクラスを使う例です。

スクリーンショット 2017-11-18 14.38.46.png

Macだと、Command+Bで定義に移動できます。

cmdb.gif

マウスオーバーで型のポップアップも出せるので、謎の変数も、なんなのか一目瞭然です。(設定すれば)

nazo.gif

また、リファクタリングが圧倒的に簡単にできます。複数ファイルにまたがる場合、javascriptでは簡単にリファクタリングする方法がありませんが、ScalaはIntelliJの力を借りて簡単にリファクタリングできます。特に命名で悩んだ時に、一旦名前をつけてあとから再考して変更する、という手法が使えるので時間に追われてもあとでリカバリーが楽ですし、メソッドのシグネチャを一気に変更できる場合もあります。

refactor.gif

他のファイルも同時に変更されているのがわかると思います。

デメリット

学習コストが高い

Webアプリケーションの開発をするにあたっては、最低限html, Javascript, CSSの知識があれば大丈夫なのですが、Scala.jsの場合さらにScalaとsbtを覚えて、Scala.jsの使い方を勉強しなければいけないため、Javascriptのように誰もが簡単には始められません。その上、例えば今回のようにscalajs-reactを使う場合は、元となるReact.jsに関する知識もある程度必要になりますし、scalajs-react独自のシンタックスを覚える必要もあります。

また言語の特徴も大きく違うため、Javascriptでの設計思想やノウハウが、Scala.jsでは全然違う形になったりします。

さらっと「Scalaとsbtを覚えて」と書きましたが、一般的にScalaは学習コストが高いと言われているので、Scalaに触れたことがない、JVM系の言語に親しくない人が多く開発に関わる場合、かなり致命的なデメリットとなりそうなので注意が必要です。
「身近に質問できる人がいますか?」というチェック項目が重要なポイントとなりそうです。

開発をはじめるまでに時間がかかる

javascriptの場合、ブラウザにjsファイルを読み込ませれば動きますが、Scala.jsはJVMのインストール、sbtのインストールなどの環境構築が必要で、そのあとScalaのプログラムをjavascriptに変換しなければいけません。

実際の開発では、javascriptもそれ単体で開発することはもはやほとんどなく、yarnやwebpackなどのツールを使って開発することが基本になってきているので、javascriptといえど環境構築をする手間が必要になるので、実際のところあまり差はないかもしれません。

Scala.js向けのライブラリが比較すると少ない

標準ライブラリはScalaの方が圧倒的に充実しているのですが、javascript向けのライブラリをScalaで使う場合、そのメリットを最大限受けるためにはjavascriptライブラリの型定義を作成する必要があります(別になくても使えますが型がつきません)。
有名なライブラリはScala.jsでも既にたくさんありますが(ex. jQuery, React.js, D3.js, etc…)、本家のjavascriptよりは少ないです(当然)。

「学習コストが高い」でも触れたのですが、ライブラリは独自のシンタックスを使っていることもあるので、覚えなければいけないことも増えることがあります。下は、JSXシンタックスとscalajs-reactの比較です。

// JSX
<input
  type="text"
  value={xxx}
  onChange={(event) => /* 副作用 */}
/>
// Scala.js
<.input(
  ^.`type` := "text",
  ^.value := xxx,
  ^.onChange ==> ((event: ReactEvent) => /* 副作用 */)
)

PlayFrameworkとは

JavaとScala向けのWebフレームワークです。
チャットツールのバックエンドではJsonWebTokenでの認証、WebSocketでの双方向リアルタイム通信を実装しました。

JsonWebToken

大雑把に言うと、認証されたユーザーに対して、ユーザーを識別できる情報と期限と、それを暗号化した情報をくっつけたトークンのことを言います。
今回はこれをユーザーに返却し、各APIアクセスごとに送られてくるJWTを復号化したデータがユーザーを識別できる情報と一致しているかどうかで認可を行います。サーバーサイドの実装を解説することを目的とした記事ではないので、楽に実装できるという理由で認可やセッション管理にJWTを使っていますが、JWTを使う場合は注意が必要だと(絶対使うべきでないとさえ)説明している記事もネット上で見受けられるので、セキュリティまわりで注意が必要な場合、改めて見直してみてください。

WebSocket

WebSocketは専用のws/wssプロトコルによって全ての通信を完結させることができるため、占有するコネクションが1つで済むことや、プロトコル自体が軽量なので通信ロスが減ることなどがメリットです。PlayFrameworkではWebSocketをサポートしているので、難しい知識がなくてもドキュメントを読めば実装できます。しいていうならば、PlayFrameworkの書き方に少し慣れがひつようかもしれないです。
ブラウザ側の実装も(執筆時点で実験段階と書いてあるが)難しい知識は不要で、new WebSocket('ws://xxx')と書くだけでWebSocket通信が開始され、生成されたインスタンスのopen, message, error, closeイベントをハンドリングすればサーバーからの通知を反映することができ、sendメソッドでメッセージの送信ができます。こちらもドキュメントを読めば実装できると思います。

簡単なチャットができるツール

冒頭のgifが今回実装したツールですが、

  1. ユーザー登録
  2. ログイン
  3. ルームの作成/参加
  4. メッセージの送信

を行っています。基本的には上に述べた技術を使うと実装できます。UIも含め全てScalaで実装しました。

使用ライブラリなど

scalajs-react

名前の通りreact.jsのScala.jsバインドです。とはいえ書き方はもはや別物です。
ドキュメントを読めばまぁだいたいわかるのですが、React.jsとはコンポーネントの書き方がだいぶ違うので読まないとわからないと思います。

一部紹介するとこんな感じです。

object NotFound {
  val notFound = ScalaComponent.builder[Props]("NotFound")
    .stateless
    .renderStatic(<.h1("Page not found."))
    .build

  def apply(props: Props) = notFound(props)
}

Javascriptだとコンポーネントを<Component {...props} />のように書きますが、Scala.jsで上のようなコンポーネントを作った場合NotFound(props)のように使います。

状態をもたないので.statelessですが、状態がある場合.initialStateなどで初期化します。

.renderStaticの他に.renderP.renderPSなどのメソッドなどが用意されており、使うものだけに記述を止めることができます。

.componentWillMount.componentShouldUpdateなどといったライフサイクルメソッドも用意されていて、基本React.jsでできるのにscalajs-reactでできないといったことはありません。

BackendScopeを用いた書き方もあり、modStateで状態の変更ができたり、複数のライフサイクルメソッドなどにまたがる冗長な処理を一箇所にまとめて書いたりできます。

ScalaCSS

scalajs-reactはInline Stylesをサポートしているのですが、Inline Stylesのデメリットとして、pseudo要素が使えなかったり、アニメーションが弱いので、しっかりと開発をするなら何かしらの方法でcssを書いた方がいいです。
とはいえcssを直書きするにしても、css modulesやsassなどを使うにしても、VDOM内にclassNameを文字列でベタ書きするか、変数にして使うにしろcssとScala.jsの二箇所に書かないといけなくなりバグとメンテナンス性低下の匂いがします。

/* css */
.hoge {
  xxx: xxx
}
// scala
val hoge = "hoge"
<.div(
  ^.className := hoge,
)

ScalaCSSを使うと、Scalaで書いたスタイルを直接css文字列に変換することができ、またVDOM内ではスタイルの変数を指定することで読み込んだスタイルを参照できるため、変更や複数箇所での使用に対して安全です。
またcssのattributeのうち基本的なものは安全に書くことができます。keyframesアニメーションやpseudo要素にも対応しています。

// 使用例
import scalacss.DevDefaults._
object Example extends StyleSheet.Inline {
  import dsl._

  val max = 100.%%

  val hoge = style(
    width(max),
    height(max),
    display.flex,
    justifyContent.center,
    alignItems.center,
  )
}
// vdomとか
<.div(
  Example.hoge,
)
// スタイル書き出し
Example.render  // => cssの文字列が返る。ファイルに書き出したり、ルーティング先のAPIに渡すなどして提供する

これらはすべてScalaのコードとして扱われるため、例えばScala.jsとScalaCSS間でアニメーション時間などの変数を共有するといったこともできます。

Material icons

オープンソースのアイコンです。使いたいアイコンを選択して、書き出し方法(svg, pngや倍率)を指定してDLできます。

Scala.jsを導入する際の判断ポイント

あくまで僕の意見ですが、Scala.jsをプロジェクトに導入するかどうかは、以下に挙げる項目で判断するとよさそうです。
技術的には、javascriptでできるのに、Scala.jsだとできないことはないです。

Scalaに詳しい人

Scalaは学習コストが高いと言われていますし、僕自身そう感じるのは、知らないと書けない系の状況が多発するからだと思っていて、そういう場合ネットでの最適な検索ワードもなかなか思いつかず時間を無駄にしてしまったり、書かなくてもいいコードを大量に書いてしまったりするので、身近にScalaの質問を気軽にできる人がいるかどうかは重要なポイントだと思います。ググり力が高い人がいるなら、特に問題にならないかもしれません。

プロジェクトの規模

ごく少人数で短期のプロジェクトであれば、得意な言語を採用した方が効率よく走りきることができると思います。
逆に、大人数で長期にわたるプロジェクトでは、Scalaのような言語を採用した方が後々効率と安定性が高くなると思います。

運用期間

運用期間が長くなると、他の人が書いたコードを変更するときにバグを生む可能性が高いので、そのような場合にScalaのメリットを活かせるかもしれません。

おすすめのプロジェクト

例えば、サーバーサイドをすでにScalaで書いていて、ある程度複雑な、UIに強いこだわりがない管理画面を作ろうとしている場合などは導入するといいかもしれません。

まとめ

今回はチャットツールの実装にScala.jsを使いましたが、管理画面の実装など複雑なものでも実装することができます。
導入するときに判断すべきことはありますが、プロジェクトによっては高い効果を生む場合もあると思います。
余裕があれば、一度導入を検討してみてはいかがでしょうか。
検証で作ったコードは、GitHub(https://github.com/akzero53/abc) に置いてあるので、興味のある方は遊んでみてください。