読者です 読者をやめる 読者になる 読者になる

Arantium Maestum

プログラミング、囲碁、読書の話題

Clojure Web Development勉強 - Devcards(その1)最低限の設定

CSSをいじりたおす根気がなかった・・・

ということで話題を変えてFigwheelの作者でもあるBruce Haumanが作ったツールであるDevcardsについて書く。

Devcardsとは

DevcardsはClojureScript+Reactで作ったコンポーネントの動作を試すための環境を提供するツールである。

qiita.com

DevcardsをReagent+Figwheelと連動させる

今回はとりあえずDevcardsとFigwheelで、Reagentコンポーネントを表示・動的に変更できる環境を整える最低限の設定を紹介する。Devcardsはサンプルコードなどを見ると、SablonoというOmでよく使われるReact対応のHiccup構文ライブラリを使っていることが多いのだが、ちゃんとReagentにも対応しているので今までどおりReagentを使い続ける。

まずproject.clj:

(defproject min-devcards "0.0.1"
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [org.clojure/clojurescript "1.9.293"]
                 [reagent "0.6.0"]
                 [devcards "0.2.2"]]

  :plugins [[lein-cljsbuild "1.1.4"]
            [lein-figwheel "0.5.7"]]

  :cljsbuild
  {:builds {:devcards {:source-paths ["src"]
                       :figwheel {:devcards true}
                       :compiler {:output-to "resources/public/js/main.js"
                                  :output-dir "resources/public/js/out/"}}}})

:dependenciesにdevcardsを追加、そしてcljsbuildの:figwheelオプションを今までtrueだったのを{:devcards true}に変えてある。

今回は簡素化のために省いたが、cljsbuild内で定義される複数buildsのうち、本番環境用のコンパイル設定であるprod、開発環境用のdevに加えて、devcardsという三つ目のbuilds設定を作成することが推奨されている。

index.htmlは今までどおり:

<!DOCTYPE html>
<html>
    <body>
        <script src="js/out/goog/base.js"></script>
        <script src="js/main.js"></script>
        <script>goog.require('min_devcards.core')</script>
    </body>
</html>

次にcore.cljs:

(ns min-devcards.core
  (:require
    [reagent.core :as r])
  (:require-macros
    [devcards.core :as d :refer [defcard]]))

(defn my-app []
  [:div
   [:h1 "Hello Reagent!"]
   [:h2 "Hello Devcards!"]])

(defn text-renderer [text]
  [:div
   [:h1 "Here is your text:"]
   [:p text]])

(defcard myapp
  (d/reagent my-app))

(defcard text1
  (d/reagent [text-renderer "Hello"]))

(defcard text2
  (d/reagent [text-renderer "Goodbye"]))

とりあえずreagent.coreに加えてdevcards.coreからdefcardマクロを使う。reagentコンポーネントとして定義したものを、devcards.core/reagentというマクロでReactコンポーネント化している。それをdefcardsマクロが受けとって表示する流れとなる。

reagent.coreは実は今回はいらないが、今後r/atomを使う時のためにrequireしておく。

これでlein figwheelを実行するとこんな感じの表示になる:

f:id:zehnpaard:20161123165527p:plain

ちなみに複数のbuilds設定がproject.cljで定義されている場合はlein figwheel <build名>、例えばlein figwheel devcardsと実行することになる。

figwheelが走っているのでコンポーネントのコードや引数をいじって即時反映させるなどして、非常にインタラクティブなUI開発が可能になる。devcards固有の良さは、いろいろな状態の同一コンポーネントを並べて表示させることで、コード変更がどの状況でどのような影響があるかを非常に見やすくしてくれるところ。

次回はdevcardsのreagent atomとの関連を見ていく。

Clojure Web Development勉強 - Garden(その3)lein-gardenとfigwheelでgarden/CSS自動コンパイル&ロード

前々々回のfigwheelによるCSS自動ロードと前回のlein-gardenによるgarden→CSS自動コンパイルを合わせると、gardenデータ構造をcljファイルに記述・変更すると即時にブラウザで表示が更新されるような環境が出来上がる。

