シリーズバックナンバー
- 何も考えずに Emacs を使って Clojure の Luminus webframework を使う
- Luminus のサーバ側で手っ取り早くAPIを試したいメモ書き
- Luminus で re-frame 、ping-pong ボタンを追加してみるメモ書き
- Luminus で GCE 上の API を叩いてみる
re-frame 難しい、難しくない?
JavaScript を Lispっぽく書ける上に、サーバサイドをうまいことしてくれる Clojure と相性のよい ClojureScript において、JavaScript の redux 的立ち位置にある re-frame ですが、redux と Clojure に慣れていないと少しとっつきにくい感が否めません。
今回は簡単にLuminusで立っているClojureサーバと ping-pong 通信を行う処理を書いてみます。
これができれば簡単なデモページくらいならなんとか本題(シリーズの1を参照)の目的くらいは達成できそうです。
環境
OS: Manjaro Linux
Build tool for Clojure: leiningen
Editor: Emacs (ciderを repl として利用)
WebFramework: Luminus (lein new luminus demo-app +reitit +aleph +swagger +re-frame +auth +oauth
)
ClojureScript を Cider につなぐ処理
M-x cider-connect-cljs
で localhost、取り敢えず Yes figwheel と選択すれば上手いこと ClojureScript の repl が開かれます。
補完を有効にするには、クラス(ファイル)上部の (ns ...
の部分にカーソルを合わせて C-c C-c
しておけば一通りは大丈夫なはずです。そのファイル中でコードを書いている途中に何らかの補完が入ります。
尚、新しいクラスを (ns ...
に書き込んだ場合にはそのたびに C-c C-c
してください。
ClojureScript での成果はブラウザで勝手に反映される
ClojureScript で適当にコードを書いた場合、それを保存した瞬間ブラウザで localhost:3000 を注視すると更新が起きます。エラー文はここに結構わかりやすく表示されるので積極的にブラウザを眺めることをおすすめします。
コード1 {project-name}/src/cljs/{project-name}/event.cljs に追記
このクラス(ファイル)では名前の通りイベント処理を行います。ページ内のデータを更新したりサーバからデータを読み込んだりする、雑に言えば内側に近い部分を処理するための部分です。そのため、パッと目でわかる変化が少ないため理解が難しいかもしれませんが、なんとか理解しなければなりません()。
(rf/reg-sub
:common/error
(fn [db _]
...))
;; my-utils
(defn- to-keyword-map [a-map]
(into {}
(for [[k v] a-map]
[(keyword k) v])))
;; my-dispatchers
(rf/reg-event-fx
:ping-pong
(fn [db [_ params]]
{:http-xhrio {:method :get
:uri "/api/ping"
:response-format (ajax/json-response-format)
:on-success [:set-pong]
:on-failure [:set-pong-fail]}}))
(rf/reg-event-db
:set-pong
(fn [db [event pong]]
(print event) ;; -> :set-pong
(print (type event)) ;; -> cljs.core/Keyword
(print pong) ;; -> {message pong}
(print (type pong)) ;; -> cljs.core/PersistentArrayMap
(assoc db :pong (to-keyword-map pong))))
(rf/reg-event-db
:set-pong-fail
(fn [db [_ _]]
(assoc db :pong {:message "Fail..."})))
;; my-subscriptions
(rf/reg-sub
:pong
(fn [db _]
(:pong db)))
説明
(defn- to-keyword-map [a-map]
(into {}
(for [[k v] a-map]
[(keyword k) v])))
ClojureScript でマップを効率良く扱うための補助関数です。この関数で行っているのは、マップ構造のキーを ClojureScript のキーワードにする処理です。つまり {message pong}
を {:message pong}
としています。こうすることで、マップ構造にアクセスする際に (:message {:message pong}) => pong
という風にできるようになります。
(rf/reg-event-fx
:ping-pong
(fn [db [_ params]]
{:http-xhrio {:method :get
:uri "/api/ping"
:response-format (ajax/json-response-format)
:on-success [:set-pong]
:on-failure [:set-pong-fail]}}))
ping-pong を行うイベント :ping-pong
をイベントリストに登録します。ここで行っているのは、http(s) 通信を使って、サーバに ping を送りつける処理です。(get method で query、body 無しで /api/ping
へリクエストを投げる)
返って来るフォーマットは json 型なので(シーズンの2を参照)、:response-format
をこれに指定します。
正しく返ってくれば :on-success
に指定したイベントが呼び出され、そうでなければ :on-failure
に指定したイベントが呼び出されます。
(rf/reg-event-db
:set-pong
(fn [db [event pong]]
(print event) ;; -> :set-pong
(print (type event)) ;; -> cljs.core/Keyword
(print pong) ;; -> {message pong}
(print (type pong)) ;; -> cljs.core/PersistentArrayMap
(assoc db :pong (to-keyword-map pong))))
:on-success
で指定したイベント :set-pong
です。これはページが保持するデータベース db
に値を書き込むイベントです。
それぞれの引数について見ていきましょう。
db
はその名の通りデータベースです。event
は呼び出された id 、つまり :set-ping
そのままになります。 pong
は所謂イベントの引数です。例えばこの場合では、/api/ping
から返って来る値(body部)である、{message pong}
となります。
(rf/reg-event-db
:set-pong-fail
(fn [db [_ _]]
(assoc db :pong {:message "Fail..."})))
これは失敗した時のイベントです。失敗したので message に "Fail..." を入れ、db
へ登録します。~~~もちろん何もしなくても良いですが、エラー処理忘れてんじゃーんって言われたくないので入れました。~~~
(rf/reg-sub
:pong
(fn [db _]
(:pong db)))
これは db
から pong を取り出すイベントです。これは pong に変更があればこれを呼び出している部分が自動的に更新されます。このおかげで同期処理が少し楽になります。
コード2 {project-name}/src/cljs/{project-name}/core.cljs に追記
(defn home-page []
[:div.container
;;;;;;
[:div.container>div.row>button {:on-click #(rf/dispatch [:ping-pong])} "Send Ping to Server!"]
(if-let [pong @(rf/subscribe [:pong])]
[:div.row>div.col-sm-6>div.card (:message pong)])
;;;;;;
(when-let [docs @(rf/subscribe [:docs])]
[:div.row>div.col-sm-12
[:div {:dangerouslySetInnerHTML
{:__html (md->html docs)}}]])])
説明
[:div.container>div.row>button]
というのは hiccup 記法と呼ばれる Clojure/ClojureScript での
HTMLの書き方の一つです。
例:
[:div#sample.container>div.row.col-sm-12>button#sampleButton
{:style {:width "10cm"}
:on-click #(set!
(-> (.getElementById js/document "sampleButton")
.-style
.-color)
"red")}
"これはボタンです。"]
<div id="sample" class="container">
<div class="row col-sm-12">
<button id="sampleButton" style="width: 10cm;">これはボタンです。</button>
</div>
</div>
といった形になります。class名に container
など指定しているのは Bootstrap の機能を使うためです。また :on-click
は javascript でいう
document.getElementById('SampleButton').style.color = "red";
です。プロパティには .-
関数には .
を付けてアクセスし、代入には set!
を用いています。
さて、本題の on-click
には #(rf/dispatch [:ping-pong])
が指定されていますが、これは :ping-pong
イベントを実行してくれ、という意味を示しています。これで pong の値が生成され、 db
に登録されることになります。
次の行の、if-let
はもし db
に :pong
の値が登録されていれば、その値を pong として保持します。そしてその値の中の :message
キーでアクセスされる値を取り出して表示します。
さて、一連の流れを確認できたので実際の動作を確認します。
追記
このプロジェクトにおいて、ClojureScript の print関数は F12 からのコンソールでも見ることができます。