社内システムでのClojureScript+re-frame活用事例

こんにちは:) Pairsエンジニアの竹内です!
この記事は、eureka Engineering Advent Calendar 2017 4日目の記事です。
前回は大久保さんの分析クエリを「速く・楽に・正確に」書くためにスニペットに登録すべきもの5選でした。

はじめに

先日弊社で開催されたeureka Meetup #07 -フロントエンドエンジニアリング-にてClojureScript+re-frameで社内アプリケーションを開発した話と言った題で登壇させていただいたのですが、
大変嬉しい事に当日の懇親会にて「より実践的な内容も聞きたいので、ぜひAdventCalendarでまとめてほしい」と言った声を頂いたため、この記事ではより実践的な内容を掘り下げて書いていきます。

当日の登壇では話さなかったClojure開発経験者向けの内容が多くなりますが、どうかご容赦くださいmm

社内システムについて

開発した社内システムは社内のイベントや部活動などを管理するためのイベント管理サービスです。
以下の機能を実装しています。

  • イベントの開催・編集
  • プロフィール編集
  • Googleカレンダー連携
  • Slackログイン
  • Slackへの開催通知

イベントの開催を社内Slackへ通知することで参加者を募ったり、
Googleカレンダーにワンボタンで予定を追加して管理が出来るシステムになっています。

また、ログインの簡単化のためにSlackアカウントを使ったSSOをサポートしています。

システムの構成

バックエンドをGolang+gin, フロントエンドをClojureScript+re-frameで構築しています。
ブラウザキャッシュの有効化や圧縮などのため、バックエンドの前にnginxを挟んでいます。

また、画像の保存場所としてGoogle Cloud Storage、データの永続化にGoogle Cloud SQLを使用しています。

続いては、ClojureScript+re-frameで構築されたフロントエンドに焦点を絞って行きます。

フロントエンド

今回の社内システムでは大まかに以下のライブラリを使用しています。

ライブラリ 概要 HP Reagent ReactをClojureScriptから扱いやすくするためのラッパ https://github.com/reagent-project/reagent re-frame Reagentを使ったアプリケーションの状態整理をするためのFramework(Reactに対するReduxと同等のもの) https://github.com/Day8/re-frame schema バリデーションライブラリ https://github.com/plumatic/schema

それぞれの社内システムでの使われ方と、選定した理由などを述べて行きます。

ReagentによるUI構築

社内システムではUI全般の処理にReagentを使っています。
ClojureScriptではReagentと同等のライブラリとしてOm(om-next), Rumなどがあり、今まではOmを用いて開発する事が多かったのですが、

  • Omにはre-frameのようなUIの状態整理のベストプラクティスがなく、UIの状態整理がオレオレ化して複雑化しがちだった
  • 最新版のom-nextが↑の点を解決しているが、まだ開発中の段階だった
  • Rumについては当時まだ情報が少なく、使うにはまだ早いと判断した

と言った理由からReagentを使うことに決めました。

Reagentを使った所感としては、

  • UIの記述言語がClojureのHTMLを記述する時に頻繁に使われるhiccupそのままである
  • 状態管理にatomを使用する
  • ComponentのライフサイクルをClojureのメタデータを使って定義出来る

と言った部分からOmよりもClojureらしく扱いが容易でした。

次に、UIの状態整理を司るre-frameがどのように使われているか述べます。

re-frameによるUI状態整理

社内システムのアーキテクチャ、UIの状態整理はre-frameが請け負っています。
例えば、イベントを開催する際のフローは以下の様になります

1. イベント開催の”Event”をdispatchする

re-frameのフローは”Event”からはじまります。
以下のコードでイベントを開催するための”Event”を送信します。

(dispatch-sync [:create-event token user-id event])

2. イベント開催の”Event”を受け取って、発火させる副作用を決める

(s/defn create-event
[{:keys [db]} [token user-id event]
:-
[(s/one s/Str "token")
(s/one s/Num "user-id")
(s/one model/Event "event")]]
;; UIの状態の更新
{:db (assoc-in db [:create-event :save-button] "loading")
;; http-xhrioを使ってhttpリクエストを送信
:http-xhrio {:method :post
:uri (str core/host "/api/event")
:params (Event->EventCreateRequest user-id event)
:timeout core/default-timeout
:format (ajax/json-request-format)
:response-format (ajax/json-response-format {:keywords? true})
:headers {:Authorization (str "AccessToken " token)}
:on-success [:success-create-event token]
:on-failure [:error-create-event]}})

↑の関数でEventを元に発火させる副作用を hash-map にまとめることで決定します。
この場合、keyとして :db と :http-xhrio が返されており、:db によってアプリケーションの状態更新し、 :http-xhrio によってHTTPリクエストを発行します。