project.clj

leiningen及びlein-garden、figwheel、lein-cljsbuildへの指示は以下の通り:

(defproject figwheel-garden "0.0.1"
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [org.clojure/clojurescript "1.9.293"]
                 [reagent "0.6.0"]
                 [garden "1.3.2"]]

  :plugins [[lein-cljsbuild "1.1.4"]
            [lein-figwheel "0.5.7"]
            [lein-garden "0.3.0"]]

  :garden
  {:builds [{:source-paths ["src"]
             :stylesheet figwheel-garden.styles/style
             :compiler {:output-to "resources/public/css/main.css"}}]}

  :figwheel
  {:css-dirs ["resources/public/css"]}

  :cljsbuild
  {:builds {:dev {:source-paths ["src"]
                  :figwheel true
                  :compiler {:output-to "resources/public/js/main.js"
                             :output-dir "resources/public/js/out/"}}}})

今回はgardenはfigwheel-garden.stylesという名前空間に定義していく。こちらは.cljファイルなのでfigwheelが間違ってJavaScriptコンパイルするようなこともない。

figwheelとcljsbuildの設定は前々々回どおり。(cljsbuildのcompilerオプションがデフォルトで:optimizations :noneにしてくれるので今回は横着して明示的に指示していない)

Reagentによるコンテンツの定義

core.cljsでいつもどおり文書のコンテンツを定義していく:

(ns figwheel-garden.core
  (:require
    [reagent.core :as r]))

(defn my-app []
  [:div
   [:h1 "Hello Reagent!"]
   [:p "Hello Garden!"]
   [:p.my-class "Hello My-Class!"]])

(r/render
  [my-app]
  (js/document.getElementById "app"))

とりあえず普通のHTMLタグと一緒に、my-classというclassのp要素も定義してある。

Styleの定義

GardenでHTML要素の見た目を定義:

(ns figwheel-garden.styles
  (:require
    [garden.def :refer [defstyles]]))

(defstyles style
  [:body {:background "#ddd"}]
  [:h1 {:color "#f00"}]
  [:p {:font "18px \"Century Gothic\", Futura, sans-serif"}]
  [:.my-class {:font-size "20px" :background "#ddf"}])

いつもながらセンスのないスタイル。

HTMLとディレクトり構造

index.htmlでCSSとJSを呼び出し。ReactがDOM要素を挿入するためのdiv#appも忘れずに作っておく。

<html>
    <head>
        <link href="css/main.css" rel="stylesheet" type="text/css">
    </head>
    <body>
        <div id="app"></div>
        <script src="js/out/goog/base.js"></script>
        <script src="js/main.js"></script>
        <script>goog.require('figwheel_garden.core')</script>
    </body>
</html>

上記4つのファイルをこんな感じのディレクトリ構造に配置する:

.
├── project.clj
├── resources
│   └── public
│       └── index.html
└── src
    └── figwheel_garden
        ├── core.cljs
        └── styles.clj

あとはlein garden autoとlein figwheelを、順番に別々のシェルウィンドウで実行し、ブラウザでlocalhost:3449を開けば環境設定完了。

これでcore.cljsとstyles.cljのどちらをいじっても、保存した時点で変更が自動的にブラウザに送られる。例えばmy-appのトップレベルの:divを:div.my-classに変えることで、my-app内の背景の色をすべて薄い青にしてみたり。簡単なことでもけっこう遊びがいがある。お試しあれ。

次回はこの設定を使ってgardenの構文などを探ってみる。

今回のコード:

gist.github.com

Clojure Web Development勉強 - Garden(その2)lein-gardenでCSSをオートコンパイル

前回紹介したワークフローだとCSSをednで定義できるのはいいが、二つほど問題があった。

  1. 変更の度にいちいちlein runでCSSコンパイルする必要がある
  2. -main関数をCSSコンパイルのために使っていた

lein-gardenはこの二つの問題を同時に解決する、gardenコンパイル用のlein pluginである。

