• 39
    Like
  • 3
    Comment
More than 1 year has passed since last update.

Web開発のクライアントサイドのフレームワークについては、これまで様々なものが流行っては廃れたりして、なかなか落ち着かない感じではありますが、今回ネタにとりあげる om(https://github.com/swannodette/om)、どこまで盛り上がるのか興味深いところです。

om とは

端的に言えば「Facebook の Client Side Framework であるReactの ClojureScript による wrapper」になります。

React は、MVC で言うところの View レイヤーという位置づけのFrameworkで、「通常の DOM より軽量な Virtual DOM を操作、Virtual DOM の差分を DOM に反映(パッチ当て)することで 高速なViewの更新を実現」しています。

Virtual DOM という考え方がどのくらい昔からあるかはよく知らないのですが、今年に入ってから頻繁に耳にする様になりました(React の影響でしょうか...)。

当然のように ClojureScript による wrapper が登場するわけですが、今のところ(というか自分が知っているだけですが) 2種類の Framework が存在します。

Reagent

Reagent は、Virtual DOM の表現のために hiccup 形式の構文を標準装備しており、Clojure に慣れたプログラマには取っ付き易いように見えます。一時期開発が停滞しているのかな、と思っていましたが、Reagent 本体よりもその周辺(leiningen template とか tutorial) が充実してきており、馴染みやすさからすると Reagent から始めても良かったかもしれません(ちょっと後悔...)。

  • Reagent Cookbook - いわゆるサンプル集、こういうのがあるととっかかりやすい印象を受けます。
  • Reagent Template - reagentを簡単に使えるleiningen template
  • Reagent Seed - こちらもleiningen templateですが、以下のものがごった煮で付いてきます。
    • secretary - client-side routing
    • garden - css を hiccup like な感じで使うライブラリ
    • austin - ClojureScript の browser REPL

om

om は、Virtual DOM生成部分は他のライブラリに任せるスタンスです。 コンポーネント を作成するために組み合わせて使えるような機能をコンパクトにまとめた、といった感じでしょうか。Virtual DOM生成機能は当然標準でも用意されていますが、hiccup のような使いやすい抽象化されたものではないので、やはり他の Framework を組み合わせて使うのが本筋なようです。知っている範囲では、

  • sablono - hiccup 的な構文を提供してくれるライブラリ
  • kioo - enlive 的な感じのテンプレートライブラリ

が組み合わせて使えるとされています。

Reagent と om、どちらが主流だとか優位性があるとかいう話ではないと思いますが、今回は om について try してみました。

om 入門

最速で Hello, world!

さて、ここからが本番です。leiningen は導入済みであるとします(なるべく最新が良いと思います。

手っ取り早くomのアプリケーションを作成するために、とてもシンプルな leiningen テンプレートが提供されています(mies-om)。

早速使ってみます。

    bash$ lein new mies-om mies-om-example
    Retrieving mies-om/lein-template/0.4.1/lein-template-0.4.1.pom from clojars
    Retrieving mies-om/lein-template/0.4.1/lein-template-0.4.1.jar from clojars
    bash$ cd mies-om-example/
    bash$ lein deps
    bash$ lein cljsbuild once
    Compiling ClojureScript.
    Compiling "mies_om_example.js" from ["src"]...
    Successfully compiled "mies_om_example.js" in 9.231 seconds.
    bash$ 

ここで mies-om-example ディレクトリ直下にある index.htmlをブラウザで開くと、以下のように表示されるはずです。

sc2.png

project.clj には、cljs を開発用にコンパイルするための最小限の設定がされております(解説は省略)。まずは index.html から。まさに最小限のコードですね。

<html>
    <body>
        <div id="app"></div>
        <script src="http://fb.me/react-0.11.1.js"></script>
        <script src="out/goog/base.js" type="text/javascript"></script>
        <script src="mies_om_example.js" type="text/javascript"></script>
        <script type="text/javascript">goog.require("mies_om_example.core");</script>
    </body>
</html>

上記<div id="app"></div>のところに、次に示すClojureScriptにより(Virtualではない)DOMが生成され埋め込まれます。

でそのClojureScriptがこちらです(omのBasic Tutorialの最初のコードと同じです)。

;; src/mies_om_example/core.cljs
(ns mies-om-example.core
  (:require [om.core :as om :include-macros true]
            [om.dom :as dom :include-macros true]))

(enable-console-print!)

(def app-state (atom {:text "Hello world!"}))

(om/root
  (fn [app owner]
    (reify om/IRender
      (render [_]
        (dom/h1 nil (:text app)))))
  app-state
  {:target (. js/document (getElementById "app"))})

om では、アプリケーションの状態を atom として管理します(app-state)。atom の中身はClojureScriptで扱えるimmutableなデータであれば何でも良いです。この例では:textをキーにもつmapになっています。

om/root は、ざっくり言うと「アプリケーションの状態をみながら React 経由での描画(というかDOM構築・パッチ当て)を行う」イメージです。om/root は、以下の引数を取ります。

(defn root
  ([f value options] ...))
  • f : IRenderIRenderStatereifyしたインスタンスを返す関数を指定します。IRenderIRenderStateは om のプロトコルで、Virtual DOM を構築して返すようにします。つまりここが Viewの中核となるところです。
  • value : Virtual DOMに対応する ClojureScript の tree 構造をもつデータ、もしくは atom で wrap した tree 構造をもつデータを指定します。通常は動的な View を構成 するためにこのフレームワークを使うので、サンプルでも無い限りatomでwrapしたデータ構造を指定することになります。
  • options : om.core/build 関数に渡すオプションのmapを指定します。:target が必須で om/root で構築されたコンポーネントを設定(mount) する先となる JavaScript の element を指定します。target以外のオプションについては omのドキュメント を参照してください。

これだけでは動きが無いためよくわからないので、少し動きのあるものを作ってみます。

Next Step (contact list)

次に示す例は、omのチュートリアルを少しモディファイしたもの(Contact List)です。まずはイメージから。css frameworkは個人的に時々使ってるInkを使っています(レイアウトがガタガタでカッコ悪いです...すみません)。

sc1.png

管理する状態

  • 今回はatomに包まれたmapを管理します。
(def app-state
  (atom
   {:contacts
    [{:name "Ben"}
     {:name "Alyssa"}
     {:name "Eva"}
     {:name "Louis"}
     {:name "Cy"}
     {:name "Lem"}]}))

contact-view 関数 (Contact List の1行を表す View)

  • IRenderStateプロトコルの関数render-stateを実装します。今回はsablonoを使ってhiccup構文で書いてみました。
  • Deleteボタンを押下したタイミングでのVirtual DOMの更新時には、cljs.core.async/put!を使っています。またそのための通知チャネルは後述するメインのviewのIWillMountプロトコルで実装しています。
(defn contact-view [contact owner]
  (reify
    om/IRenderState
    (render-state [this {:keys [delete-chan]}]
      (html
       [:li
        [:span (:name contact)]
        [:button {:onClick (fn [e] (put! delete-chan @contact))
                  :class "ink-button red"} "Delete"]]))))

contacts-view 関数 (Contacts List 全体の View)

(defn contacts-view [app owner]
  (reify
    om/IInitState              ;; (1)
    (init-state [_]
      {:delete-chan (chan)
       :text ""})
    om/IWillMount              ;; (2)
    (will-mount [_]
      (let [delete-chan (om/get-state owner :delete-chan)]
        (go (loop []
              (let [contact (<! delete-chan)]
                (om/transact! app :contacts
                              (fn [xs] (vec (remove #(= contact %) xs))))
                (recur))))))
    om/IRenderState            ;; (3)
    (render-state [this state]
      (html
       [:div {:class "control-group"}
        [:h2 "Contact list"]
        (apply dom/ul nil (om/build-all contact-view (:contacts app)    ;; (4)
                                        {:init-state state}))
        [:div
         [:input {:type "text" :ref "new-contact" :value (:text state)  ;; (5)
                  :onChange #(handle-change % owner state)              ;; (6)
                  :placeholder "New Contact List Member"}]
         [:button {:onClick #(add-contact app owner)                    ;; (7)
                   :class "ink-button blue"
                   :disabled (empty? (:text state))} "Add contact"]]    ;; (8)
        [:div (:text state)]]))))                                       ;; (9)
  • (1) IInitState : このView内部で使う状態を初期化します。ここでは Contact List に入力する文字列(:text)と、Deleteボタン押下時の通知に使うcljs.core.asyncのチャネル(:delete-chan)を設定しています。
  • (2) IWillMount : このViewが mount される直前に一度だけ呼ばれるプロトコルです。cljs.core.async/go ループはここで実装します。この View では、子供の View である contact-view からの通知が来たら、 状態の更新、つまりDeleteボタンを押下した行のデータを削除(remove)しています。あくまで状態の更新、であって、DOMそのものは修正しない、ということがポイントです(ただしこの実装だと単純に名前が一致していたら消す、という動きなので、本当はもう少し考慮が必要です)。
  • (3) IRenderState : Virtual DOM の構築を行うよう実装します。行の追加や削除等「操作に対する処理」は全く書かず「状態のあるがままを反映する」だけなので、記述は非常に楽になります。
  • (4) contacts-view の各行は、contact-view 関数で定義していたので、ここでは om/build-all 関数を呼び出すことでその内容を展開しています。contacts-view が親、contact-viewが子、になるわけです。
  • (5) :ref "new-contact" というのは、後述する処理でHTMLinput要素を参照する必要があるため、参照するための名前として定義してあります。
  • (6) :onChangeinput要素のonChangeイベント発火時に呼ばれる関数を定義します。ここは普通に ClojureScript の関数をシームレスに書けるので、コードがすっきりして良いです。
  • (7) Add contact ボタン押下時には後述するadd-contact関数を呼びます。
  • (8) テキスト入力欄が空欄の時にはボタンを押下できないようにしてみました(disabled=true)。
  • (9) テキスト入力に連動して画面が動いてくれるか、確認するために:textの内容を表示してみました。

add-contact 関数

(defn add-contact [app owner]
  (let [new-contact (-> (om/get-node owner "new-contact") .-value)]       ;; (1)
    (when-not (empty? new-contact)
      (om/transact! app :contacts #(conj % (assoc {} :name new-contact))) ;; (2)
      (om/set-state! owner :text ""))))                                   ;; (3)
  • (1) 前述(contacts-view 関数の(5)) で定義した :ref をここで使っています。om/get-node を使って実際のDOM要素にあるvaluenew-contactとして取得しています(つまり、管理している状態に反映される前に、画面から直接値を取得する、というわけです)。
  • (2) contact-list にデータを追加します。ここでもあくまで 状態の更新 をしているだけであって、画面の更新は一切ここでは行っていないというところがポイントです。
  • (3) 追加が終わったらテキスト入力欄をクリアするため、:text を空白にしています。

handle-change 関数

(defn handle-change [e owner {:keys [text]}]
  (let [value (.. e -target -value)]
    (if-not (re-find #"[0-9]" value)
      (om/set-state! owner :text value)
      (om/set-state! owner :text text))))
  • 詳しく説明しませんが、数値は入力できないようにしてみました。

以下実際の動き

  • テキスト入力欄にaと入力したところ。テキスト入力欄の下にも入力された文字が反映されています。また、一文字目が入力された時点でAdd contactボタンが enable になっています。

sc3-2.png

  • aを入力後Add contactボタンを押下したところ。上のContact Listに行が追加されました。またテキスト入力欄はクリアされ、Add contactボタンは再び disable になりました。

sc4.png

  • aの上の行にあった Lem のDeleteボタンを押下したところ。Lemの行がなくなりました。

sc5-2.png

全部まとめたソースを github にあげておきました。git clone してlein cljsbuild onceし、index.htmlを開いて実際に動きを確かめてみていただければ、と思います。

まとめ

今回触れなかったことはいっぱいあります。

  • om の用語としてのCursorとかLifecycle Protocolとか..
  • sablono のかわりに kioo 使ってみる、とか..
  • React から SVG いじるとか普通にできるので、グラフィカルな感じのもやってみたい、とか..
  • clientとserverを連携してみる、とか..
  • もっと実用的な画面(画面遷移とか含め)でサンプル作ってみたり、とか..
  • figwheel使うとClojureScriptの変更が即座にブラウザに反映されるので、開発が超ラクチンになるよ、とか..
  • client side routing、とか..

やり残したことはいっぱいあるけど、今後のお楽しみということで、おいおい勉強していきたいと思います。

感想

  • ClojureScript いい感じ。ちょっとずつ慣れてきたのでもっと勉強したくなった。
  • 来年は React がもっと流行ると思う(Virtual DOM更新だけで良い、のはすごく楽)。

see also

以上になります。