3. 副作用の発火

2番にて決められた副作用がre-frameによって発火されます。
今回の場合、UIの状態更新とHTTPリクエストの発行が行われ、
HTTPリクエストの結果を元にまた2番の処理を行います。

4. アプリケーションの状態の展開

副作用の発火によってUIの状態が書き換わると、
状態を見ている query が反応し、 query として登録された関数を元に状態の写像が作られます。

例えば、以下の query はイベント開催画面に表示する最新のイベント情報を表現します。

(s/defn get-create-event :- (s/maybe model/Event)
[db :- db/AppDB
_]
;; アプリケーションの状態からイベント開催画面の
;; イベント情報だけを取り出す
(get-in db [:create-event :event]))

5. Viewテンプレートの作成

4番の query を以下のように subscribe 関数を使って購読することで、状態の変更を検知して値を受け取る事が出来ます

(subscribe [:get-create-event])

subscribe した状態は、以下のようにViewテンプレートに組み込む事が出来ます。

(s/defn create-event-view []
(let [reader (js/FileReader.)
me (subscribe [:get-me])
;; イベント情報
event (subscribe [:get-create-event])
save-button (subscribe [:get-create-event-button])]
[:div#create-event.event-form
[:h1 "Create An Event"]
[:div.img-form.clearfix
;; イベントのサムネイル表示
[:img.circle.thumbnail {:src (:image_url @event)}]
...

6. テンプレートを元にブラウザ上へ描画

アプリケーションの変更を元に新しいViewテンプレートが出来上がると、Reagent/Reactを使って画面が再描画されます。

この1->2->3->4->5->6->1…の繰り返しによって、システムが動作し続けます。

結果的に3番以外の全ての層が関数でのみ構成されるため、定性的な話ではありますがOm単体でアプリケーションを作るよりぐっと見通しが立てやすくなった事を実感出来ました。

最後に、開発環境でしか使っていなかったものの大いに役に立ったSchemaの活用について述べます。

SchemaによるValidation

社内システムでは、フォームの入力バリデーションとは別に関数でやりくりする値のバリデーションのために Schema を導入しています。
これにより、静的型チェックだけでは確認しきれない ある書式に従った文字列かどうか~以上~以下の数字かどうか などの実装者が決めた決まりごと(ライブラリの中ではSchemaと呼ばれている)を満たしているかどうかを実行中にチェック出来ます。

この手のライブラリとしてはClojure公式ライブラリである clojure.spec が有名なのですが、まだ開発中であったためSchemaを導入しました。

このライブラリでは、既に今までのサンプルコードにある通り以下のように :- に続いてSchemaを書くことでバリデーションが可能になります。

;; Schema `Me` を定義
(s/def Me
{:id s/Num
:slack_id s/Str
:name s/Str
:token s/Str
:picture_url (s/maybe s/Str)})
;; 引数が `Me`
(s/defn save-me
"Save Me to localStorage"
[me :- Me]
(.setItem js/localStorage
"me"
(-> me clj->js js/JSON.stringify)))
実際に値がSchemaを満たせているかどうかは (validate Me me) のように明示的にvalidate関数を呼び出す等の処置が必要ですが、ライブラリには関数の引数・返り値を必ずチェックさせるか否かを決める set-fn-validation! 関数が用意されているため、今回はこちらを以下のように開発環境でだけ呼び出して使用しています。
;; 開発向け設定
(when ^boolean js/goog.DEBUG
(s/set-fn-validation! true))
Schemaの導入により単純なunittestを書く手間を減らせたり、デプロイ後に思わぬバグでクラッシュする事が殆どなくなる等の恩恵を得られました。
最後に
以上、やや駆け足ではありますが弊社でのClojureScript+re-frame(とSchema)の弊社社内システムでの活用事例について紹介させていただきました。
総括としては
  • OmとReagentで比較してみるとReagentの方が扱いが安易だった
  • re-frameのパターンのお陰でUIの状態整理が健全になった
  • Schemaの導入で品質向上と時間短縮が出来た
の三点があるかと思います。
ただ、このシステムを開発中にom-nextがalpha版からbeta版へ開発が進んでいたり、clojure.specのalpha版を含んだClojureの新バージョンが今年中にはリリースされそうな状況にあるなど状況は変わって来ているので、この構成がベストだとは到底言えそうにないです(特にSchemaはここ数ヶ月程更新が途絶えています…)
しかしもし、この記事が少しでもClojureScriptを使った開発への助けになれば幸いです:)
それでは、また!
次回は山本さんのgRPCでAPI作ってみた話です、お楽しみに!