project.clj

dependenciesにgarden、pluginsにlein-garden、そしてgardenのコンパイル設定を別途指定している:

(defproject min-lein-garden "0.0.1"
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [garden "1.3.2"]]
  :plugins [[lein-garden "0.3.0"]]
  :garden
  {:builds [{:source-paths ["src"]
             :stylesheet min-lein-garden.core/style
             :compiler {:output-to "resources/public/css/main.css"}}]})

:gardenの指示は以前からちょくちょく出ている:cljsbuildのコンパイル設定とけっこう似ている。

の三点を指定。コンパイルしたいcssファイルごとに:buildsの中のvectorに設定mapを入れてやればいい。

core.clj

CSS自体の定義はcore.cljで:

(ns min-lein-garden.core
  (:require [garden.def :refer [defstyles]]))

(defstyles style
  [:body
   {:font-size "14px"
    :background "#f00"}])

前回のようにcssマクロでgardenデータを文字列に変形するのではなく、defstylesというマクロを使ってstyleという名前のgardenデータ構造を定義している。project.cljに渡していた:stylesheetのmin-lein-garden.core/styleという変数名とこのdefstyleで定義している変数名は一致する必要がある。

ディレクトリ構造と実行方法

ディレクトリ構造は非常に簡単で、以下の通り:

.
├── project.clj
└── src
    └── min_lein_garden
        └── core.clj

lein-gardenが自動で出力先のディレクトリなどを作成してくれる。

シェル・プロンプトでlein garden autoと叩くと

Compiling Garden...

Compiling "resources/public/css/main.css"...

Wrote: resources/public/css/main.css

Successful

というような表示が出て、プロセスが走り続ける。別のウィンドウからresources/public/css/main.cssを覗いてみると

body {
  font-size: 14px;
  background: #f00;
}

とちゃんとコンパイルされているのがわかる。

これでcore.cljのdefstylesを以下のようにいじって保存すると:

(defstyles style
  [:body
   {:font-size "16px"
    :background "#ff0"
    :color "#fff"}])

lein garden autoが走っているシェルでは自動的に以下のように表示されて:

Compiling "resources/public/css/main.css"... Wrote: resources/public/css/main.css Successful

main.cssはこのようになっている:

body {
  font-size: 16px;
  background: #ff0;
  color: #fff;
}

これで-main関数を使わず、gardenデータを定義しているファイルを保存するだけで自動的にcssが出力される設定が実現できた。

次回はこれをfigwheelと合わせることで、gardenを定義・変更することでブラウザ上で動的にスタイルが変わっていくのを確認できる環境を設定する。

Clojure Web Development勉強 - Garden(その1)最低限の設定でCSSをコンパイルしてみる

前回に続いてCSSの話。

Reagent/Hiccupを使ってHTML/DOMをedn形式で定義しているのだから、できればCSSも同じくednで定義したい!という末期症状な人間のために、gardenというライブラリが存在している。

github.com

Hiccupと同じように、Clojureのデータ構造であるVectorとMapでCSSを定義できる。最近は生のCSSで書くのではなく、より表現力の高いPreprocessorなどを使って記述するケースが増えているようだが、そのようなPreprocessorよりもさらに強力なClojureのデータ構造処理の機能や関数化・データ再利用がすべて使えるというのは一つの強みである。

個人的にはそういったまっとうな理由よりも「どうせだったら全部ednで書こう」という思いのほうがgarden導入の強い動機になっているが。

今回は最低限gardenを使うための設定について。

Project.clj

(defproject min-garden "0.0.1"
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [garden "1.3.2"]]
  :main min-garden.core)

今回はあくまでgardenデータの定義とコンパイルについてのみなので、ClojureScriptやそれに関係するライブラリはまったく使わない。ClojureそのものとGardenをdependenciesに入れて、コンパイルさせるためにmain関数のありかを指定している。

core.clj

今回は.cljsではなく.cljファイルを定義:

(ns min-garden.core
  (:require [garden.core :refer [css]]))

