Let’s start ClojureScript with Luminus template and Figwheel

先日の発表をしたときに ClojureScript の初心者向けの環境ってなんだろうって思ったけど、普通に Luminus を使えば良かった。

重ねて言うけど、 Chestnut は少々古いので積極的にオススメしない。と思ったけど この PR が取り込まれればいい感じになるのでこちらを使っても全然良いと思う。

今回は Figwheel を使った自動リロード環境の使い方を最もポピュラーなテンプレートである Luminus を使って説明したいと思う。

前提

最低限 Leiningen が使えればとりあえず問題ありません。また、分かりやすくは書くつもりですが、ターミナルなどの操作方法については解説しませんので悪しからず。

今回使うもののバージョンはこのようになっています。

  • Leiningen v1.5.1
  • Luminus v2.9.8.3
  • Clojure v1.7.0
  • ClojureScript v1.7.10

Clojure/ClojureScript のバージョンは Luminus テンプレートの中で指定されますが、このドキュメントの情報としての価値はこの頃のものであるというのを明示するために書いています。 また Luminus テンプレートのバージョン指定は現在できないため、基本的にその時点の最新となってしまいます(恐らく 1,2 年くらいこの情報は使えると思っていますが…)。 athos さんがこの問題を解決するための PR を送っていてそれが取り込まれるであろう次のバージョン(恐らく Leiningen v1.5.2? )では解消されていると思います。

この記事のゴールは ClojureScript の自動リロード環境を Luminus テンプレートを使って作り、それを使う方法を説明することです。またもう少し進んだ話も最後の方でしたいと思います。

Luminus で Web アプリを作成する

Leiningen はインストールしてある前提で話を進めます。もしインストールしてなければ公式サイトの install の項を参照してインストールしてください。

まずは Web アプリを作成しましょう。これは Luminus を使えば簡単にできます。ターミナルで次のコマンドを実行しましょう。

$ lein new luminus luminus-app +cljs

着目する点としては +cljs フラグをつけることでしょうか。これによって ClojureScript のモダンな開発環境が簡単に作ることができます。

初めて Luminus テンプレートを使う場合は色々とダウンロードされるので、待ちましょう。作成されたら次のコマンドをターミナルで実行します。

$ cd luminus-app
$ lein repl

とすると REPL が起動できると思うので、次に REPL の中で次のフォームを評価しましょう。

luminus-app.core=> (start-app [3000])

そしたら REPL 中にサーバーを起動したというメッセージが出てくると思うので、 http://localhost:3000/ に接続しましょう。

../../../_images/before_cljs_compile.png

このような画面が見えたらここまでのステップは問題ないです。ここまでで Figwheel を使う準備が実は整ってしまっています。

このサーバーは次のステップでも使うのでそのまま起動したままにしておいてください。

Figwheel の自動リロードを実際に使う

さて、ここまで Figwheel, Figwheel と書いてきましたが、これは数ある Leiningen プラグインのひとつです。

Figwheel の大きな特徴がライブコードリローディングです。これは以下の動画を見てもらえれば早いですが、コードをセーブしたタイミングで自動的にブラウザの環境をリロードする仕組みを提供しています。リフレッシュではないところが肝です。

これを今回の Luminus で作ったアプリの中でも使ってみましょう。

先ほどの REPL の中でサーバーを起動することまでは出来ました。そのサーバーはそのままに別のターミナルを開いて、プロジェクトルート (luminus-app/) を開きます。

そうしたら Figwheel サーバーを起動しましょう。以下のコマンドを実行してください。

$ lein figwheel

サーバーが起動したら、 http://localhost:3000/ を開いている画面をリフレッシュしてください。すると次のような画面が見えるはずです。

../../../_images/after_cljs_compile.png

これが見えたら実際にコードを書き換えていきましょう。分かりやすいようにエディタとブラウザを同時に見えるようにしておくのをオススメします。

luminus-app/src-cljs/luminus_app/core.cljs をエディタで開いてみましょう。開いたら次の home-page 関数を探します(画面上部に見えているはず)。

(defn home-page []
  [:div.container
   [:div.jumbotron
    [:h1 "Welcome to luminus-app"]
    [:p "Time to start building your site!"]
    [:p [:a.btn.btn-primary.btn-lg {:href "http://luminusweb.net"} "Learn more »"]]]
   [:div.row
    [:div.col-md-12
     [:h2 "Welcome to ClojureScript"]]]
   (when-let [docs (session/get :docs)]
     [:div.row
      [:div.col-md-12
       [:div {:dangerouslySetInnerHTML
              {:__html (md->html docs)}}]]])])

