Arantium Maestum

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

Clojure Web Development勉強 - Reagent(その2)Reagent Atom

さて、Reagentの二大要素の二つ目であるReagent Atomについて。

Reagentでは、コンポーネントの可変な状態を保持するために特殊なAtomを用意している。

と言っても概念・機能は通常のClojureAtomとほとんど同じで、Immutableなデータ構造へのポインタのようなものである。

Clojure Atomと同じInterfaceを持ち、swap!やreset!などで新しいデータを指すようにでき、その指し示すデータを@でderefできる。

例えば:

(def a (r/atom 0))
(js/console.log @a)

(reset! a 5)
(js/console.log @a)

(swap! a inc)
(js/console.log @a)

これでブラウザのログに0と5と6が出力される。

ではReagent AtomClojure Atomとの違いは何か。公式のReagentチュートリアルによると:

The easiest way to manage state in Reagent is to use Reagent’s own version of atom. It works exactly like the one in clojure.core, except that it keeps track of every time it is deref’ed. Any component that uses an atom is automagically re-rendered when its value changes.

つまり、あるReagent Atomを参照しているコンポーネントはそのAtomの値が変わるたびに自動的に更新され、常に最新の値を反映しているようになる。

例:

(def clicks (r/atom 0))

(defn clicker []
  [:div
    [:button {:on-click #(swap! clicks inc)} "Click me"]
    [:p "Click count: " @clicks]])

最初のbutton要素はclickイベントに反応して#(swap! clicks inc)が走るようになっており、clicksのその時その時の値が、p要素に表示されるようになっている。

Reagent Atomの中身はもちろんVectorやMapであっても良い:

(def clicks 
  (r/atom {:John 0 :James 0}))

(defn clicker [name-key]
  [:div
    [:button {:on-click #(swap! clicks update name-key inc)} name-key]
    [:p "Click count: " (get @clicks name-key)]])

(defn my-app []
  [:div
    [clicker :John]
    [clicker :James]])

clickerコンポーネント関数がkeyを受け取り、そのkeyに応じてclick r/atomのmapに入っている正しい値を表示したり更新したりしている。:button内での名前の表示は、Reagentの0.6.0-rc以降、keywordを直接コンポーネントの引数としていれると文字列として表示してくれるようになったのを利用している。

次回はこのr/atomを使ったコンポーネントの状態管理のパターンをいくつか紹介したい。

Clojure Web Development勉強 - Reagent(その1)概要とHiccup構文

ReactのClojureScript WrapperであるReagentライブラリについて。

前回書いたとおり、ClojureScriptには有名なReact Wrapperが三つある。Om/Om-Next、Quiescent、そしてこのReagentである。

最初の二つの特徴を非常にざっくりと要約するなら:

  • Om/Om-Nextは精緻、強力、先進かつ複雑
  • QuiescentはClojureScriptでReactを使うため必要最低限の機能を備えた簡単かつ自由度の高いデザイン

となる。

それに対して、Reagentは

Reactの思想を汲みつつ「VectorとMapをベースにしたHiccup的DSL」と「Clojure Atomの機能を拡張したReagent Atom」の二要素を中心として極力シンプルかつClojure的に再構築したライブラリ

である。この「シンプルかつClojure的」というのがとても大事で、個人的には、習得しやすく、書き始めると自然に感じられ、書いていて気持ちが良い、と非常に好印象である。今のところClojureScriptを書く場合ほとんどReagentを使っており、少し知見もたまってきたので、他のライブラリよりも詳細に踏み込んで書いていく。

今回は第一要素である「VectorとMapをベースにしたHiccup的DSL」の話をしたい。

HTMLコードの解説

と、その前にReact/Reagentのお約束となるHTMLドキュメントのレイアウトについて一言。

大体こんな感じになるかと思う。

<html>
    <body>
        <div id="appdiv"></div>

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

<script>の部分は毎度おなじみだが、その前に<div id="appdiv"></div>という新要素が追加されている。このappdivという名前が付いているDOM要素をReagentに渡してやると、Reagentがその中にどんどんDOM要素を詰め込んでページを構築してくれる。

appdivという名前は特に重要ではなく、あくまでHTMLとcljsファイルの双方で同じ名前が使われていれば良い。

<div id="appdiv"></div><script>の後に来ると、最初にロードされたreagentコードがDOM要素を見つけられずエラーを起こすので注意。必ず<div id="appdiv"></div>が先にくる必要がある。

ReagentのComponent関数

(defn my-app []
  [:div
    [:h1 "Hello Reagent"]
    [:p "ClojureScript + React + Atoms + Hiccup"]])

この時点で注意したいのは、このコンポーネント関数、あくまでVectorを返す純粋関数であって、副作用を起こすようなものはまったくない。何の変哲もないVectorなので様々なClojureのデータ構造操作関数で作ったりいじったりできる。

さらに、非常に簡単にコンポーネント化して組み合わせることができる。

(defn my-component []
  [:div
    [:h1 "Hello Reagent"]
    [:p "ClojureScript + React + Atoms + Hiccup"]])

(defn my-app []
  [:div
    [my-component]
    [my-component]])

先ほどまでmy-appという名前だった関数をmy-componentと名称変更し、my-appはそのmy-componentが二つ入っている:divにした。my-componentをmy-appから関数呼び出しするのではなく、関数自体をVectorに入れているのが注意ポイント。この例だとほとんど違いはないがより動的なコンポーネントの場合、このReagentの機能を使うための記述をする・しないで挙動や効率が大きく変わってくる。

コンポーネント関数はその名の通り関数なので、引数を取れる:

(defn my-component [name-str]
  [:div
    [:h1 "Hello " name-str]
    [:p "ClojureScript + React + Atoms + Hiccup"]])

(defn my-app []
  [:div
    [my-component "Reagent"]
    [my-component "Figwheel"]])

my-componentでname-strという引数をとり、h1内で表示する。my-appで[my-component "Reagent"]のように、関数呼び出しではなくあくまでVectorの第一要素がコンポーネント関数、第二要素(以降)が引数となる。

DOM要素のプロパティはMapで定義できる:

(defn my-component [name-str]
  [:div
    [:h1 {:on-click #(js/alert name-str)}
     "Hello " name-str]
    [:p "ClojureScript + React + Atoms + Hiccup"]])

(my-app関数は変化なし)

h1要素のon-clickプロパティに、JavaScriptのalertでname-strを出力する匿名関数を束縛した。これでh1要素をクリックするとアラートメッセージで"Reagent"や"Figwheel"と表示される。

と、ここまでの情報でHTMLのあらかたの要素とプロパティが使えてしまう。さらにfigwheelを使っていれば、いじって保存して、でグリグリとブラウザ上でページが変わっていくのでとても楽しい。ぜひお試しあれ。

ただし、コンポーネント関数自体は描画のロジックはまったく含まれていない。というか現時点では本当にただのデータ構造を返す関数である。

ReagentのRender関数

そのコンポーネント関数を受けとり、実際にブラウザ上で表示させるのを担当するのがreagentのrender関数である。

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

上記のコードでは、reagent.coreという名前空間をrに束縛してある。r/renderがreagentライブラリのrender関数である。

アプリケーショントップレベルのコンポーネント関数を含んだVectorと、document.getElementByIdというJavaScript関数を使ってアクセスした"appdiv"というidのDOM要素を引数にしている。この部分だけは純粋関数ではなくて「描画」という副作用を起こすものとなっている。

データ構造を返す純粋関数として作られているコンポーネントを組み合わせて、最終的に作り上げたトップレベコンポーネントを描画担当のrender関数に渡すのがここまでの流れ。次回はアプリケーションの状態を保持し、ステート変更をうまくComponent関数に伝達するReagent Atomの話。

今回のコード:

gist.github.com

Clojure Web Development勉強 - Reactあれこれ

Facebookが提供するJavaScriptライブラリであるReactについていろいろと書こうと思う。

まず、私自身がReactについて調べていた時に大変参考になった記事を三つ挙げる:

qiita.com

mizchi.hatenablog.com

qiita.com

本記事と前後してこれらも読めば、Reactについてはよく理解いただけるのではないか。

React前夜

今までのWebフロントエンドのモデルは

  1. HTMLで文書の構造を記述し
  2. CSSで見た目を整え
  3. JavaScriptでブラウザサイドの動的なロジックを加える

というもので、JQueryの出現などによって多少の動的挙動ならそれなりに記述しやすくなった。しかしブラウザ上でサービスを提供するビジネスの隆盛に伴い、デスクトップアプリケーションと同等の複雑な挙動をページリロードなしで実装するSingle Page Application(略してSPAーー昔はRich Internet Applicationとか言いましたね)が必要となってくる。

JQuery(あるいは生のJavaScriptで書くというさらなる地獄)ではこういった動的かつ柔軟なインターフェイスを実装するための基本的なモデルは「HTMLで記述された文書構造から立ち上がってくるDocument Object Modelの関連部分をJavaScriptによって逐次変更していく」というものだ。

その結果「グローバルなステートにローカルな変更を逐次与えていく」という命令型オブジェクト指向プログラミングで一番辛い流れになってしまう。

Reactモデル

Reactはその辛さを解消するための仕組みである。

HTML/CSS/JavaScriptの役割分担としては:

  1. HTMLには最低限のCSS+JavaScriptインポート、そしてReactが操作するためのフックとなる単一の空DOM要素を記述
  2. CSSは今までどおり見た目部分を記述
  3. JavaScriptで、引数によって必ず同一のDOM要素(あるいは要素のツリー)を返すコンポーネント関数を数多く記述
  4. 作ったコンポーネント関数を組み合わせてさらに大きなコンポーネント関数を作っていき、最終的にアプリケーション全体を表示するコンポーネントを作成
  5. ページの変遷はコンポーネントに与えられる引数が変わることで起きる

と、JavaScriptがHTMLを食ったような形である。実際JSXというJavaScriptの拡張言語で、より自然にHTMLタグをJSコードに埋め込んだ形式で記述することも可能になる。

HTMLで記述して描画したDOMをJavaScriptでいじるのではなく、すべてを「引数は『状態』、返り値は『DOM要素』」なJavaScriptの純粋関数で書いてしまって、状態変遷の度に単に新しい引数を渡して新しいDOMを作成してしまおう、というモデルである。

メリットとしては

  1. 引数によって描画されるものの同一性が担保されるので、アプリケーションのロジックについて考えやすくなる
  2. 同じ理由でテストが容易
  3. コンポーネント化によって再利用性が非常に高くなる

React自体からはちょっと離れるが、「Reactを使ったアプリケーションの作り方」であるFluxにおいてはさらに「Global Stateを一つのデータオブジェクトにすることで、ほとんどのロジックをそのデータに対する純粋関数として記述できる」という大きなメリットが生まれる。

しかし、このままでは状態変遷ごとに新しいDOMが作成されて描画されて、非常に効率が悪くなってしまう。DOMのローカルな部分をJavaScriptでいじるのは少なくとも「最低限のDOM変更で済む」というメリットがあったわけだ。

そこでReactの影の主役であるVirtual DOMという仕組みが生きてくる。このライブラリ側の仕組みが、「まるで新しいDOMを作成するかのように書かれたコード」をベースに「現在のDOMとあるべきDOMの差分」を算出し、その差分だけの「最低限のローカルなDOM変更のみ」を適用してくれる。宣言的に書いたコードが命令的にうまい具合に変換されるのである。

関数型言語コンパイラインタプリタが「泥臭い命令型の指示」を吸収して、プログラマにはそれらを意識することなく「純粋関数で書かれたコード」を書かせてくれるのとちょっと似てるかもしれない。

ReactとClojureScript

さて、ここまで書いたことを踏まえて考えていただくとわかると思うのだけれど、ReactとClojureScriptの相性は非常にいい。

いやむしろ

JQueryや生のJavaScriptのスタイルをそのまま使ってしまうと『DOMいじり』という非常に命令的でステートフルなモデルからしてClojureScriptの強さがまったく生きない

といった方が正解かもしれない。React的な方向にしろ別の方向(例えばHoplon)にしろ、どうにかしてその一般的なJavaScriptモデルから脱却せざるをえない。

逆にReactのような

DOMの初期構造も純粋関数の組み合わせで記述

状態変遷も本質的にはデータ構造として捉えられてるアプリケーション状態に純粋関数を適用して変換していくことで実施

というモデルではClojureScriptの効率のいいPersistent Immutable Data Structureと豊富かつ強力なデータ操作関数群が猛威を振るう。

現在比較的有名なClojureScript上のReact系ライブラリとしては

がある。

実際に私が使ったことがあるのはReagentのみ。他の二つに関して、今持っている知識と印象を書くと:

  • Om/Om-NextはClojureScriptのメインディベロッパーであるDavid Nolenのライブラリで、非常に精緻かつ先進的な機能を多く取り入れた強力なライブラリのようである。(Om-Nextは後方互換性のまったくない後継プロジェクト)ただしその分かなり独特で、Omの思想と技術的背景をある程度理解しないと使うのは難しい印象を受ける。
  • Quiescentは逆にReactをClojureScriptから使うための最小限のラッパーに近い。ただし、コンポーネントレベルで独自のステートを持つような機能がないので、Flux的な「トップレベルで一つの状態オブジェクトを持ち、そのデータが子コンポーネントに枝分かれして渡されていく」というモデルがデフォルトのようだ。

いつかこれらライブラリも是非使ってみたいものである。

Reagentの詳細は次回以降。

Clojure Web Development勉強 - ClojureScript(その4)figwheelの最小限設定

Figwheelは素晴らしい。FigwheelはClojureScriptを使うことで得る大きな喜びの一つだ。

と過剰に思われる売り込みで始める。しかし、個人的には全く誇張している意識はない。

Figwheelはwebsocketを使って、ソースファイルの変更を自動的にブラウザに反映させる開発ツールである。

前回lein cljsbuild autoでClojureScriptからJavaScriptへのコンパイルを自動化し、ソースファイルを変更して保存すれば自動的にJavaScriptが生成され、そのあとブラウザをリロードすることで変更を反映させることができる、というワークフローを紹介した。

Figwheelはそれをもう一歩進めた形になる。ClojureScriptのソースファイルを変更・保存した時点でブラウザリロードする必要なく自動的にブラウザに新しいJavaScriptがロードされる。

文章で説明しても凄さがいまいち伝わらない感があるが、動画で見た時は相当な衝撃を受けたのを覚えている。見たのはfigwheelの開発者Bruce Haumanによる紹介動画:

www.youtube.com

そして実際に使うとさらに感動する。「cljsbuildから一歩進んだ」というのは正しいが、その一歩がいかに重要かを充分伝えきれていないと思う。

まず、エディタから離れることなくブラウザで変更が確認できる。私の場合はvimを使っているのだが、:wと叩いた時点でブラウザに反映される。書いては:w、書いては:wでホームポジションから手を離すことなしに開発サイクルが回る。

そしてページリロードをしないことで状態が維持される。リロードした場合ページが初期状態に戻されてしまい、例えばそこから状態を色々変更していた場合、前回と同じ変更を加えていって手動でリロード前の状態に戻す必要が生じる。Figwheelを使うとその必要がなく、コード変更前のページの状態のまま挙動などが変わるのを確認できる。

今回のコード:

gist.github.com

ポイントとなるのは3点。

  • pluginsにlein-figwheelを追加する
  • cljsbuildの:optimizationsがないビルドに:figwheel trueを追加する
  • output-to/output-dirをresources/publicディレクトリのどこかにする

最初の二つはleiningenとcljsbuildにちゃんとfigwheelと連動するよう伝えるオプション。

三つ目のポイントは、figwheelが自動的に立ち上げてくれるローカルサーバがデフォルトでresources/pubicをルートディレクトリにしているので、サーバからアクセスするファイルはhtmlであれjavascriptであれすべてresources/publicの下位ディレクトリ(あるいはresources/public内)になくてはいけない。

これでlein figwheelとコマンドを走らせると

Figwheel: Cutting some fruit, just a sec ...
Figwheel: Validating the configuration found in project.clj
Figwheel: Configuration Valid :)
Figwheel: Starting server at http://0.0.0.0:3449
Figwheel: Watching build - dev
Figwheel: Cleaning build - dev
Compiling "resources/public/js/main.js" from ["src"]...
Successfully compiled "resources/public/js/main.js" in 11.823 seconds.
Launching ClojureScript REPL for build: dev
Figwheel Controls:
          (stop-autobuild)                ;; stops Figwheel autobuilder
          (start-autobuild [id ...])      ;; starts autobuilder focused on optional ids
          (switch-to-build id ...)        ;; switches autobuilder to different build
          (reset-autobuild)               ;; stops, cleans, and starts autobuilder
          (reload-config)                 ;; reloads build config and resets autobuild
          (build-once [id ...])           ;; builds source one time
          (clean-builds [id ..])          ;; deletes compiled cljs target files
          (print-config [id ...])         ;; prints out build configurations
          (fig-status)                    ;; displays current state of system
  Switch REPL build focus:
          :cljs/quit                      ;; allows you to switch REPL to another build
    Docs: (doc function-name-here)
    Exit: Control+C or :cljs/quit
 Results: Stored in vars *1, *2, *3, *e holds last exception object
Prompt will show when Figwheel connects to your application

こんな感じのメッセージが出力される。

あとはブラウザからlocalhost:3449にアクセスするとindex.htmlが開き、コンパイルされたJavaScriptが走る。

ChromeJavaScriptコンソールなどを立ち上げると

Figwheel: trying to open cljs reload socket  utils.cljs?rel=1478607372893:49
Figwheel: socket connection established  utils.cljs?rel=1478607372893:49

とソケットで通信していることがわかる。

これでcore.cljsをいじって保存してみる。例えば

(ns min-figwheel.core)
(js/alert "Hello ClojureScript!")

(ns min-figwheel.core)
(js/alert "Hello Figwheel!")

に変更してセーブする。それだけで自動的に新しいアラートメッセージがブラウザ上で表示される。

あるいは

(ns min-figwheel.core)
(js/console.log "Hello Figwheel!")

と変更してコンソールを確認すると

Figwheel: notified of file changes  utils.cljs?rel=1478607372893:49
Hello ClojureScript!  core.cljs?rel=1478641111736:2
Figwheel: loaded these files  utils.cljs?rel=1478607372893:49
("../min_figwheel/core.js")  utils.cljs?rel=1478607372893:51 

こんな風に表示される。ちゃんとソースの変更が反映されてコンソール出力できているのがわかる。core.cljsのどこに対応しているのかまで表示していて、リンクをクリックするとソースが確認できるのもなかなか面白い。

さて、今まで恐ろしく簡単なClojureScript機能しか使ってきていないので、Figwheelのありがたみがまだ明確に出てきていない。Figwheelの真価が発揮されるのはClojureScriptで複雑な処理を書き出してからである。特に、FigwheelとDOM操作系のライブラリを合わせることでページをグリグリと非常にインタラクティブに作成できるようになる。

次はClojureScript上でどうやってDOM操作をしていくかという観点からReactについて書きたい。

Clojure Web Development勉強 - ClojureScript(その3)cljsbuildオプションあれこれ

cljsbuildには様々なオプションがある。なんでこんなにあるの?と言いたくなるくらいある

個人的に使いそうだなぁと思うものを抜粋すると:

  :cljsbuild {
    :builds {
      :main {
        :source-paths ["src-cljs"]
        :compiler {
          :optimizations :whitespace
          :output-to "resources/public/js/main.js"
          :output-dir "target/my-compiler-output-"
          :source-map "resources/public/js/main.js.map"
          :externs ["jquery-externs.js"]
          :libs ["closure/library/third_party/closure"]
          :foreign-libs [{:file "http://example.com/remote.js"
                           :provides  ["my.example"]}]}}

こんなところだろうか。

最後の4つについて簡単に紹介すると:source-mapでブラウザ上から実行されているJavaScriptのソースを確認でき、デバッグなどに非常に便利(らしい)、:externs、:libs、:foreign-libsの三つを記述することでコンパイルされたJavaScriptがさらにminifyされても外部ライブラリが使用できるように設定できる。後々より詳細に解説したい。

今回特に集中的に説明したいのが、Google Closure Compilerを使ったJavaScriptのMinificationのオプションである:optimizationsの詳細と、:buildsの値を前回のようなvectorではなくmapにすることで複数のビルド設定を指定し、開発環境と本番環境とのビルドを区別する方法である。

Google Closure Compilerと:optimizationsオプション

ここ十年間で起きたJavaScriptの隆盛の中心にGoogleがいるのは間違いない。Google MailとGoogle Mapsという二大ajaxアプリを引っ提げて00年代中盤のソフトウェア界に衝撃を与えたのももう十年も昔の話だ。そしてJavaScriptエンジンのV8やそれを搭載して開発環境(というのは言い過ぎにしても)としても優れているChromeを発表し、「重いしセキュリティ的にも心配だからJavaScript切っとこう」という態度を過去のものにした。

そのGoogleが自社のJavaScript開発用のツールとしてOSS化したのがGoogle Closure Toolsである。Compiler、Library、Templates、Stylesheets、Linterの5つのパーツからなるこのツールセットは、標準ライブラリを持たないJavaScriptにとって高品質な共通ライブラリを提供し得るものとなっている。(正直そのポジションの最も美味しい部分はJQueryに食われたけど)

ClojureScriptはGoogle Closure Toolsのうち、Google Closure LibraryとGoogle Closure Compilerを根本的なツールとして組み込んでいる。その二つを使うことによって複雑な処理を行うコードでもかなりバイト数を絞り込み、トラフィックもロード時間も抑え込むことが可能となっている。ただし、その結果コンパイルされたJavaScriptコードが難読化されバグがわかりにくくなったりすることと、そもそもコンパイルに数秒かかるので開発中にフローが中断されてしまう、などという難点がある。

そこで開発中はMinificationは切っておいて分かりやすいJavaScriptアウトプットを素早く作成し、本番にデプロイする時点でGoogle Closure Compilerの全力を出してもらう、という流れになる。そのコントロールのための設定が:optimizationsであり、:none、:whitespace、:simpleそして:advancedの四つの値のいずれかを取る。

この四つのオプションについての詳細は以下の記事がよくまとめている:

ClojureScript optimizations on Node - huh?

:none

Google Closure Compilerを全く使わない。ClojureScriptコンパイラが吐くJavaScriptをそのまま使っている。その結果、ファイルサイズが大きくなっていることと、Google Closure Libraryへのdependencyや自分の書いたコードの呼び出しが一つのファイルで完結しないため、HTMLドキュメントに明示的に記述しなければいけない。具体的に言うと前回の例でも出てきた

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

の三行目と五行目は:optimizations :noneの時にだけ必要なものである。

:whitespace

このオプションからGoogle Closure Compilerが使われだす。この時点では、構文上必要ない空白や括弧などを自動的に排除して、dependencyなどを全部一つのファイルにまとめてくれる。なのでHTMLは以下のように記述できる:

<html>
  <body>
    <script src="out/main.js"></script>
  </body>
</html>

ちなみにこの記事の頭でリンクしたこのサンプルコードのコメントに

; Defaults to :whitespace.

とあるが、現状では明らかに間違いで:optimizationsの指定がない場合は:noneにデフォルトしている。

:simple

:whitespaceの時の最適化に加え、関数内のローカル変数のシンボルを短くする。

:advanced

:simpleの最適化に加え、かなり突っ込んだコードの変形を行う。あらゆるシンボルを短くし、コードのインライン化なども行い、さらにはコードの中から使われていない部分を探し出してバッサリ削除するというDead Code Eliminationも行う。最後のものは、サード・パーティ・ライブラリなどを使っていると特に素晴らしい効果を発揮する。しかし、そういった激しい最適化が行われている分、外部ライブラリなどもGoogle Closure Compilerに対応しているか、こちらで最低限対応するようExternなどの設定をしてやらないと、シンボルが正しく参照されなかったりなどと問題を引き起こす。

開発環境では:none、本番環境用には:advanced(外部ライブラリなどが許せば・・・)というのが望ましい。

複数のビルド設定の管理

というわけで少なくとも二つ(あるいは例えばテスト環境も入れたければ三つ)のビルド設定が望ましいわけだが、いちいちproject.cljを書き換えるよりもどちらの設定もproject.cljに記述してcljsbuildにどの設定を使うか指定して実行させたい。

前回は以下のように:

  :cljsbuild {:builds [{:source-paths ["src"] ...

と:buildsの値をvectorにしてあったのだが、それをmapに変え、キーを設定名、値を設定にしてやる。

 :cljsbuild
 {:builds
  {:dev {:source-paths ["src"]
         :compiler {:output-to "devout/main.js"
                    :output-dir "devout"}}
   :prod {:source-paths ["src"]
          :compiler {:output-to "out/main.js"
                     :output-dir "out"
                     :optimizations :advanced}}}}

コンパイルするには、

lein cljsbuild once dev

あるいは

lein cljsbuild once prod

とコマンドの末尾に設定名を指定するだけ。

:dev設定だと:optimizationsはなしで前回と同じアウトプットがdevoutディレクトリに出力される。

:prod設定だと思いっきり最適化されたJavaScriptがoutディレクトリに出力される。out内のディレクトリ構成も:

out
├── cljs
│   ├── core.cljs
│   └── core.js
├── constants_table.js
├── main.js
└── try_cljsbuild
    └── core.js

とかなり簡単になっているのがわかる。しかし、コンパイルにかかる時間は大体十倍ぐらいの違いが出る。

自動コンパイル

最後に軽く自動コンパイルについて触れておきたい。

今まで

lein cljsbuild once dev

などとやって一回ずつコンパイルしてきた。しかし開発を進めていく上でできればそういう手動なステップは最低限に留めておきたい。具体的にはsrcディレクトリ内のcljsファイルに変更を加えて保存した時点で自動的にコンパイルが行われるようにしたい。そのためにはbashから以下のコマンドを走らせる:

lein cljsbuild auto dev

そうすると以下のメッセージが表示される:

Watching for changes before compiling ClojureScript...

cljsbuildが走っている端末はそのまま放置して、他のウィンドウでcore.cljsをアップデートすると:

Compiling "devout/main.js" from ["src"]...

Successfully compiled "devout/main.js" in 0.772 seconds.

と先ほどのウィンドウで自動的にコンパイルが行われたのが通知される。そしてブラウザをリロードすれば、新しいコードが実行される。これで1ステップ省略でき、よりストレスフリーでフィードバックループが直接的な開発環境に近づいた。

しかしまだブラウザのリロードという邪魔な手順がある。次回はそれを省いてくれるツールfigwheelについて解説する。

今回のコード:

gist.github.com

Clojure Web Development勉強 - ClojureScript(その2)cljsbuildでコンパイル

Clojureビルドツールあれこれ

Clojureビルドツール界隈ではleiningenが圧倒的シェアを誇っておりそこにbootが食らいついている、というような状況になっている。

私はbootは使ったことはないが、leiningenにはいつも非常にお世話になっている。project.cljに依存ライブラリなども含めたビルドオプションを記述して、あとはライブラリのダウンロードからコンパイルから実行までleinコマンドでできてしまうのは本当に便利だ。

前回のClojureScriptのコンパイルではleiningenもbootも出る幕がなく、自分でClojureScriptコンパイラのjarを落としてきてjavaコマンドで実行していた。しかし、他のライブラリへの依存性管理や、より便利なワークフローツール(特にfigwheel!)の利用などのためにも、ビルドツールとしてleinを使っていきたい。

普段はプロジェクトの雛形すらlein new <project-name>あるいはlein new reagent <project-name>といったコマンドでleiningenに用意してもらうのだが、今回はそこは手動でやって最低限のプロジェクトの条件を見ていく。実は似たようなことをQiitaでもやったが、その後いろいろと試していくうちに知見も溜まったので・・・

ClojureScript/cljsbuild用のproject.clj

用意するディレクトリとファイルは以下の通り:

.
├── index.html
├── project.clj
└── src
    └── min_cljsbuild
        └── core.cljs

src/min_cljsbuild/core.cljsとindex.htmlは前回でもおなじみ。内容も前回と全く同じ。

ClojureScriptコンパイラcljs.jarとビルドのために実行するスクリプトであるbuild.cljが消え、そのかわりleiningenがビルドに必要な情報を記述したproject.cljが加わっている。

本当の最低限

いろいろと試した結果、cljsbuildがエラーを吐かずに「動作」するproject.cljは以下の通り:

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

  :plugins [[lein-cljsbuild "1.1.4"]]
  
  :cljsbuild {:builds []})

上記のようなproject.cljを置いて、bashlein cljsbuild onceとコマンドを走らせると

Compiling ClojureScript...

とメッセージが表示される。その後ディレクトリを調べると、いろんなファイルが出力されているのがわかる。

.
├── dev-resources
├── index.html
├── project.clj
├── resources
├── src
│   └── min_cljsbuild
│       └── core.cljs
├── target
│   ├── classes
│   │   └── META-INF
│   │       └── maven
│   │           └── min-cljsbuild
│   │               └── min-cljsbuild
│   │                   └── pom.properties
│   ├── cljsbuild-crossover
│   └── stale
│       └── extract-native.dependencies
└── test

しかし肝心のJavaScriptファイルがない。というのもcljsbuildに「どこにコンパイルしてほしいClojureScriptファイルが入っているのか」という情報を渡していなかったため、単にテンプレ的に必要なファイルやディレクトリを作成したのみで終わってしまったわけだ。

なので「コンパイルしてほしいClojureScriptはsrcディレクトリに入っていますよー」という情報を渡してやる。

JavaScriptが出力される最低限

(defproject min-cljsbuild "0.0.2"
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [org.clojure/clojurescript "1.9.293"]]

  :plugins [[lein-cljsbuild "1.1.4"]]
  
  :cljsbuild {:builds [{:source-paths ["src"]}]}) ;;ここ

まずは先ほどcljsbuildが作成したディレクトリを全部削除してしまう

rm -rf dev-resources;rm -rf test;rm -rf resources;rm -rf target

そしてもう一回実行:

lein cljsbuild once

そうするとこんなメッセージが出て:

Compiling ClojureScript...

Compiling "/min-cljsbuild/target/cljsbuild-main.js" from ["src"]...

Successfully compiled "/min-cljsbuild/target/cljsbuild-main.js" in 0.82 seconds.

こんなディレクトリ構成が出力される:

.
├── dev-resources
├── index.html
├── project.clj
├── resources
├── src
│   └── min_cljsbuild
│       └── core.cljs
├── target
│   ├── classes
│   │   └── META-INF
│   │       └── maven
│   │           └── min-cljsbuild
│   │               └── min-cljsbuild
│   │                   └── pom.properties
│   ├── cljsbuild-compiler-0
│   │   ├── cljs
│   │   │   ├── core.cljs
│   │   │   ├── core.js
│   │   │   └── core.js.map
│   │   ├── goog
│   │   │   ├── array
│   │   │   │   └── array.js
│   │   │   ├── asserts
│   │   │   │   └── asserts.js
│   │   │   ├── base.js
│   │   │   ├── debug
│   │   │   │   └── error.js
│   │   │   ├── deps.js
│   │   │   ├── dom
│   │   │   │   └── nodetype.js
│   │   │   ├── math
│   │   │   │   ├── integer.js
│   │   │   │   └── long.js
│   │   │   ├── object
│   │   │   │   └── object.js
│   │   │   ├── reflect
│   │   │   │   └── reflect.js
│   │   │   └── string
│   │   │       ├── string.js
│   │   │       └── stringbuffer.js
│   │   └── min_cljsbuild
│   │       ├── core.cljs
│   │       ├── core.cljs.cache.edn
│   │       ├── core.js
│   │       └── core.js.map
│   ├── cljsbuild-crossover
│   ├── cljsbuild-main.js
│   └── stale
│       └── extract-native.dependencies
└── test

前回見たようなJavaScriptファイルがtargetディレクトリに入っている。とくに重要なのがtarget/cljsbuild-compiler-0/goog/base.jsとtarget/cljsbuild-main.jsの2ファイルで、これが前回のout/goog/base.jsとout/main.jsに相当する。

ただ、target/cljsbuild-compiler-0/googといったディレクトリパスも「コンパイル中一時的な置き場として名前衝突が起きないようつけました」感満載だしcljsbuild-main.jsと最終的にロードするJavaScriptファイルにコンパイラツールの名前が付いているのもちょっとダサい。

ということで、後もう一歩、出力ファイルの名前とパスをproject.cljに記述する。

出力先を指定したproject.clj

(defproject min-cljsbuild "0.0.3"
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [org.clojure/clojurescript "1.9.293"]]

  :plugins [[lein-cljsbuild "1.1.4"]]
  
  :cljsbuild {:builds [{:source-paths ["src"]
                        :compiler {:output-to "out/main.js" ;;ここ
                                   :output-dir "out"}}]})

コンパイラにmain.jsの出力先と名前、そしてgoogを含めた必要とされるJavaScriptライブラリの出力先となるディレクトリを指定している。

後は先ほどと同じく、cljsbuildに作成されたディレクトリを削除してからlein cljsbuild onceを実行する。

.
├── dev-resources
├── index.html
├── out
│   ├── cljs
│   │   ├── core.cljs
│   │   ├── core.js
│   │   └── core.js.map
│   ├── goog
│   │   ├── array
│   │   │   └── array.js
│   │   ├── asserts
│   │   │   └── asserts.js
│   │   ├── base.js
│   │   ├── debug
│   │   │   └── error.js
│   │   ├── deps.js
│   │   ├── dom
│   │   │   └── nodetype.js
│   │   ├── math
│   │   │   ├── integer.js
│   │   │   └── long.js
│   │   ├── object
│   │   │   └── object.js
│   │   ├── reflect
│   │   │   └── reflect.js
│   │   └── string
│   │       ├── string.js
│   │       └── stringbuffer.js
│   ├── main.js
│   └── min_cljsbuild
│       ├── core.cljs
│       ├── core.cljs.cache.edn
│       ├── core.js
│       └── core.js.map
├── project.clj
├── resources
├── src
│   └── min_cljsbuild
│       └── core.cljs
├── target
│   ├── classes
│   │   └── META-INF
│   │       └── maven
│   │           └── min-cljsbuild
│   │               └── min-cljsbuild
│   │                   └── pom.properties
│   ├── cljsbuild-crossover
│   └── stale
│       └── extract-native.dependencies
└── test

となって、手動でcljs.jarを使った時と概ね似た構成になる。違いはdev-resources、resources、testという空ディレクトリと、コンパイル時の一時ファイルの残りであるtargetディレクトリで、これらは挙動に何ら影響しない。この時点では削除してしまっても構わないくらいである。

あとは前回と同じindex.htmlを開けば同じアラートが表示される。

今回のコード:

gist.github.com

次回はcljsbuildのオプションを少し調べていきたい。

Clojure Web Development勉強 - Clojurescript(その1)cljs.jarでコンパイル

一旦サーバサイドを離れて、フロントエンドの開発を進めてみる。

フロントエンドについては以前から少し試していて、拙いながらも渋谷のLisp.Meetupでもライブコーディングしながらプレゼンしたことがある。その時はClojurescriptからFigwheel、Reagent、d3.jsなどと結構いろんな部分に言及できた(その分話が散漫になったきらいはあるが・・・)。

初心に戻ってもう一度簡単なところから調べていきたい。

というわけで、生のclojurescriptをコンパイルするところから。

ClojureScript - Quick Start

公式サイトの上記のドキュメントをタネに、最低限のClojurescriptを書いてコンパイルして表示してみる。

そのコードというのはこれ:

(ns manual_clojurescript.core)
(js/alert "Hello Clojurescript!")

名前空間の定義と、ブラウザのアラート機能で"Hello Clojurescript!"と表示させるだけ。js/alertはClojurescriptからJavaScriptの機能を呼び出している。とりあえずこれをcore.cljsとしておく。Clojurescriptなので.cljsである。(cljcの話は違う記事で言及する)

さて、コードを書いたはいいが、これをどうすればいいのか。以下の三つのステップが必要になる。

  1. ClojurescriptをJavaScriptコンパイル
  2. JavaScriptをロードするHTMLドキュメントを作成
  3. HTMLドキュメントをブラウザで表示

そのために幾つかサポートのためのファイルとディレクトリ構成を設定する。最終的に以下のような構成になる。

.
├── build.clj
├── cljs.jar
├── index.html
└── src
    └── manual_clojurescript
        └── core.cljs

core.cljsがsrc/manual_clojurescript/のディレクトリの下にあるに注意。それ以外のファイルは全てディレクトリのトップレベルにある。

ClojurescriptをJavaScriptコンパイル

さて、まずステップ1のために必要なcljs.jarとbuild.cljについて。

cljs.jarはQuick Startに載っているリンクから落とせる、Javaで書かれているClojurescript->Javascriptコンパイラである。

そのコンパイラに出す指示を入れたファイルがbuild.cljで、内容は以下のようになっている。

(require 'cljs.build.api)
(cljs.build.api/build "src" {:output-to "out/main.js"})

srcディレクトリに含まれているcljsファイルをJSにコンパイルして、まだ未作成のoutディレクトリのmain.jsファイルに出力してくれ、という内容。

src/manual_clojurescript/core.cljs、build.cljそしてcljs.jarの三つが揃えばステップ1のコンパイルができる。bashから

java -cp cljs.jar:src clojure.main build.clj

で、outというディレクトリにmain.jsやその他もろもろが作成されるはずだ。構成としてはこんな感じ:

.
├── build.clj
├── cljs.jar
├── index.html
├── out
│   ├── cljs
│   │   ├── core.cljs
│   │   ├── core.js
│   │   └── core.js.map
│   ├── goog
│   │   ├── array
│   │   │   └── array.js
│   │   ├── asserts
│   │   │   └── asserts.js
│   │   ├── base.js
│   │   ├── debug
│   │   │   └── error.js
│   │   ├── deps.js
│   │   ├── dom
│   │   │   └── nodetype.js
│   │   ├── math
│   │   │   ├── integer.js
│   │   │   └── long.js
│   │   ├── object
│   │   │   └── object.js
│   │   ├── reflect
│   │   │   └── reflect.js
│   │   └── string
│   │       ├── string.js
│   │       └── stringbuffer.js
│   ├── main.js
│   └── manual_clojurescript
│       ├── core.cljs
│       ├── core.cljs.cache.json
│       ├── core.js
│       └── core.js.map
└── src
    └── manual_clojurescript
        └── core.cljs

outに見えるファイルの概要は以下のとおり:

  1. manual_clojurescriptディレクトリ:srcに含まれていた自前のclojurescriptコードのコンパイル
  2. cljsディレクトリ:clojurescript自体の言語機能やライブラリが含まれている
  3. googディレクトリ:Clojurescriptが乗っかっているGoogleオープンソースJavaScriptライブラリであるGoogle Clojure Libraryの出力先
  4. main.js:上記のディレクトリに含まれているJSをつなぎ合わせるコード

JavaScriptをロードするHTMLドキュメントを作成

さて、次はこのコンパイルされたJavaScriptのロード先であるHTMLドキュメント、index.htmlを作成する。今回は最低限ということで以下のようなファイルになる:

<html>
    <body>
        <script type="text/javascript" src="./out/goog/base.js"></script>
        <script type="text/javascript" src="./out/main.js"></script>
        <script type="text/javascript">goog.require("manual_clojurescript.core");</script>
    </body>
</html>

まずGoogle Closure Libraryの便利機能を裏で色々使うので、それをgoog/base.jsを通じてロードしておく。

次に自分の書いたコードも呼び出せるようmain.jsをロード。

最後に、Google Closure Libraryのgoog.requireを使って自分の書いたClojurescriptのコンパイル先であるmanual_clojure.coreを実行。これで、core.cljsに記述したロジックに従って処理が行われる。

HTMLドキュメントをブラウザで表示

それでは実際に走らせてみる。

一番手軽なのはbashから

open ./index.html

でローカルファイルとして開いてしまうこと。これでちゃんとアラートポップアップに"Hello Clojurescript!"と表示されるはず。

もうちょっとちゃんとローカルサーバ立ててみたい場合は:

python -m http.server

あるいは

python -m SimpleHTTPServer

(前者はPython 3.x、後者はPython 2.xの場合)

のようにサーバ立ててブラウザでlocalhost:8000などと見てもオッケー。

とりあえず最低限のClojurescriptを書いてコンパイルしてロードするHTML用意して開くところまでは来れた。次はlein-cljsbuildを使ったコンパイルプロセスの簡略化と自動化。