Help us understand the problem. What is going on with this article?

ClojureScript 開発時の project.clj を理解する

More than 3 years have passed since last update.

本記事は Clojure Advent Calendar 2016 4日目の記事です。

はじめに

ClojureScript を使って Web 開発をしたい、と思った時に、プロジェクトの環境設定は面倒くさく悩ましいネタの一つです。Clojure で代表的な Web フレームワークであるluminusduct、あるいは(フレームワークではないかもしれないが) chestnut等だと、めんどくさい設定等を間違えることなく良きにはからってくれる leiningen template が用意され、開発を容易に始めることができます。ですが、自動生成された環境は、時に自分の思い通りと少し違っていたり、オーバースペックゆえにいじりにくかったりする場合があります。

そこで本記事では、今時の ClojureScript を前提とした Web 開発の環境について、一旦シンプルなところから徐々に拡張していく感じで理解を進めていく、ということをしてみたいと思います。

step1. まずは lein-cljsbuild

(Boot派の人ごめんなさい...)まずは ClojureScript でのビルドツールとして leiningen のプラグインである lein-cljsbuildを使うための最小限の環境を考えてみます。

まずは project.clj。REPL から使う前提として必要最小限だとこんな感じでしょうか。

project.clj
;; https://github.com/emezeske/lein-cljsbuild を参考にわりと最小構成で
(defproject step01 "0.1.0-SNAPSHOT"
  :description "step01"
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [org.clojure/clojurescript "1.9.293" :scope "provided"]]
  :plugins [[lein-cljsbuild "1.1.4"]]
  :clean-targets ^{:protect false} [:target-path :compile-path "resources/public/js"]
  :cljsbuild
  {:builds [{:source-paths ["src"]
             :compiler
             {:main "step01.core"
              :asset-path "js"
              :output-to "resources/public/js/main.js"
              :output-dir "resources/public/js"
              :source-map true
              :optimizations :none
              :pretty-print true}}]})

ディレクトリ構成は以下のような感じで、index.htmlcore.cljsのみ事前に作成しておきます。

    .
    ├── project.clj
    ├── resources
    │   └── public
    │       └── index.html
    │
    └── src
        └── step01
            └── core.cljs

index.htmlcore.cljs、は最小構成だと以下のような感じです。

resources/public/index.html
<!DOCTYPE html>
<html>
    <head lang="en">
        <meta charset="UTF-8">
    </head>
    <body>
        <script src="js/main.js"></script>
    </body>
</html>
src/step01/core.cljs
(ns step01.core)

(enable-console-print!)

(defn init
  []
  (.log js/console "Hello, world"))

(init)

これをビルドしてみます。コマンドは lein cljsbuild once。すると、resources/public/js 以下に JavaScript のコードが生成され、ブラウザで index.html を開くと、デバッガのコンソール上に Hello, world と出力されます。

コンパイル結果を確認するため resources/public/js 以下を見てみると、main.jsの他にフォルダやらファイルやらが生成されているのが確認できます。

bash$ cd resources/public/js
bash$ ls
cljs/         cljs_deps.js  goog/         main.js       step01/
bash$

実際のところ、main.jsは「それらを include するためのファイル」という形で生成されます。

resources/public/js/main.js
var CLOSURE_UNCOMPILED_DEFINES = null;
if(typeof goog == "undefined") document.write('<script src="js/goog/base.js"></script>');
document.write('<script src="js/cljs_deps.js"></script>');
document.write('<script>if (typeof goog == "undefined") console.warn("ClojureScript could not load :main, did you forget to specify :asset-path?");</script>');
document.write('<script>goog.require("step01.core");</script>');

上記 main.jsindex.html から参照されたとき、まず js/goog/base.js その他もろもろを動的に include していることがわかります。これは :main オプションを指定している場合であり、 :main オプションを指定しなければ、コンパイル時点で出力先の JavaScript (main.js) に全てのコードを突っ込む、という動作をします。

また、上記 main.js の中に見られる js/ で始まるパスは、:asset-path の指定した値になっています。:asset-path が未指定の場合はフルパスになるようです(よって :main オプションを指定するパターンの場合は :asset-path は必須)。

以下にオプションを簡単に記載しておきます。もっと詳しい例は lein-cljsbuild の sample.project.clj を見ると良いです。

option 説明
:source-paths 指定ディレクトリ以下にある ClojureScript ファイルをコンパイルします。
:main 最初にコンパイルするべき namespace を指定します(コンパイルの起点っぽい)
:asset-path 上記参照。
:output-to 出力ファイル(JavaScriptファイル名)を指定します。
:output-dir コンパイル時の temporary file 群の出力先ディレクトリを指定します。ここでは include するべき JavaScript を正しく配置するために resources 以下に指定しています。
:source-map source-map を出力するか否かを指定します。
:optimizations :none、:whitespace、:simple、:advanced、のいずれかを指定します。ブラウザのJavaScript デバッガでデバッグする際は :none、リリース向けには :advanced を指定しますが、:advanced 指定時にはいくつか他に設定が必要です(後述)。
:pretty-print コンパイルした JavaScript を pretty print するかどうかを指定します。ブラウザのJavaScript デバッガでデバッグする際は true、それ以外は false で良いと思います。

step2. リリース向け設定

step1.では、JavaScript デバッグのために必要なファイルを出力することを意図していましたが、本番向けとしては圧縮できるものは可能な限り圧縮したいし、ファイルのデバッグ情報も必要ありません。そこで、cljsbuild の設定を変更します。