手始めに [:H1 "WELCOME TO LUMINUS-APP"]WELCOME TO LUMINUS-APPHELLO, WORLD と書き換えてみましょう。そうするとブラウザがリフレッシュされずに表示だけが変わったのが確認できたと思います。

次に luminus-app/resources/public/css/screen.css をエディタで開いてみましょう。

html,
body {
    font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
    height: 100%;
    padding-top: 40px;
}

このような CSS が既に書いてあると思うので、ここにもう少しコードを足してみましょう。

h1 {
    animation-duration: 3s;
    animation-name: slidein;
    animation-iteration-count: infinite;
}

h1::before {
    content: "***";
}

h1::after {
    content: "***";
}

@keyframes slidein {
  from {
    margin-left: 100%;
    width: 300%
  }

  to {
    margin-left: -100%;
    width: 100%;
  }
}

そうしたら、 ***Hello, world*** が右から左にスライドし続けるようになりましたね。

../../../_images/slidein-hello.png

うるさいなと思ったらさっきのコード消してください。

というところでここまでが Figwheel の簡単な使い方でした。これまでこういう経験をしたことがなければ恐らくは「おお…」と感嘆の声を画面の前であげたんじゃないでしょうか。

基本的にライブコードリローディングだけであれば lein figwheel とすれば使えます( Luminus テンプレートを使っている場合は別でアプリは起動しておく必要がありますが)。 Figwheel は静的なファイルサーバーとしても動くので Figwheel だけでバックエンドを切り離したフロントエンド開発もできるみたいですが、今回そこまでの話はしません。

また lein figwheel で起動した場合はブラウザ REPL が自動的に起動されるので、ブラウザ環境を参照したりすることもできます。

例えば、このように。

cljs.user=> (println (.-title js/document))
Welcome to luminus-app
nil

Tips: もしライブコードリローディングを使っていてリロードの挙動がおかしいなと思ったときは

Figwheel のライブコードリローディングがうまく使える条件は「リローダブル」なコードであることです。簡単な例だと JavaScript の setInterval などを使った副作用が紛れ込んでいるとうまくリロードされない場合があります(もしくはリロードされたタイミングで副作用が蓄積されてしまう)。

なので、そういうときは Figwheel のリロードを行うエントリポイントを編集しましょう。これは luminus-app/env/dev/cljs/luminus_app/dev.cljs にあります。

(figwheel/watch-and-reload
 :websocket-url "ws://localhost:3449/figwheel-ws"
 :jsload-callback core/mount-components)

この :jsload-callback [1] にセットされている関数がリロードの際に呼び出されるエントリポイントとなっているので、ここにセットする関数を調整するようにしましょう。

[1]本当はこのキーが既に非推奨となっているので :on-jsload を代わりに使うべきです。

もう少し進んだ Figwheel の使い方

ここまで読んできた勘のいい方なら、「 lein repllein figwheel 起動するってふたつターミナル開くとかしないといけないの?それだったら Chestnut テンプレートの方がいいじゃん」と思われたかもしれません。

ここまでの例では、ターミナルをふたつ開いてそれぞれ Clojure REPL 用と Figwheel 用にして使っていましたが、ふたつの環境を行き来するというのはめんどくさいですよね。出来ればサーバーを起動したタイミングで ClojureScript の自動ビルド/リロードを行うようにしたいですよね。

ということでやってみましょう。これまで起動した REPL や Figwheel などがあれば一旦全て閉じておいてください。

まずは luminus-app/env/dev/clj/luminus_app/dev.clj を作成します。 luminus-app/env/dev/clj/luminus_app ディレクトリがないと思うのでこれは自分で作成します。

そして以下のようなコードを書いておきます。

(ns luminus-app.dev
  (:require [clojure.edn :as edn]
            [clojurescript-build.auto :as auto]
            [figwheel-sidecar.auto-builder :as fig-auto]
            [figwheel-sidecar.config :as conf]
            [figwheel-sidecar.core :as fig]))

(defonce fig-server (atom {}))

(defn start-figwheel []
  (let [server (fig/start-server { :css-dirs ["resources/public/css"] })
        builder (fig-auto/autobuild*
                 {:builds [{:id "dev"
                            :source-paths ["src-cljs" "env/dev/cljs"]
                            :compiler {:output-to            "resources/public/js/app.js"
                                       :output-dir           "resources/public/js/out"
                                       :source-map           true
                                       :optimizations        :none
                                       :source-map-timestamp true
                                       :externs ["react/externs/react.js"]}}]
                  :figwheel-server server})]
    (reset! fig-server {:figwheel-server server :cljs-builder builder})))

