ClojureScriptやそのエコシステムは絶賛開発中のためライブラリの更新がとても盛んに行われています。2015年に出版されている英語の書籍でもすでに情報が古くなっています。最新の情報をネットで調べてもバージョンが違うと内容が異なっているので自分で手を動かして確認する必要があります。Figwheel のQuick Start を例にして最新のClojureScriptの開発方法を勉強していきます。
Figwheel Figwheel はClojureScritのコードをローカルで編集するとブラウザに自動的にプッシュして最新の状態にしてくれるLeiningen のプラグインです。最近はREPL機能も持つようになってさらに便利になっています。
figwheel templateでプロジェクトの作成 lein new
コマンドでfigwheel templateを使いプロジェクトを作成します。
$ lein new figwheel hello-cljs
templateで作成されるproject.cljのデフォルトは以下のようになっています。Quick Start も現在最新版に修正中のようですが内容が古くなっています。
~/clojure_apps/hello-cljs/project.clj (defproject hello-cljs "0.1.0-SNAPSHOT" :description "FIXME: write this!" :url "http://example.com/FIXME" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html" } :dependencies [[org.clojure/clojure "1.6.0" ] [org.clojure/clojurescript "0.0-3211" ] [org.clojure/core.async "0.1.346.0-17112a-alpha" ]] :plugins [[lein-cljsbuild "1.0.5" ] [lein-figwheel "0.3.1" ]] :source-paths ["src" ] :clean-targets ["resources/public/js/compiled" "target" ] :cljsbuild { :builds [{:id "dev" :source-paths ["src" ] :figwheel { :on-jsload "hello-cljs.core/on-js-reload" } :compiler {:main hello-cljs.core :asset-path "js/compiled/out" :output-to "resources/public/js/compiled/hello_cljs.js" :output-dir "resources/public/js/compiled/out" :source-map-timestamp true }} {:id "min" :source-paths ["src" ] :compiler {:output-to "resources/public/js/compiled/hello_cljs.js" :main hello-cljs.core :optimizations :advanced :pretty-print false }}]} :figwheel { :css-dirs ["resources/public/css" ] })
以下のようにライブラリのバージョンを上げます。特にFailed When Compiling by Using Cljs 0.0-3255 #393 にあるissueのように、ClojureScriptmの0.0-3255
以降ではClojureのバージョンは1.7.0-beta2
以降が必要です。
:cljsbuild
ディレクティブではクラウド上の開発環境にローカルのブラウザから接続できるようにWebSocketのホストをパブリックIPアドレスに指定しています。
~/clojure_apps/hello-cljs/project.clj ... :dependencies [[org.clojure/clojure "1.7.0-beta3" ] [org.clojure/clojurescript "0.0-3269" ] [org.clojure/core.async "0.1.346.0-17112a-alpha" ]] :plugins [[lein-cljsbuild "1.0.5" ] [lein-figwheel "0.3.3" ]] ... :cljsbuild { :builds [{:id "dev" :source-paths ["src" ] :figwheel { :on-jsload "hello-cljs.core/on-js-reload" :websocket-host "210.xxx.xxx.xxx" }
FigwheelのWebサーバーを起動 lein figwheel
コマンドでのWebサーバーを起動します。
$ lein figwheel Figwheel: Starting server at http://localhost:3449 Focusing on build ids: dev Compiling "resources/public/js/compiled/hello_cljs.js" from ["src" ]... Successfully compiled "resources/public/js/compiled/hello_cljs.js" in 14.0 seconds. Started Figwheel autobuilder 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 (build-once [id ...]) ;; builds source one time (clean-builds [id ..]) ;; deletes compiled cljs target files (fig-status) ;; displays current state of system (add-dep [org.om/om "0.8.1" ]) ;; add a dependency. very experimental 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
ローカルのブラウザでFigwheelのサーバーに確認します。
http://210.xxx.xxx.xxx:3449/
REPL ブラウザからWebSocketの接続ができるとREPLが使えるようになります。
Prompt will show when figwheel connects to your application To quit, type : :cljs/quit cljs.user=> cljs.user=>
JavaScriptのDate関数をnewしてみます。
cljs.user=> (js/Date.) #inst "2015-05-24T13:20:18.481-00:00"
Linuxで動作しているREPLからJavaScriptのアラートをブラウザに表示することもできます。
cljs.user=> (js/alert "browserにアラートがでます。")
ただしREPLはreadlineが有効になっていないため上アローキーやC-p
でヒストリーバックが効きません。rlwrap でleinコマンドをラップすると良いらしいですが、Docker Composeで実行するとうまく動作できませんでした。
$ docker-compose run --rm --service-ports lein figwheel rlwrap: error: My terminal reports width=0 (is it emacs?) I can't handle this, sorry! Removing clojureapps_lein_run_1...
Quick Startと違うところ figwheel templateで作成されるcore.cljsのデフォルトです。
~/clojure_apps/hello-cljs/src/hello_cljs/core.cljs (ns ^:figwheel-always hello-cljs.core (:require )) (enable-console-print! ) (println "Edits to this text should show up in your developer console." ) (defonce app-state (atom {:text "Hello world!" })) (defn on-js-reload [] )
(enable-console-print!) Quick StartではJavaScriptのconsoleを使う例になっています。
(.log js/console "Hey Seymore! wts goin' on?" )
figwheel templateでは(enable-console-print!)
が有効になっています。ClojureScriptのcore.cljs で定義されている関数です。
(defn enable-console-print! "Set *print-fn* to console.log" [] (set! *print-newline* false) (set! *print-fn* (fn [& args] (.apply (.-log js/console) js/console (into-array args)))))
ちなみにスレッド毎に動的束縛が使えるな変数をアスタリスクで囲うことをearmuff
と呼ぶそうです。かわいい。。
println関数を使ってブラウザのコンソールにログを出力してみます。
~/clojure_apps/hello-cljs/src/hello_cljs/core.cljs (ns ^:figwheel-always hello-cljs.core (:require )) (enable-console-print! ) (println "enable-console-print!を実行するとprintlnが使えます" ) )
on-js-reload関数 on-js-reload関数をコメントアウトするとフックが呼ばれなくなります。
ブラウザのコンソールに毎回ログがでるので残しておいた方がよさそうです。
Figwheel: :on-jsload hook 'hello-cljs.core/on-js-reload' is missing
index.html figwheel templateではid="app"
とCSSの読み込みがすでにが定義されています。
~/clojure_apps/hello-cljs/resources/public/index.html <!DOCTYPE html > <html > <head > <link href ="css/style.css" rel ="stylesheet" type ="text/css" > </head > <body > <div id ="app" > <h2 > Figwheel template</h2 > <p > Checkout your developer console.</p > </div > <script src ="js/compiled/hello_cljs.js" type ="text/javascript" > </script > </body > </html >
Quick Startしてみる Quick Start の手順にしたがってプログラムを修正していきます。
Sablono project.cljにSablono を追加します。SablonoはHiccup 風に記述してReact 用のHTMLをレンダリングできるテンプレートです。Om
~/clojure_apps/hello-cljs/project.clj :dependencies [[org.clojure/clojure "1.7.0-beta3" ] [org.clojure/clojurescript "0.0-3269" ] [org.clojure/core.async "0.1.346.0-17112a-alpha" ] [sablono "0.3.4" ]]
lein clean
をしてから依存関係をダウンロードします。
$ lein clean $ lein figwheel Retrieving sablono/sablono/0.3.4/sablono-0.3.4.pom from clojars Retrieving sablono/sablono/0.3.4/sablono-0.3.4.jar from clojars
defonce Quick Startでは以下のように最初はdef
で定義してブラウザでリロードするとstateが消えてしまう例が書いてあります。defonce
に変更して状stateを保持するようになっています。
(def app-state (atom { :likes 0 }))
figwheel templateではデフォルトでdefonce
を使っています。
~/clojure_apps/hello-cljs/src/hello_cljs/core.cljs (defonce app-state (atom {:text "Hello world!" }))
Quick Startのようにatomの変更して:links
キーを持つマップにします。
~/clojure_apps/hello-cljs/src/hello_cljs/core.cljs (defonce app-state (atom {:likes 0 }))
defonce
はcore.clj に定義されているマクロです。この辺りがClojureらしいところです。
(defmacro defonce [x init] `(when-not (exists? ~x) (def ~x ~init)))
core.cljsは以下のようになります。
~/clojure_apps/hello-cljs/src/hello_cljs/core.cljs (ns ^:figwheel-always hello-cljs.core (:require [sablono.core :as sab] [hello-cljs.components :refer [like-seymore]])) (enable-console-print! ) (println "enable-console-print!を実行するとprintlnが使えます" ) (defonce app-state (atom {:likes 0 })) (defn like-seymore [data] (sab/html [:div [:h1 "いいね!の数: " (:links @data)] [:div [:a {:href "#" :onClick #(swap! data update-in [:links ] inc)} "いいね!" ]]])) (defn render! [] (.render js/React (like-seymore app-state) (.getElementById js/document "app" ))) (add-watch app-state :on-change (fn [_ _ _ _ ] (render! ))) (render! ) (defn on-js-reload [])
ブラウザを開き、いいね!
のリンクをクリックするとカウントアップします。cljsを編集してコードをプッシュしてもstateは保持されるので0に戻りません。
自動リロードの設定 ^:figwheel-always 自動リロードの使い方もQuick Startとtemplateでは違いがあります。テンプレートではcore.cljsのnsの先頭に^:figwheel-always
が入っています。
~/clojure_apps/hello-cljs/src/hello_cljs/core.cljs (ns ^:figwheel-always hello-seymore.core (:require [sablono.core :as sab] [hello-seymore.components :refer [like-seymore]]))
^:figwheel-always
をnsの先頭に記述するとcljsに変更があるとnamespaceのリロード対象になります。
cljsの分割 like-saymore関数を切り出してcomponents.cljsを作成します。ns関数に^:figwheel-always
を追加してリロードの対象に追加します。
~/clojure_apps/hello-cljs/src/hello_cljs/components.cljs (ns ^:figwheel-always hello-cljs.components (:require [sablono.core :as sab])) (defn like-seymore [data] (sab/html [:div [:h1 "いいね!の数: " (:links @data)] [:div [:a {:href "#" :onClick #(swap! data update-in [:links ] inc)} "いいね!" ]]]))
^:figwheel-always
を使うとcomponents.cljsを編集しただけでもcore.jsもリロードされます。namespace全体がリロードされるのでプログラムが大きくなった時には遅くなりそうなので注意が必要です。
("hello_cljs/components.js" "hello_cljs/core.js" )
CSSのリロードとコンパイル済みのアセット コンパイル済みのアセットをresources/public
ディレクトリからサーブします。あわせてCSSを修正しても自動リロードされるようにします。こちらもテンプレートではデフォルトでproject.cljに設定が有効になっています。
asset-path、outputo-to、output-dirの指定
clean-targetsの指定
~/clojure_apps/hello-cljs/project.clj ... :clean-targets ["resources/public/js/compiled" "target" ] :cljsbuild { :builds [{:id "dev" :source-paths ["src" ] :figwheel { :on-jsload "test.core/on-js-reload" } :compiler {:main test.core :asset-path "js/compiled/out" :output-to "resources/public/js/compiled/test.js" :output-dir "resources/public/js/compiled/out" :source-map-timestamp true }} :figwheel { :css-dirs ["resources/public/css" ] }
以下のようにCSSを記述すると自動的に変更がブラウザにプッシュされます。
clojure_apps/hello-cljs/resources/public/css/style.css body { background-color : yellow; }