メリークリスマス! アドベントカレンダーは全然関係ないですけど、クリスマスなので僕から今年最後のプレゼントです。
ClojureScript を書く上でもはやなくてはならないと言っても過言ではない Figwheel ですが、 Figwheel の作者はそれに飽きたらず新しいライブラリを作っていました。
詳しくは以下の動画で全てを語っているので、そちらを見てもらうと全部分かりますが僕なりにこれを紹介してみましょう。
"Literate interactive coding: Devcards" by Bruce Hauman
Devcards
Devcards とは何か
Figwheel の作者 Bruce Hauman が開発した ClojureScript 用のライブラリです。これは REPL のようなインタラクティブな開発環境を提供してくれます。
(ちなみにこのライブラリ自体は 2014 年頃くらいから開発されていたらしいけど、 ClojureScript Year In Review では今年のこととして扱われている( Strange Loop で大々的に発表したのが今年だったから?))
まずこのライブラリの有り難さを理解するためにはフロントエンド開発の課題を認識する必要があります。
- フロントエンド開発は主に、コードを書いて、ブラウザをリロードして、操作して、検証する繰り返しである
- UI コーディングは延々とコードを微調整する作業である
これらはフロントエンド開発で少しでも複雑なものを作ったことがある人にとっては理解しやすい問題だと思います。これを解決する方法が動画内でも述べられているように以下の通りになります。
- ホットコードリローディングを使う
- リローダブルなコードを書く
リローダブルなコードを書くことは React.js と ClojureScript の組み合わせることで、プログラマが気を使えば簡単に実現出来るようになりました。ホットコードリローディングも Figwheel によって実現され、それは導入コストを大きく超える利益をもたらしてくれるモノでした。
しかし、ホットコードリローディングだけでは解決出来ない問題があります。ひとつのアプリケーション内に幾つもあるコンポーネントというのは、通常幾つかの状態を持ちえます。例えば簡単な例で TODO リストでは「未完了」「完了」というふたつの状態が存在します。実際のアプリケーションではこれらの状態と UI が密接に関わっていることも多く、 TODO リストでは「完了」状態なら取り消し線で消すなどすると思います。これが単純に二値だけであるなら良いですが、他の状態なども加味する必要がある場合、確認する必要がある状態は沢山あることでしょう。
ですが、普通アプリケーションを起動したまま確認出来るその画面(あるいはコンポーネント)の状態は一度にひとつだけです。ここで考えます。もしも、コンポーネントの状態を一度に複数確認出来たらと。それを実現するためにあるのが Devcards です。
だいたい、どういうときに使えるものなのか理解出来た気がしますよね。では早速、手を動かしてみましょう。
Devcards 入門
簡単に始めるために次のようにプロジェクトを作成します。
$ lein new devcards myapp
今回使ったテンプレートのバージョンは devcards/lein-template 0.2.1-3
なので、もしこのデモを完全に再現させたい場合は次のように実行するのが安全です。
$ lein new devcards myapp --template-version 0.2.1-3
次に簡単にこのプロジェクトが動くかテストします。
$ cd myapp
$ lein figwheel dev devcards
dev
と devcards
を Figwheel で監視する対象にして起動しています。 Figwheel が立ち上がったら http://localhost:3449/index.html
へとアクセスし This is working
と画面に表示されているのを確認します。この状態で http://localhost:3449/cards.html
へとアクセスすると Devcards 用の画面が表示されます。
このような画面が表示されたら、 myapp.core
リンクをクリックします。すると次のような画面が表示されます。
この first-card
というのは何処で定義されているかというと src/myapp/core.cljs
の次のコードです。
(defcard first-card
(sab/html [:div
[:h1 "This is your first devcard!"]]))
この部分に少し修正を加えると(例えば :h1
を :h2
に書き換えてみるなど) Figwheel が動いていれば動的に変化するのが分かると思います。もう少しこの defcard
というマクロを試してみましょう。
(defcard second-card
"Hello, Devcards")
(defcard third-card
"*This is third card*"
(* 100 200))
このようなものを書くと次のように表示されます。
third-card
の例から分かる通り、 Devcards は必ずしも ReactDOM を使う必要がないことが分かります(ちょっと説明を飛ばしてましたが first-card
の例では SABLONO というライブラリを使って Hiccup 記法で記述した ReactDOM を返却しているので、 defcard
内で定義した DOM が表示されていました)。また第二引数として渡した文字列は Markdown として解釈されるので、これは例の説明に用いることが出来ます。
さらにもう少し高度な使い方を見てみます。
(defn slider [v]
(let [init-value (:value @v)]
(sab/html [:input {:type "range" :value init-value :min 0 :max 100
:style {:width "100%"}
:on-change (fn [e]
(swap! v assoc :value (.-target.value e)))}])))
(defcard fourth-card
"My slider component"
(fn [data-atom _] (slider data-atom))
{:value 10}
{:inspect-data true
:history true})
このように書くと次のように表示されると思います。
この例では slider
関数というのを作っていて、それは atom
(参照型)を受け取り ReactDOM で作られた Slider のコンポーネントを返すようにしています。注目すべきは defcard
が第三引数として関数を受け取り、第四、第五引数としてマップ型でデータを受け取っていることでしょう。なんとなく察しが着くと思いますが、第三引数として渡した関数の第一引数として、第四引数として渡したマップデータが渡るようになっています。第五引数はこのカードのプロパティになっていて、 :inspect-data
を true
とすることで現在の atom
の状態が見えるようになり、 :history
を true
にすることでこのカード上の操作を巻き戻したりすることが出来ます(凄い!)。
さて、 defcard
マクロは一体どういう引数を受け取るのかというと次のようになります。
(defcard fourth-card ;; オプショナル: シンボルネーム
"My slider component" ;; オプショナル: マークダウンドキュメント
(fn [data-atom _] (slider data-atom)) ;; defcard で注目するオブジェクト
{:value 10} ;; オプショナル: 初期データ
{:inspect-data true ;; オプショナル: devcard の設定
:history true})
もっと詳しく知りたい場合は defcard の API リファレンスを参照してください。
ここまでで Devcards が提供してくれるものが何かというのが分かったと思います。
-
defcard
という API がある- ホットリロードされる
- 内部で書いたものが表示される( ReactDOM なら HTML として、数値などのデータはデータのまま)
- コンポーネントの状態を可視化したり、操作を巻き戻したりすることが出来る
-
defcard
を使ったネームスペースが全て洗い出される- Devcards の最初の画面にネームスペースがリストアップされる
ここまでで簡単に Devcards を紹介しました。
Devcards 実践編
さて、簡単な例だけではいまいちこのライブラリの良さを理解出来ないかもしれないので、もう少し具体的な例を示していきましょう。まずは project.clj
を次のように修正しましょう。
;; 前略
:dependencies [[org.clojure/clojure "1.7.0"]
[org.clojure/clojurescript "1.7.170"]
[devcards "0.2.1"]
[reagent "0.5.1"] ;; Reagent を追加
]
;; 中略
:builds [{:id "devcards"
:source-paths ["src" "env/devcards"] ;; env/devcards を追加
:figwheel { :devcards true }
:compiler {:main "myapp.card-loader" ;; devcards 用のエントリポイントを変更
:asset-path "js/compiled/devcards_out"
:output-to "resources/public/js/compiled/myapp_devcards.js"
:output-dir "resources/public/js/compiled/devcards_out"
:source-map-timestamp true }}
;; 後略
依存関係の Devcards 用の cljsbuild 設定を修正しました。合わせて env/devcards/myapp/card_loader.cljs
を作成します。
(ns myapp.card-loader) ;; まだ何も書かない
そして src/myapp/core.cljs
を次のように修正します。
(ns myapp.core
(:require [reagent.core :as r :refer [atom]]))
(enable-console-print!)
(def todos (atom []))
(defn toggle [id]
(swap! todos update-in [id :completed] not))
(defn todo-component [id {:as todo :keys [title completed]}]
[:li {:class (when completed "completed")
:on-click #(toggle id)}
title])
(defn todo-list-component []
[:ul
(for [[id todo] (map-indexed vector @todos)]
^{:key id}
[todo-component id todo])])
(defn main []
(when-let [node (.getElementById js/document "main-app-area")]
(r/render [todo-list-component] node)))
(main)
;; remember to run lein figwheel and then browse to
;; http://localhost:3449/cards.html
取り消し線のスタイルを定義します( resources/public/css/myapp_style.css
)。
.completed {
text-decoration: line-through;
}
比較的適当に実装した TODO アプリです。クリックしたら取り消し線が付くだけの簡単な TODO アプリです(簡単のために追加や削除は実装していません)。
次に実際に Devcards を実装しましょう。
env/devcards/myapp/core_card.cljs
を作って、次のように書きます。
(ns myapp.core-card
(:require-macros [devcards.core :refer [defcard-rg]])
(:require [myapp.core :as mc]))
(reset! mc/todos [{:title "じゃがいもを買う"}
{:title "たまねぎを買う"}
{:title "牛肉を買う" :completed true}
{:title "カレールーを買う"}])
(defn helper [ratom idx]
[:ul
[mc/todo-component idx (get @ratom idx)]])
(defcard-rg todo-component-card1
[helper mc/todos 0]
mc/todos)
(defcard-rg todo-component-card2
[helper mc/todos 2]
mc/todos)
(defcard-rg todo-component-with-inspecter
[helper mc/todos 1]
mc/todos
{:inspect-data true})
そして、先ほど作っていた env/devcards/myapp/card_loader.cljs
を次のようにします。
(ns myapp.card-loader
(:require [myapp.core-card]))
Figwheel を再起動して http://localhost:3449/cards.html
にアクセスすると myapp.core_card
というリンクが見えているのでこれをクリックすると次のような画面が見えると思います。
さっと実装してしまいましたが、 Reagent で簡単な TODO アプリを実装してその中の小さなコンポーネントを Devcards を使って表示しているだけですね。ですが、これによって最初に説明したようなコンポーネントの複数の状態をひとつの画面で表示することが出来ています。
この例の注意点としては Reagent を使って実装したコンポーネントは通常の defcard
マクロではなく defcard-rg
を使っているところです。詳しくは こちら を参照してください。
また、この例では実装と Devcards を上手く切り離しているので、プロダクション環境で Devcards が表示されるということはありません(勿論開発環境にも表示されません)。
ただ、今回 Reagent を使いましたが、これに re-frame を一緒に使うと少々相性が悪いです。何故かというと、 re-frame では subscribe
や dispatch
という関数を使って特定のネームスペースに宣言されているデータストア( atom
)から変更を察知してデータを受け取ったり、ユーザー操作を元にデータを更新したりするため、 defcard
マクロ経由で atom
を渡せないからです。
とはいえ、 subscribe
したデータを受け取ってコンポーネントを表示するするようなものであれば表示状態を作ることが出来るため全く使えないというわけではありません(この場合、 dispatch
が正常に動きませんがそこに目を瞑っても得られる効果は小さくはないでしょう / つまり、表示は出来るけど Devcards 上の操作が効かない)。
まとめ
Devcards を使うことに UI コーディングを格段に快適にすることが出来ました。また Devcards を使うことによって上に上げた例以外にも幾つかの恩恵を受けることが出来ます。例えば
- 関数の具体的な使い方を書き留めることが出来る(関数のリファレンスとして活用出来る)
- 静的なページとしても吐き出しておけるので、 QA などコードを開いたりすることが出来ない人にもコンポーネントの全状態を検査してもらいやすくなる
- 将来的に実装する機能などを試しに実装しておくことが出来る
- cljs.test と統合することが出来るぽい
などです。個人的には面白いライブラリだと思うので何処かで実際に使っていきたいなと思っています。