(def style
  [:body {:font-size "16px"}])

(defn -main []
  (css {:output-to "resources/public/css/main.css"}
    style))

garden.coreからcssマクロをreferしている。styleというのはただのデータ構造で、vectorに:bodyキーワードとmapが入っていて、mapに:bodyに対して適用するスタイルの宣言が入っている。

-mainには、styleデータに基づいたcssをresources/public/css/main.cssというファイルにコンパイルする、という指示が入っている。ちなみにファイル指定なしで(css style)などとするとstyleデータをcssフォーマットに直した文字列が返される。

ディレクトリ構造とコンパイル指示

resources/public/cssというディレクトリが存在しないとエラーになるので、mkdirなどでディレクトリ構造をちゃんと整える必要がある。

cssコンパイルする前のディレクトリ構造は以下のとおり:

.
├── project.clj
├── resources
│   └── public
│       └── css
└── src
    └── min_garden
        └── core.clj

あとはシェルからlein runを実行:

$lein run
Wrote: resources/public/css/main.css

-main関数が走り、resources/public/cssに以下のmain.cssファイルが作成される:

body {
  font-size: 16px;
}

次回はこのコンパイルプロセスを自動化するlein-gardenの話。

Clojure Web Development勉強 - FigwheelでCSS Auto-Loading

Single Page Applicationで複雑かつ洗練されたGUIを実装するためには、一般的にHTMLで定義されている文書のコンテンツだけではなく、CSSで定義されているスタイルをどうコントロールするかも非常に重要なポイントになる。

JavaScriptで直接DOMにコンポーネントごとのスタイルを挿入することも可能なのだが、Reactの実例を見るとどちらかというとCSSは別途用意することの方が多いようである。コンポーネントのclassやid指定でCSSと紐付ける形になる。公式チュートリアルもこの方式を採っていた。

別にCSSファイルを用意するのはいいのだが、CSSの変更は普通HTMLをリロードしないと反映されない。せっかくFigwheelでClojureScriptの変更を自動的かつ即座に反映させているのに、コンポーネントの見た目をいじってもリロードしないと変化が見えないというのは片手落ちである。

幸いFigwheelには特定のディレクトリのCSSをモニタして、変更があればWeb Socketでオートロードしてくれる設定があるので使ってみる。

ディレクトリ構造

とりあえずこんな感じのディレクトリ構造

.
├── project.clj
├── resources
│   └── public
│       ├── css
│       │   └── main.css
│       └── index.html
└── src
    └── min_css
        └── core.cljs

いつもの構成に加えてresources/public/cssにmain.cssを定義している。

HTML

もちろんHTML文書でこのmain.cssを指定している:

<html>
    <head>
        <link href="css/main.css" rel="stylesheet" type="text/css">
    </head>
    <body>
        <div id="app"></div>
        
        <script src="js/out/goog/base.js"></script>
        <script src="js/main.js"></script>
        <script>goog.require('min_css.core')</script>
    </body>
</html>

あとは今までどおりにreagent用のapp div作成とJavaScriptの呼び出し。

Reagentコンポーネント

core.cljsの中にあるReagentのmy-appコンポーネントで要素を三つ定義している:

(defn my-app []
  [:div
   [:h1 "Hello Reagent"]
   [:p "Some random text in a regular p tag"]
   [:p.my-class "More random text in a my-class p tag"]])

h1とただのpとp.my-classである。Reactチュートリアルでも見たように、Reagentでは:tag.xでxクラス属性を、:tag#yでyというidをDOM要素に付与できる。

CSS

body全体とこの三つの要素のスタイルをCSSで定義する:

body {
    background: #ddd;
}

h1 {
    color: #f00;
}

p {
    font: 18px "Century Gothic", Futura, sans-serif;
}

.my-class {
    font-size: 12px;
    background: #ddf;
}

見た目はひどいがまあサンプルだと思って・・・

Figwheel設定

あとはproject.cljでfigwheelにcssの在処を指定してオートロードするよう設定する:

(defproject min-css "0.0.1"
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [org.clojure/clojurescript "1.9.293"]
                 [reagent "0.6.0"]]

  :plugins [[lein-cljsbuild "1.1.4"]
            [lein-figwheel "0.5.7"]]

  :figwheel {:css-dirs ["resources/public/css"]} ;ここ

  :cljsbuild
  {:builds {:dev {:source-paths ["src"]
                  :figwheel true
                  :compiler {:output-to "resources/public/js/main.js"
                             :output-dir "resources/public/js/out/"
                             :optimizations :none}}}})

一行figwheelへの指示を追加するだけ。

実行

あとはlein figwheelで実行してブラウザでlocalhost:3449を開くだけ。

core.cljsもmain.cssも変更して保存した瞬間にブラウザで変更が反映される。

スタイルを動的に変更できるようになるのはけっこう快感。CSSシークレットみたいな本をいろいろとイジリ倒してみたくなる。

次はCSSの指定もednで!という業の深い話を。

Clojure Web Development勉強 - ReactチュートリアルをReagentでやってみる(その5)

ReactチュートリアルのShowing the Movesから最後のImplementing Time Travelまで。

Showing the Moves

https://facebook.github.io/react/tutorial/tutorial.html#showing-the-moves

前回作成したhistoryデータを使って過去の動きを表示するようにする。

JavaScript版:

class Game extends React.Component {
  ...
  render() {
    ...
    const moves = history.map((step, move) => {
      const desc = move ?
        'Move #' + move :
        'Game start';
      return (
        <li>
          <a href="#" onClick={() => this.jumpTo(move)}>{desc}</a>
        </li>
      );
    });

    return (
      <div className="game">
        ...
        <div className="game-info">
          <div>{status}</div>
          <ol>{moves}</ol>
        </div>
      </div>
    );
  }
}

historyのデータをもとにlistアイテムを動的に作って<ol>タグの中に入れている。各moveのlist itemをクリックするとJumpToという現在定義されていない関数が呼び出される。

実際にはこの段階だと「過去の動きの番号を並べる」だけで、実際にどういう手だったのかはわからない。その情報を表示するにはImplementing Time Travelまで待たないといけない。

ClojureScript版:

(defn Game []
  (let [...]
    (fn []
      (let [...
            jumpTo (fn [move] nil)
            move-desc #(if (zero? %) 
                        "Game start"
                        (str "Move #" %))
            move-elem (fn [move]
                        [:li 
                         [:a {:href "#" 
                             :onClick #(jumpTo move)}
                          (move-desc move)]])
            moves (->> @history
                       count
                       range
                       (map move-elem)
                       doall)]
        [:div.game
         ...
         [:div.game-info
          [:div status]
          [:ol moves]]]))))

JavaScriptと違ってClojureScriptでは定義されていない関数があるとコンパイルしないので、何もしないJumpTo関数を定義している。Implementing Time Travelでちゃんと内容も書く。

このコード、このままでも動くは動くのだが、Console Logを調べるとこんなワーニングが出ている:

react.inc.js:20209 Warning: Each child in an array or iterator should have a unique "key" prop.

Check the render method of react_tutorial.core.Game. See https://fb.me/react-warning-keys for more information.

in li (created by react_tutorial.core.Game)

in react_tutorial.core.Game

このワーニングを解決するために、コンポーネントにキーをつける必要がある。

Keys

https://facebook.github.io/react/tutorial/tutorial.html#keys

チュートリアルの説明によると:

key is a special property that's reserved by React (along with ref, a more advanced feature).

Keys tell React about the identity of each component, so that it can maintain the state across rerenders.

とのこと。キーをpropsっぽい形でコンポーネントに渡してやると、Reactが動的に作成されているコンポーネントの同一性を認識できるようになる。

JavaScript:

class Game extends React.Component {
  ...
  render() {
    ...
    const moves = history.map((step, move) => {
      ...
      return (
        <li key={move}>
          <a href="#" onClick={() => this.jumpTo(move)}>{desc}</a>
        </li>
      );
    });
    ...
  }
}

