0%

Figwheelで最新のClojureScriptの開発方法を勉強をする

ClojureScriptやそのエコシステムは絶賛開発中のためライブラリの更新がとても盛んに行われています。2015年に出版されている英語の書籍でもすでに情報が古くなっています。最新の情報をネットで調べてもバージョンが違うと内容が異なっているので自分で手を動かして確認する必要があります。FigwheelQuick 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 ^{:protect false} ["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 {
;; :http-server-root "public" ;; default and assumes "resources"
;; :server-port 3449 ;; default
:css-dirs ["resources/public/css"] ;; watch and update 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.")

;; define your app data so that it doesn't get over-written on reload

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


(defn on-js-reload []
;; optionally touch your app-state to force rerendering depending on
;; your application
;; (swap! app-state update-in [:__figwheel_counter] inc)
)

(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が使えます")
)

figwheel-println.png

on-js-reload関数

on-js-reload関数をコメントアウトするとフックが呼ばれなくなります。

(defn on-js-reload []
;; optionally touch your app-state to force rerendering depending on
;; your application
;; (swap! app-state update-in [:__figwheel_counter] inc)
)

ブラウザのコンソールに毎回ログがでるので残しておいた方がよさそうです。

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}))

defoncecore.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に戻りません。

5times.png

自動リロードの設定

^: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 ^{:protect false} ["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"] ;; watch and update CSS
}

以下のようにCSSを記述すると自動的に変更がブラウザにプッシュされます。

clojure_apps/hello-cljs/resources/public/css/style.css
body {
background-color: yellow;
}