project.clj
(defproject step02 "0.1.0-SNAPSHOT"
  :description "step02"
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [org.clojure/clojurescript "1.9.293" :scope "provided"]]
  :plugins [[lein-cljsbuild "1.1.4"]]
  :clean-targets ^{:protect false} [:target-path :compile-path "resources/public/js"]
  :cljsbuild
  {:builds [{:source-paths ["src"]
             :compiler
             {:output-to "resources/public/js/main.js"
              :optimizations :advanced
              :pretty-print false}}]})

だいぶシンプルになりました。step1. との違いを書いておきます。

option 説明
:main 不要なので削除
:asset-path 不要なので削除
:output-dir 削除(デフォルトは target 以下に作られる)
:source-map 不要なので削除
:optimizations :none から :advanced に変更
:pretty-print true から false に変更

step3. profileを分ける

step1、step2、では、project.clj そのものをデバッグ用/リリース用と分けていましたが、実際のところ1つの project.clj でデバッグ向け/リリース向けに切り替える必要があります。step3 ではこれを合体させることを考えます。

project.clj
(defproject step03 "0.1.0-SNAPSHOT"
  :description "step03"
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [org.clojure/clojurescript "1.9.293" :scope "provided"]]
  :plugins [[lein-cljsbuild "1.1.4"]
            [lein-environ "1.1.0"]]
  :resource-paths ["resources" "target/cljsbuild"]
  :target-path "target/%s/"
  :clean-targets ^{:protect false} [:target-path :compile-path "resources/public/js"]
  :profiles
  {:dev [:project/dev :profiles/dev]
   :uberjar {:omit-source true
             :prep-tasks ["compile" ["cljsbuild" "once" "min"]]
             :cljsbuild
             {:builds
              {:min
               {:source-paths ["src"]
                :compiler
                {:output-to "resources/public/js/main.js"
                 :optimizations :advanced
                 :pretty-print false}}}}}
   :project/dev {:cljsbuild
                 {:builds
                  {:dev
                   {:source-paths ["src"]
                    :compiler
                    {:main "step03.core"
                     :asset-path "js"
                     :output-to "resources/public/js/main.js"
                     :output-dir "resources/public/js"
                     :source-map true
                     :optimizations :none
                     :pretty-print true}}}}}
   :profiles/dev {}})

切り替えはざっくり profile で分けることにします。ここではデバッグ向けは :dev、リリース向けは :uberjar としています(他にテスト向け :test も考慮すべきですが簡単にするためここでは省略)。

上記設定では :dev をさらに :project/dev:profiles/dev に分けています。ここらへんの設定は luminus の設定のまんまです。各プロファイルの :cljsbuildの設定は step1、step2、のそのままにしています。

これでテストしてみます。まずはデフォルトの :dev プロファイル。

bash$ lein clean
bash$ lein cljsbuild once
Compiling ClojureScript...
Compiling "resources/public/js/main.js" from ["src"]...
Successfully compiled "resources/public/js/main.js" in 1.196 seconds.
bash$ tree
.
├── project.clj
├── resources
│   └── public
│       ├── index.html
│       └── js
│           ├── cljs
│           │   ├── core.cljs
│           │   ├── core.js
│           │   └── core.js.map
│           ├── cljs_deps.js
│           ├── goog
│           │   ├── array
│           │   │   └── array.js
:            :
:            :  (中略)
:            :
│           ├── main.js
│           └── step03
│               ├── core.cljs
│               ├── core.cljs.cache.edn
│               ├── core.js
│               └── core.js.map
├── src
│   └── step03
│       └── core.cljs
└── target
:            :
:            :  (以下略)

26 directories, 26 files
bash$

次に :uberjar プロファイルで cljsbuild してみます。lein with-profile <profile> でプロファイルを指定して leiningen task を実行します。

bash$ lein clean
bash$ lein with-profile uberjar cljsbuild once
Compiling ClojureScript...
Compiling ClojureScript...
Compiling "resources/public/js/main.js" from ["src"]...
Successfully compiled "resources/public/js/main.js" in 19.953 seconds.
bash$ tree
.
├── project.clj
├── resources
│   └── public
│       ├── index.html
│       └── js
│           └── main.js
├── src
│   └── step03
│       └── core.cljs
└── target
:            :
:            :  (以下略)

18 directories, 10 files
bash$

あと、:uberjar プロファイルの :prep-task を指定しているので lein uberjar するタイミングで cljsbuild が走ります。

bash$ lein uberjar
Compiling ClojureScript...
Compiling "resources/public/js/main.js" from ["src"]...
Successfully compiled "resources/public/js/main.js" in 19.988 seconds.
Created /.../step03/target/uberjar/step03-0.1.0-SNAPSHOT.jar
Created /.../step03/target/uberjar/step03-0.1.0-SNAPSHOT-standalone.jar
bash$

この段階ではサーバーサイドのコードがないので uberjar しても意味はなさそうですが...。

まとめ

本当は、サーバーサイドも含めたプロジェクト構成の設計(ClojureScript の REPL 環境を提供するfigwheelや、テスト用ツール dooを組み込んだ状態)まで進みたかったのですが、基礎的なことに集中して記事にすることにしました。

(※本当の本当は om.next の remotingについて作業中だったのですが間に合わず...でした。せっかくなので project.cljへのリンク だけでも晒しておきます。)

この記事が、ClojureScript での開発をされるどなたかのお役に立てることができれば幸いです。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away