<li key={move}>というようにまるでpropsのように渡している。ちなみにこのkeyはあくまでこのコンポーネント内というコンテキストでの同一性判定に使われるので、まったく違うコンポーネントの中で同じキーが使われていても問題ない。

ClojureScript版:

(defn Game []
  (let [...]
    (fn []
      (let [...
            move-elem (fn [move]
                        [:li {:key move}
                         [:a {:href "#" 
                             :onClick #(jumpTo move)}
                          (move-desc move)]])
            ...]
        ...))))

ClojureScript/ReagentだとこのようにProps的に渡す方法と、^{:key move} [:li ...]というようにメタデータという形式でコンポーネントに紐付ける方法の2種類がある。今回はJavaScript/Reactと同じような見た目になるようPropsぽく渡している。

これでワーニングは消えるはずである。

Implementing Time Travel

https://facebook.github.io/react/tutorial/tutorial.html#implementing-time-travel

最後に、Showing the Movesで表示させた「過去の動き」をクリックするとゲームがその動きまで巻き戻る機能を実装する。

JavaScript:

class Game extends React.Component {
  constructor() {
    super();
    this.state = {
      ...,
      stepNumber: 0
    };
  }
  handleClick(i) {
    const history = this.state.history;
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      ...,
      stepNumber: history.length
    });
  }
  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: (step % 2) ? false : true,
    });
  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    ...
  }
}

Gameコンポーネントをかなり広範囲にわたっていじっている。

Gameに現在何手目かという情報を保持するstepNumberというstateフィールドを定義。それに合わせてhandleClickとrenderを変更し、jumpToでクリックされた「過去の動き」の番号にstepNumberを変更し、xIsNextもそれに合うように変更する。

上記のコードではmovesを消しているからわからないが、チュートリアルJavaScriptコードがすごくいけてない。jumpToの引数名はstepなのに、それを使っているmovesの中ではstepとmoveの二つの変数があってjumpToに渡しているのはmove。わかりにくい。

ClojureScript:

(defn Game []
  (let [...

        stepNumber (r/atom 0)
        
        handle-click
        (fn [i]
          (let [squares (:squares (get @history @stepNumber))]
             (if-not (or (calculateWinner squares) 
                         (squares i))
               (let [next-move (if @xIsNext "X" "O")
                     next-squares (assoc squares i next-move)
                     next-step {:squares next-squares}]
                 (do (reset! stepNumber (count @history))
                     (swap! history #(conj % next-step))
                     (swap! xIsNext not))))))

        jumpTo
        (fn [move]
          (do (reset! stepNumber move)
               (reset! xIsNext (zero? (mod move 2)))))]

    (fn []
      (let [squares (:squares (get @history @stepNumber))
            ...]
        ...))))

やってることはだいたいJavaScript版と同じ。

チュートリアルのこの章の最後のほうで

You may also want to update handleClick to be aware of stepNumber when reading the current board state so that you can go back in time then click in the board to create a new entry.

と書いてある。実際上記のコードを試すと、クリックして途中から新しい手を打つと、明らかにバグっぽい作動(同じプレーヤーが二回連続で打てたり)が出る。

「過去の動き」を表示している時にボードがクリックされると、その時点以降の「未来」データを上書きするようにhandleClickを修正する。

JavaScript:

class Game extends React.Component {
  ...
  handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber+1);
    ...
  }
  ...
}

けっこう簡単で、handleClickの冒頭でhistoryを「現在表示されているボードの状態」のところ以降を切り捨てるだけ。

ClojureScript版:

(defn Game []
  (let [...
        handle-click
        (fn [i]
          (swap! history #(apply vector (take (inc @stepNumber) %)))
          ...)))

JavaScriptと概念的にはまったく同じで、handle-clickの冒頭でhistory r/atomをstepNumberのところまでだけとって残りは捨てている。

これで途中まで戻って打ち出してもバグっぽい挙動は起きない。

まとめ

というわけでReactチュートリアルは一段落。

JavaScriptとClojureScriptで対応させるのは多くの場合は比較的簡単な印象。ちょこっと怪しいところもあるが・・・ (renderSquareが危険な香りがする)

なのでReactで書かれたコードをReagentにポートした上で修正したい、というような要望があってもそこまで難しくはないのではないか。

ただ、このチュートリアルのコードもそうだが、JavaScriptだとStateの扱いが雑になりがちな印象がある。結局Mutableなので、子コンポーネントに渡したStateを簡単に書き換えられる。

ClojureScriptだとデータそのものはImmutableなので、r/atomを渡すのか、r/atomクロージャに持つ関数を渡すのか、などStateをどこに配分してどこからどうやって変更するか、もうちょっと気をつけて考えることが多いと思う。JavaScript通りに書きたいなら全部r/atomにしてしまえばいいのだけど、そうじゃないデザインを自然と志向するようになる。

もしこの○×ゲームを一からReagentで実装するとすれば、多分けっこう違う書き方をすると思う。いつか「Reagent的にはこう書く」というコードも紹介するかも。

Reactチュートリアルの最終形のコード:

gist.github.com

Clojure Web Development勉強 - ReactチュートリアルをReagentでやってみる(その4)

最後までやるつもりだったのだが、長くなったので今回はStoring a Historyだけ。

https://facebook.github.io/react/tutorial/tutorial.html#storing-a-history

これまでReactチュートリアルでは、まずSquareを状態を持ったコンポーネントとして実装し、その後状態をBoardコンポーネントに移し、Squareは独自の状態を持たずpropsを通してのみ値を受け取るように変更した。

今回はそれをさらに一歩推し進め、Boardから状態をGameコンポーネントに引き上げる。その上でGameには現在の状態とともに、過去の状態もすべて保持させる。

コードの構造とデータの有無は変わるのだけど、実際のゲーム自体の挙動は何も変わらないので地味である。

状態の定義

Gameコンポーネントのコンストラクタに状態を定義:

class Game extends React.Component {
  constructor() {
    super();
    this.state = {
      history: [{
        squares: Array(9).fill(null)
      }],
      xIsNext: true
    };
  }
  ...
}

今まではBoardコンポーネントのstateの中にsquaresというフィールドがあり、そのフィールドにnullが9つ入ったArrayが入っていた。

今度はGameコンポーネントのstateの中にhistoryというArrayが入っていて、初期設定ではそのArrayに一つオブジェクトが入っている。このオブジェクトが以前Boardに定義されていたstateと同じで、squaresフィールドにnullが9つ入ったArrayが入っている。

ついでにxIsNextもBoardからGameに移行。

ClojureScriptだといつもどおりGameにr/atomを定義する:

(defn Game []
  (let [history (r/atom [{:squares (apply vector (repeat 9 nil))}])
        xIsNext (r/atom true)
        ...]
    ...))

historyはnilが9つ入ったvectorが値で:squareがキーなmapの入ったvectorを格納したr/atom。うーむ。

Render関数

次に、render関数で勝ち負け・次のプレーヤーの表示と、Boardに渡すデータを追加:

class Game extends React.Component {
  ...
  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);

    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }
    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={(i) => this.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

<Board />にsquaresとonClickというpropsが追加されている。そして、以前はBoardで表示していたstatusがGameの<div>{status}</div>に移っている。

ClojureScript:

(defn Game []
  (let [...]
    (fn []
      (let [squares (:squares (last @history))
            status (if-some [winner (calculateWinner squares)]
                     (str "Winner: " winner)
                     (str "Next player: " (if @xIsNext "X" "O")))]
        [:div.game
         [:div.game-board
          [Board squares handle-click]]
         [:div.game-info
          [:div status]
          [:ol]]]))))

render関数の外のletではr/atomを使った状態の定義、中のletではローカル変数定義。

handle-click関数

今回の肝であるhandle-clickのアップデート:

class Game extends React.Component {
  ...
  handleClick(i) {
    const history = this.state.history;
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      history: history.concat([{
        squares: squares
      }]),
      xIsNext: !this.state.xIsNext,
    });
  }
  ...
}

