LightTableによるclojure開発を始めるための手順です。
今回は例としてduct projectの開発を始めるまでの手順を紹介します。
基本方針はRDD(REPL駆動開発)です。
この手順により(理想的には)一度もreplを切らずに開発を進めることが可能です。
下記がインストールされていることを前提とします。
- jdk 1.8 以上
- Leiningen 2.5.2 以上
- LightTable 0.8.1 以上
IDEとしてLightTableを推奨するのはclojureに対するcode formatが強力なこととnreplに接続可能だからです。
LightTableはショートカットキーなどが若干特殊なため必要に応じて設定変更する必要があります。下記は私の例です。
- LighTableのツールバーより、[View] -> [Commands]で開いたCommandsバーに"userkeymap"と入力し、"Settings: User keymap"を選択
- 開いたuser.keymapに下記を内容追加
;; User keymap
;; ...
[
;; ...
[:app "cmd-p" [:navigate-workspace]] ;; Navigator表示のtoggle
[:app "cmd-shift-p" [:show-commandbar]] ;; Commandbar表示のtoggle
[:app "cmd-shift-c" [:toggle-console]] ;; console表示のtoggle
[:app "cmd-shift-d" [:workspace.show]] ;; workspace表示のtoggle
[:app "cmd-shift-o" [:workspace.add-folder]] ;; 新workspaceの追加
[:editor "cmd-i" [:smart-indent-selection]] ;; code format
[:editor "cmd-shift-r" [:instarepl]] ;; instareplを起動
[:editor "cmd-r" [:instarepl-current]] ;; 現在開いているファイル上にinstareplを起動
[:editor "f3" [:editor.jump-to-definition-at-cursor]] ;; 定義に飛ぶ
[:editor "shift-f3" [:editor.unjump]] ;; 呼び出し元に戻る
[:editor "cmd-/" [:toggle-comment-selection]] ;; コメント行のtoggle
[:editor "cmd-m" [:editor.doc.toggle]] ;; 関数のdocのtoggle
[:editor "cmd-d" [:editor.sublime.selectNextOccurrence]] ;; 現在選択中の単語をマルチセレクト
[:editor "cmd-shift-l" [:editor.sublime.splitSelectionByLine]] ;; 選択行を行ごとにマルチセレクト
]
sublime text 3 + eclipseっぽいkeymapです。
- [Cmd + Shift + p] で開いたCommandsバーに"userbehaviors"と入力し、"Settings: User behaviors"を選択
- 開いたuser.behaviorsに下記内容を追加
;; User behaviors
;; ...
[
;; ...
[:editor :lt.objs.editor/line-numbers] ;; 行番号の表示
[:editor :lt.objs.editor/highlight-current-line] ;; カーソルがある行のハイライト
[:editor :lt.objs.editor.file/remove-trailing-whitespace] ;; ファイル保存時に行末のスペースを削除
[:app :lt.objs.intro/show-intro false] ;; 起動時にintroページを開かない
]
- [Cmd + Shift + p] で開いたCommandsバーに"showplugin"と入力し、"Plugins: Show plugin manager"を選択
- "Reminisce"で検索し"install"実行
ReminisceはLighttableを終了して再び開いた際に以前のタブ状態を復元していくれるほぼ必須のpluginです。
leiningenによりduct projectを作成します。
$ lein new duct myduct +cljs +site +example
下記の構成で新規projectが生成されます。
myduct
├── README.md
├── project.clj
├── dev
│ ├── cljs
│ │ └── user.cljs
│ └── user.clj
├── resources
│ └── myduct
│ ├── endpoint
│ │ └── example
│ │ └── example.html
│ ├── errors
│ │ ├── 404.html
│ │ └── 500.html
│ └── public
│ ├── css
│ │ └── site.css
│ ├── favicon.ico
│ ├── index.html
│ └── robots.txt
├── src
│ └── myduct
│ ├── component
│ ├── endpoint
│ │ └── example.clj
│ ├── config.clj
│ ├── main.clj
│ └── system.clj
└── test
└── myduct
└── endpoint
└── example_test.clj
上記の構成はclojureでは一般的なproject構成ですがjava srcを追加した場合などに都合が悪いため下記構成に直します。
myduct //
├── README.md //
├── project.clj // <- clojure projectの構成や依存関係、ビルド設定などを設定するファイル.
└── src //
├── dev // <- dev profileで起動した場合に含めるsrc. repl起動時にここを初期nsとして利用するため、開発用の関数などを定義する.
│ ├── clj //
│ │ └── user.clj //
│ └── cljs //
│ └── user.cljs //
├── main // <- app src. java srcが必要になった場合はここ以下にjavaディレクトリを切る.
│ ├── clj //
│ │ └── myduct //
│ │ ├── endpoint //
│ │ │ └── example.clj //
│ │ ├── component //
│ │ ├── config.clj //
│ │ ├── main.clj //
│ │ └── system.clj //
│ ├── cljs //
│ └── resources //
│ └── myduct //
│ ├── endpoint //
│ │ └── example //
│ │ └── example.html //
│ ├── errors //
│ │ ├── 404.html //
│ │ └── 500.html //
│ └── public //
│ ├── css //
│ │ └── site.css //
│ ├── favicon.ico //
│ ├── index.html //
│ └── robots.txt //
└── test // <- test src.
└── clj //
└── myduct //
└── endpoint //
└── example_test.clj //
上記の構成変更に伴いproject.clj, user.cljの該当部分の修正が必要となります。
また開発に役立つleiningen pluginとして下記の導入を推奨します。
leiningen pluginへの依存や設定は通常project.cljの開発プロファイルに追加しますが、"~/.lein/profiles.clj"に追加することで全てのprojectに反映させることも可能です。
plugin | |
---|---|
eastwood | Clojure lint tool |
lein-ancient | 依存ライブラリのversionチェック及び最新化 |
alembic | project.cljの動的読み込み |
lein-light-nrepl | nreplにLightTableから接続可能に |
ring/ring-mock | endpointのテストを簡易化するmock |
修正版はこちら。
Lightableにmyduct projectをimportします。
- [Cmd + Shift + o] で開いたダイアログからmyductディレクトリを選択して下さい
- [Cmd + Shift + d] で開いたworkspaceにmyductが追加されていることを確認
myduct project直下で下記のコマンドを打つことでreplを起動します。
$ lein repl
なお、leiningenは起動時に下記のようにprofileを指定することでビルドの設定を切り替えることも出来ます。
$ lein with-profile [profile] repl
指定しない場合のデフォルトではdevを含む[base system user provided dev]が有効化されるようです。
https://github.com/technomancy/leiningen/blob/master/doc/PROFILES.md
$ lein repl
nREPL server started on port 54426 on host 127.0.0.1 - nrepl://127.0.0.1:54426
REPL-y 0.3.7, nREPL 0.2.12
Clojure 1.8.0
Java HotSpot(TM) 64-Bit Server VM 1.8.0_65-b17
Docs: (doc function-name-here)
(find-doc "part-of-name-here")
Source: (source function-name-here)
Javadoc: (javadoc java-object-or-class-here)
Exit: Control+D or (exit) or (quit)
Results: Stored in vars *1, *2, *3, an exception in *e
user=>
2行目に表示されているnrepl://[host]:[port]でLighTableから接続します。
- [Cmd + Shift + p] で開いたCommandsバーに"addconnection"と入力し、"Connect: Add Connection"をして下さい
- 接続一覧から"Clojure (remote nREPL)"をして下さい
- "Server:"に上記のnreplの[host]:[port]を入力し、[connect]を押下して下さい
- フッターに"Connected to [host]:[port]"と表示されたことを確認します
以上でLightTableによる開発の準備が全て整いました。
webアプリとしてはDB接続やその他middlewareなど必要なライブラリ、またnsも不足していますが 以降の開発手順を実践することでそれらは動的に追加していくことが可能です。
以降の開発はrepl及びLightable上で行います。
まずreplよりwebアプリを起動します。
起動時の処理にcljsのビルドが設定されておりcljsディレクトリが存在しないと失敗するため、事前にディレクトリを作成する必要があります。
$ mkdir src/main/cljs
...
user=> (go) ;; Enter
Compiling "target/figwheel/myduct/public/js/main.js" from ["src/main/cljs" "src/dev/cljs"]...
notifying browser that file changed: target/figwheel/myduct/public/js/goog/deps.js
notifying browser that file changed: target/figwheel/myduct/public/js/cljs_deps.js
notifying browser that file changed: out/cljs/user.js
Successfully compiled "target/figwheel/myduct/public/js/main.js" in 0.23 seconds.
sending changed CSS file: src/main/resources/myduct/public/css/site.css
:started
webアプリが3000番ポートで起動しているのでブラウザからアクセスします。
画面が表示されたことを確認したら開発を進めます。
RDDにおいてはreplから起動済みのインスタンス上でUnit testを実行できるため、高速にTDDを実践することが出来ます。
duct は eftest というライブラリを使うことを推奨しているようです。
指定したパスから全てのtestを検出して実行するだけのライブラリなので別の選択肢もありますが今回はこれを使います。
今回のprojectでは既にuser.cljにeftestの実行用関数 test が用意されています。
;; myduct/src/dev/user.clj
;; ...
(ns-unmap *ns* 'test) ;; coreライブラリの関数名とぶつかるため名前空間からunmapしている
(defn test []
(eftest/run-tests (eftest/find-tests "src/test/clj") {:multithread? false}))
;; ...
replからこのtest関数を評価(clojureでは括弧で囲むことで関数を実行できそれを評価という)すれば自動テストが実行されます
user=> (test) ;; Enter
1/1 100% [==================================================] ETA: 00:00
Ran 1 tests in 0.006 seconds
1 assertion, 0 failures, 0 errors.
nil
既に実装済みの example_test.clj#smoke-test が実行されて成功しました。
では次にendpointにユーザ情報を返すrouteを追加してみましょう。
仕様は下記です。
- /example/user/1 を叩くことでid=1のuser情報(id, name)が返却される
db接続がまだ存在しないため静的なmapからユーザ検索する仕様で作成します。
まず example.clj に空のユーザ検索関数を追加します。
;; src/main/clj/myduct/endpoint/example.clj
(defn get-user [id])
作成した関数をLightTbale上で評価して関数を利用可能にします。
LightTable上での評価とはclojureのコードをインスタンスに渡して定義/実行することです。
LighTableで書いたコード上にカーソルを置き [Cmd + Enter] を押下することで評価を実行できます。
評価したコードの後ろに正常な評価結果が表示されれば成功です。
成功すると起動中のインスタンスに関数定義が追加されます。
;; src/main/clj/myduct/endpoint/example.clj
(defn get-user [id]) ;; [Cmd + Enter] --> #'myduct.endpoint.example/get-user と表示される
次に作成した関数が満たすべき仕様に基づきテストを書きます。
;; src/test/clj/myduct/endpoint/example_test.clj
(deftest get-user-test
(testing "存在するユーザIDを指定した場合ユーザ情報を取得できる"
(is (= {:id 1 :name "test user 1"} (example/get-user 1))))
(testing "存在しないユーザIDを指定した場合nilが返却される"
(is (nil? (example/get-user 4)))))
上記テストコードををLightTable上で評価し、replからテストを実行します。
評価することでインスタンスにはテストが追加されているのでeftestがテストを検出します。
user=> (test) ;; Enter
FAIL in myduct.endpoint.example-test/smoke-test (/work/myduct/src/test/clj/myduct/endpoint/example_test.clj:19)
存在するユーザIDを指定した場合ユーザ情報を取得できる
expected: {:id 1, :name "test user 1"}
actual: nil
1/1 100% [==================================================] ETA: 00:00
Ran 1 tests in 0.026 seconds
2 assertions, 1 failure, 0 errors.
nil
get-user関数はまだ空実装のためテストが失敗しました。
テストを通るようにget-user関数を実装します。
;; src/main/clj/myduct/endpoint/example.clj
(defn get-user [id]
(->> [{:id 1 :name "test user 1"}
{:id 2 :name "test user 2"}
{:id 3 :name "test user 3"}]
(filter #(= (Integer/parseInt id) (:id %)))
first))
上記関数を評価してreplからもう一度testを実行します。
user=> (test) ;; Enter
1/1 100% [==================================================] ETA: 00:00
Ran 1 tests in 0.005 seconds
2 assertions, 0 failures, 0 errors.
nil
仕様を満たすget-userが完成しました。
次にhttp経由でユーザ情報が取得できるようにhandlerにルートを追加します。
;; src/main/clj/myduct/endpoint/example.clj
(defn example-endpoint [config]
(context "/example" []
(GET "/" []
(io/resource "myduct/endpoint/example/example.html"))
(GET "/user/:id" [id]
(str (get-user id)))))
既に実装済みのsmoke_testを元に、追加したルートが結果を返すというテストを追加します。
;; src/test/clj/myduct/endpoint/example_test.clj
(deftest get-user-route-test
(testing "example page exists"
(-> (session handler)
(visit "/example/user/1")
(has (status? 200) "page exists"))))
しかし、この状態では追加したコードを評価してtestを実行すると失敗します。
user=> (test) ;; Enter
FAIL in myduct.endpoint.example-test/get-user-route-test (/work/myduct/src/test/clj/myduct/endpoint/example_test.clj:27)
example page exists
page exists
expected: (status? 200)
actual: nil
2/2 100% [==================================================] ETA: 00:00
Ran 2 tests in 0.006 seconds
3 assertions, 1 failure, 0 errors.
nil
原因は、ductはendpointをcomponentという一つのライフサイクルを持ったオブジェクトとして管理しているためです。
endpointの変更を反映させるためにはコードの評価以外にcomponentの初期化が必要となります。
初期化はductが提供するreset関数により可能です。
user=> (reset) ;; Enter
:reloading (myduct.config myduct.endpoint.example myduct.system myduct.main myduct.endpoint.example-test user)
Compiling "target/figwheel/myduct/public/js/main.js" from ["src/main/cljs" "src/dev/cljs"]...
notifying browser that file changed: target/figwheel/myduct/public/js/goog/deps.js
notifying browser that file changed: target/figwheel/myduct/public/js/cljs_deps.js
notifying browser that file changed: out/cljs/user.js
Successfully compiled "target/figwheel/myduct/public/js/main.js" in 0.742 seconds.
sending changed CSS file: src/main/resources/myduct/public/css/site.css
:resumed
user=> (test) ;; Enter
3/3 100% [==================================================] ETA: 00:00
Ran 3 tests in 0.010 seconds
4 assertions, 0 failures, 0 errors.
nil
http://localhost:3000/example/user/1 にアクセスするとユーザ情報が表示されることがわかります。
repl + LighTableによる開発は、基本的にこの手順を繰り返すことで行います。
開発を進めるにあたり新しい名前空間(name space : ns)が必要になった場合に実施する手順です。
myduct.util という ns が必要になった場合を例に説明します。
まず、対応するcljファイルを作成します。
LightTableのworkspaceからmyduct/src/main/clj/myduct を開き、[右クリック] -> [New] でutil.cljを作成します。
;; src/main/clj/myduct/util.clj
(ns myduct.util)
(defn ->int
[v]
(Integer/parseInt (str v)))
(ns myduct.util) を評価することでインスタンス上にnsが追加されます。
defn 以下を評価することでnsに関数が追加されます。
次にnsに定義した関数をendpointから利用します。
;; src/main/clj/myduct/endpoint/example.clj
(ns myduct.endpoint.example
(:require [compojure.core :refer :all]
[myduct.util :as util] ;; <-- 追加
[clojure.java.io :as io]))
(defn get-user [id]
(->> [{:id 1 :name "test user 1"}
{:id 2 :name "test user 2"}
{:id 3 :name "test user 3"}]
(filter #(= (util/->int id (:id %)))) ;; <-- 修正
first))
endpoint の ns を再評価することで endpoint に util への依存が追加されます。
開発を進めていく上で新しい依存ライブラリが必要になった場合の手順です。
/example/user/1 は現状でednを文字列として返却していますがjson形式で返す必要が発生したとします。
clojureのjson parserはいくつか存在しますがcheshireを使うことにします。
まず project.clj に chesire への dependency を追加します。
;; project.clj
(defproject myduct "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:min-lein-version "2.0.0"
:dependencies [[org.clojure/clojure "1.8.0"]
[org.clojure/clojurescript "1.7.228"]
;; ...
[cheshire "5.5.0"]] ;; <-- 追加
;;...
)
project.cljはproject情報を表したただの静的なhasnmapです。
LightTableから直接評価することは出来ません。
そこですでに myduct に追加済みの alembic というライブラリを利用します。
alembic はproject.clj の情報を動的に読み取ってインスタンスに反映することが出来ます。
user.clj から alembic.still/load-project という関数を呼び出します。
まず LightTable上でuserにalembic.still/load-projectへの依存を追加し、評価します。
;; src/dev/clj/myduct/user.clj
(ns user
(:require [clojure.repl :refer :all]
[alembic.still :refer [load-project]])) ;; <-- 追加 して [Cmd] + [Enter]
これでインスタンス上でも依存が追加されているので、replから直接load-projectを呼び出せます。
user=> (load-project) ;; Enter
WARN: com.fasterxml.jackson.core/jackson-core version 2.5.3 requested, but 2.6.1 already on classpath.
Loaded dependencies:
[[alembic "0.3.2"]
;; ...
[cheshire "5.5.0"]
;; ...
[ring/ring-ssl "0.2.1"]]
Dependencies not loaded due to conflict with previous jars :
[[com.fasterxml.jackson.core/jackson-core "2.5.3"]]
nil
chesireが追加されたことがわかります。
chesireへの依存をendpointに追加します。
;; src/main/clj/myduct/endpoint/example.clj
(ns myduct.endpoint.example
(:require [compojure.core :refer :all]
[myduct.util :as util]
[clojure.java.io :as io]
[cheshire.core :refer [generate-string]])) ;; <-- 追加
;; ...
(defn example-endpoint [config]
(context "/example" []
(GET "/" []
(io/resource "myduct/endpoint/example/example.html"))
(GET "/user/:id" [id]
(generate-string (get-user id))))) ;; <-- 修正
LightTable でコードを評価しreplで reset を実行します
ブラウザから http://localhost:3000/example/user/1 へアクセスするとjsonで結果が表示されることがわかります。
cljs開発はdev以下の下記nsを起点に行います。
このnsではcljs用開発環境(figwheel等など)の設定、起動、開発用関数の定義などを行います。
figwheelはwsでjsの更新をリアルタイムに画面に反映させるleiningen pluginです。
startにより指定されたwebsocket-urlでブラウザからインスタンスに接続します。
;; src/dev/cljs/user.cljs
(ns cljs.user
(:require [figwheel.client :as figwheel]))
(js/console.info "Starting in development mode")
(enable-console-print!)
(figwheel/start {:websocket-url "ws://localhost:3449/figwheel-ws"})
実際のアプリ部分はsrc/main/cljs以下に配置し、このnsから呼び出すようにします。
例えばreact(om/reagent)を使う場合はここからlender関数を実行するようにします。
アプリのエントリーポイントとしてcore.cljsを実装します。
;; src/main/cljs/myduct/core.cljs
(ns myduct.core)
(defn run [] (println "Hello World!"))
実装したcore.cljsをuser.cljsから呼び出します。
;; src/dev/cljs/user.cljs
(ns ^:figwheel-no-load cljs.user ;; <-- 追加:figwheelでこのファイルはloadしない
(:require [figwheel.client :as figwheel]
[myduct.core :as core]))
...
(enable-console-print!)
(defn run [] (core/run)) ;; <-- 追加:figwheelから直接呼び出すと更新を反映できないためラッパー関数を作る
(figwheel/start {:websocket-url "ws://localhost:3449/figwheel-ws"
:on-jsload run}) ;; <-- 追加:figwheelでloadした際にcllbackする
(run) ;; <-- 追加:初回ロード時には直接呼び出す
ductはgo/reset実行時にcljsのビルドを実行します。
cljsビルドの設定は下記ファイルにあります。
;; src/dev/clj/user.clj
;; ...
(def dev-config
{:app {:middleware [wrap-stacktrace]}
:figwheel
{:css-dirs ["src/main/resources/myduct/public/css"]
:builds [{:source-paths ["src/main/cljs" "src/dev/cljs"]
:build-options
{:optimizations :none
:main "cljs.user"
:asset-path "/js"
:output-to "target/figwheel/myduct/public/js/main.js"
:output-dir "target/figwheel/myduct/public/js"
:source-map true
:source-map-path "/js"}}]}})
ビルドされたjsファイルは target/figwheel/myduct/public/js 以下に出力されます。
devプロファイルでは target/figwheel を resources に追加し、src/main/clj/myduct/system.clj で resources 以下 myduct/public をサーバで公開しているため、
ビルドしたファイルは uri : /js で参照できます。
resetを実行して下さい。
user=> (reset) ;; Enter
:reloading ()
Compiling "target/figwheel/myduct/public/js/main.js" from ["src/dev/cljs" "src/main/cljs"]...
notifying browser that file changed: target/figwheel/myduct/public/js/cljs_deps.js
notifying browser that file changed: out/myduct/core.js
notifying browser that file changed: out/cljs/user.js
Successfully compiled "target/figwheel/myduct/public/js/main.js" in 0.26 seconds.
:resumed
"src/dev/cljs" "src/main/cljs"がビルドされてjsファイルが作成されました。
また、ファイルの更新がブラウザに通知されたこともわかります。
http://localhost:3000 を開いてjsコンソールを確認して下さい。
user.cljs:5 Starting in development mode
core.cljs:150 Hello World!
utils.cljs:38 Figwheel: trying to open cljs reload socket
utils.cljs:38 Figwheel: socket connection established
myduct.core/run が呼びだされていることがわかります。
core.cljsを更新します。
;; src/main/cljs/myduct/core.cljs
;; ...
(defn run [] (println "Hello World?")) ;; <-- 更新
resetします。
user=> (reset) ;; Enter
:reloading ()
Compiling "target/figwheel/myduct/public/js/main.js" from ["src/dev/cljs" "src/main/cljs"]...
notifying browser that file changed: out/myduct/core.js
Successfully compiled "target/figwheel/myduct/public/js/main.js" in 0.226 seconds.
:resumed
http://localhost:3000 を開いてjsコンソールを確認して下さい。
リロードは不要です。
utils.cljs:38 Figwheel: notified of file changes
utils.cljs:38 Figwheel: loaded these files
utils.cljs:40 ("../myduct/core.js")
utils.cljs:38 Figwheel: NOT loading these files
utils.cljs:40 figwheel-no-load meta-data: ("../cljs/user.js")
core.cljs:150 Hello World?
ファイルの変更がブラウザに通知され、変更内容が出力されていることがわかります。
figwheelの機能を使うことによって、cljsもreplで開発することが出来ます。
まず、repl上でcljs-replを起動します。
user=> (cljs-repl) ;; Enter
To quit, type: :cljs/quit
nil
ブラウザで http://localhost:3000/ を開いた状態で、cljs-replにcljsコードを入力します。
cljs.user=> (js/alert "test") ;; Enter
nil
cljs-replに入力した内容がリアルタイムにjsにコンパイルされブラウザにpushされます。
結果、ブラウザに"test"というアラートが表示されます。
cljのreplに戻るときは下記を入力します。
cljs.user=> :cljs/quit ;; Enter
nil
user=>
cssの変更もfigwheelがブラウザにpushしてくれます。(下記の設定)
;; src/dev/clj/user.clj
;; ...
(def dev-config
{:app {:middleware [wrap-stacktrace]}
:figwheel
{:css-dirs ["src/main/resources/myduct/public/css"] ;; <-- ここ
:builds [{:source-paths ["src/main/cljs" "src/dev/cljs"]
;; ...
:source-map-path "/js"}}]}})
/* src/main/resources/myduct/public/css */
/* ... */
.welcome body {
background: red; /* <-- 変更 */
color: #333;
font-family: Helvetica, Arial, sans-serif;
max-width: 700px;
padding: 15px;
margin: auto;
}
user=> (reset) ;; Enter
:reloading ()
sending changed CSS file: src/main/resources/myduct/public/css/site.css
:resumed
画面を確認するとリロード無しでcss変更が反映されていることがわかります。
以上の手順を繰り返せば(理想的には)replを一度も切らずに開発を実践できることができます。
TODO
Practical Reader Conditionals & Transit format
基本的にclojure-style-guideに従います。
indentに関してはLighTableのformatterで問題ありません。