(defn stop-figwheel []
  (let [server (:figwheel-server @fig-server)
        builder (:cljs-builder @fig-server)]
    (when builder
      (auto/stop-autobuild! builder))
    (when server
      (fig/stop-server server))
    (reset! fig-server {:figwheel-server nil :cljs-builder nil})))

次に project.clj:project/dev:source-paths ["env/dev/clj"] を足します。そうすると :project/dev の部分は以下のようになります。

:project/dev  {:source-paths ["env/dev/clj"] ;; 今回足した部分はココだけ!!
               :dependencies [[ring/ring-mock "0.2.0"]
                              [ring/ring-devel "1.4.0"]
                              [pjstadig/humane-test-output "0.7.0"]
                              [lein-figwheel "0.3.7"]
                              [org.clojure/tools.nrepl "0.2.10"]]
               :plugins [[lein-figwheel "0.3.7"]]
               :cljsbuild
               {:builds
                {:app
                 {:source-paths ["env/dev/cljs"] :compiler {:source-map true}}}}

               :figwheel
               {:resource-paths "resources"
                :http-server-root "public"
                :server-port 3449
                :nrepl-port 7002
                :css-dirs ["resources/public/css"]
                :ring-handler luminus-app.handler/app}

               :repl-options {:init-ns luminus-app.core}
               :injections [(require 'pjstadig.humane-test-output)
                            (pjstadig.humane-test-output/activate!)]
               ;;when :nrepl-port is set the application starts the nREPL server on load
               :env {:dev        true
                     :port       3000
                     :nrepl-port 7000}}

これで env/dev/clj 以下が REPL 起動時に読み込まれるようになりました。

では実際に REPL を起動して確認してみましょう。先ほどと同じように REPL を起動してサーバーまで起動します。

$ lein repl
luminus-app.core=> (start-app [3000])

そのあとに次のフォームを評価します。

luminus-app.core=> (require '[luminus-app.dev :as ld])
nil
luminus-app.core=> (ld/start-figwheel)
Figwheel: Starting server at http://localhost:3449

そしてさっきほどと同様にブラウザで http://localhost:3000/ を開いて、 luminus-app/src-cljs/luminus_app/core.cljs を編集してみてください。修正したタイミングで変更が反映されているようなら成功です。

確認出来たら一旦サーバーを REPL ごと閉じておきましょう。

次にこれをサーバーを起動したタイミングで起動出来るようにします。

luminus-app/src/luminus_app/handler.clj を開いて init 関数を探します。

(defn init
  "init will be called once when
   app is deployed as a servlet on
   an app server such as Tomcat
   put any initialization code here"
  []

  (timbre/merge-config!
    {:level     (if (env :dev) :trace :info)
     :appenders {:rotor (rotor/rotor-appender
                          {:path "luminus_app.log"
                           :max-size (* 512 1024)
                           :backlog 10})}})

  (if (env :dev) (parser/cache-off!))
  (start-nrepl)
  (timbre/info (str
                 "\n-=[luminus-app started successfully"
                 (when (env :dev) " using the development profile")
                 "]=-")))

(if (env :dev) (parser/cache-off!)) の部分を書き換えます。

(when (env :dev)
  (parser/cache-off!)
  (require 'luminus-app.dev)
  ((resolve 'luminus-app.dev/start-figwheel)))

こうすることで (start-app [3000]) とフォームを評価してサーバーを起動した際に自動で Figwheel が立ち上がり自動リロードが行われるようになります。試しに REPL とサーバーを起動した後に ClojureScript のコードを修正すると自動的にリロードされたのが確認出来ると思います。

ということでこれで簡単に Figwheel のライブコードリローディング機能を使えるようになりました。開発前にやることが減ってストレスが減りましたね。

Tips: Figwheel-sidecar プロジェクト

このステップで説明しなかったこととして luminus-app/env/dev/clj/luminus_app/dev.clj に書いたコードがあるんですが、よく読んでいる人はこの中で require した figwheel-sidecar に気付いたと思います。

Figwheel-sidecar プロジェクトを使うことで Leiningen プラグインを直接使わなくても、 Figwheel の機能を使うことが出来るようになります。現在まだ WIP なプロジェクトなのでドキュメント化もされていませんが、うまく使えば Component ベースのプロジェクトなどにも組み込んだり出来て便利です。

Chestnut テンプレートではこのステップで行っていることと同じことをやっています。

最後に

長々と書いてきましたが詰まるところライブコードリローディングは便利ということです。 また今回の最後のステップを適用してもブラウザ REPL には接続出来ないのですが、 piggieback と weasel を自力で組み込めば使えるようになるので自分でやってみてもいいかと思います。また時間があるときにでも解説しようとは思っていますが。

Chestnut のテンプレートが参考になるので以下を参考に組み込んでみてください。