Arantium Maestum

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

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