やはりBoardにあったものをあらかた移してきた流れだが、一つ大きな違いがある。

まずthis.state.squaresではなく、this.state.historyから最後(つまり最新)の要素をとり、そのオブジェクトのsquaresフィールドをsquaresとしてコピー。

クリックされた四角の場所に応じてsquaresを更新(以前のsquaresをmutateするのではなく、コピーして変更した新しいsquaresに入れ替える)。そのsquaresを入れたオブジェクトをhistoryに追加している。

ClojureScript:

(defn Game []
  (let [...
        handle-click
        (fn [i]
          (let [squares (:squares (last @history))]
             (if-not (or (calculateWinner squares) 
                         (squares i))
               (let [next-move (if @xIsNext "X" "O")
                     next-squares (assoc squares i next-move)
                     next-step {:squares next-squares}]
                 (do (swap! history #(conj % next-step))
                     (swap! xIsNext not))))))]
    ...))

せっかくなので一歩ずつ解説すると:

  • 現在の状態を(:squares (last @history))でとってきて
  • もし以下二つの条件がどちらもマッチしなければ
    • 勝者がいる
    • クリックされた四角にすでに値が入っている(すでに過去クリックされていた)
  • xIsNextの値によって次の手・プレーヤーを決め
  • squaresのiの位置にその手を入れた新しいsquaresを作成し
  • そのsquaresを入れたmapを作成し
  • そのmapを付け加えるようhistory r/atomの値を更新し
  • xIsNextの値を反転させる

まあそのまんまだ。

Boardコンポーネント

あとはBoardコンポーネントから状態や表示を消すだけ。

class Board extends React.Component {
  renderSquare(i) {
    return <Square value={this.props.squares[i]} onClick={() => this.props.onClick(i)} />;
  }
  render() {
    return (
      <div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
      ...
      </div>
    );
  }
}

renderSquareは残っているがそれ以外の関数や状態定義は消えた。

Boardにstateがなく、GameからPropsとしてsquaresとonClickが渡されるのでthis.state.squaresだったのがthis.props.squaresなどになっている。

<div {status} />もGameコンポーネントに移行したのでBoardからは消えた。

renderSquareがなければBoardもFunctional Componentにできたのだが。

ClojureScript版:

(defn Board [squares click-fn]
  (let [renderSquare (fn [i] 
                       [Square (get squares i) #(click-fn i)])]
    (fn []
      [:div
       [:div.board-row
        [renderSquare 0]
        [renderSquare 1]
        [renderSquare 2]]
       ...])))

Reagentだとpropsが引数として入ってきて、その引数の名前でlet内のrenderSquareに渡すだけでClosure化するので楽。

追記

おおっと、「楽」と思って油断していたら間違えた。letの中にfn []があると、Boardを作成した時のsquaresとclick-fnで固定されてしまう。解決策としてはsquaresの代わりに何らかのreagent atomを渡すか、renderSquareも毎回生成するようにするか。これだけ簡単かつライトなSPAならrenderのたびにrenderSquareも作成していいと思うが、より複雑・大規模なアプリケーションの場合コスト的にどうなんだろうか。

(defn Board [squares click-fn]
  (let [renderSquare (fn [i] 
                       [Square (get squares i) #(click-fn i)])]
    [:div
     [:div.board-row
      [renderSquare 0]
      [renderSquare 1]
      [renderSquare 2]]
     ...]))

JavaScriptの方はrenderSquareにmutable stateを渡して一回だけ作成する方向なので、atomを使ったほうが合致しているかも。

まとめ

というわけで、冒頭でも言ったがStateの在処を移したのとhistoryという内部データ構造を作っただけで、現在ゲームの挙動は変わらない。次はこのhistoryを表示させるコードを書いていく。

そして今度こそ次回でチュートリアル終了、なはず。

今回のコード(BoardとGameのみ):

gist.github.com