Part3.5: 開発用のミドルウェアを追加する

リロード、スタックトレース

… HTML の説明に行く前にちょっと小休止して開発用のミドルウェアについて説明します。

Part3 で一気にファイルが増えてしまい編集する度にファイルをロードしたりするのがそろそろ大変になってきました。どんなに REPL 駆動開発が出来て便利だとは言えどもファイルを編集して保存したらロードする必要があるというのは少々面倒ですよね。なのでこの問題を Ring ミドルウェアを追加することで解決したいと思います。またエラーが起きたときに分かり難いのでもうちょっと親切なエラー画面を出すようにします。

ファイルを編集して保存した際に自動的にロードしてくれるミドルウェアは ring-devel に入っていて、それは既に ring 自体の依存性に入っているため既に使える状態となっています。親切なエラー画面を出すためのミドルウェアは新しいライブラリを追加して対応しましょう。 prone を使います。 prone は Rails の better_errors に似たような画面を出力してくれます。

project.clj に次の依存性を dev プロファイルへと追加します。

(defproject todo-clj "0.1.0-SNAPSHOT"

  ;; 中略

  :profiles
  {:dev {:dependencies [[prone "0.8.2"]]}})

次に todo-clj.middleware ネームスペースを作成して、そこに今回足す開発用ミドルウェアを wrap-dev という関数にまとめたいと思います。

;; src/todo_clj/middleware.clj
(ns todo-clj.middleware)

(defn- try-resolve [sym]
  (try
    (require (symbol (namespace sym)))
    (resolve sym)
    (catch java.io.FileNotFoundException _)
    (catch RuntimeException _)))

(defn wrap-dev [handler]
  {:pre [(or (fn? handler) (and (var? handler) (fn? (deref handler))))]}
  (let [wrap-exceptions (try-resolve 'prone.middleware/wrap-exceptions)
        wrap-reload (try-resolve 'ring.middleware.reload/wrap-reload)]
    (if (and wrap-reload wrap-exceptions)
      (-> handler
          wrap-exceptions
          wrap-reload)
      (throw (RuntimeException. "Middleware requires ring/ring-devel and prone;")))))

いつもと様子が違いますね。 ns マクロの中では特定のネームスペースを require していません。これはこのミドルウェア (wrap-dev) が使われるときだけ、開発用のミドルウェアを動的に require したいからです。動的に require するための関数は try-resolve として定義してあります。このようにしておけば dev プロファイルの依存性へ追加する prone のようなものを扱いやすくなります。

注釈

dev プロファイルに追加している依存性は lein uberjar などするときには参照されなくなるので注意が必要。

次に todo-clj.core/app にこのミドルウェアを適用しましょう。

;; src/todo_clj/core.clj
(ns todo-clj.core
  (:require [compojure.core :refer [routes]]
            [ring.adapter.jetty :as server]
            [todo-clj.handler.main :refer [main-routes]]
            [todo-clj.handler.todo :refer [todo-routes]]
            [todo-clj.middleware :refer [wrap-dev]])) ;; 追加

(def app
  (-> (routes
       todo-routes
       main-routes)
      wrap-dev))

Part3 で説明したように routes 関数はハンドラーを返すので、このようにミドルウェアを適用することが出来ます。しかし、これでは開発環境とプロダクション環境の切り分けが出来ていないため、折角 wrap-dev が動的に require するようになっているのにいつでも読み込まれるようになってしまい意味がありません。なのでここでまたもうひとつライブラリを追加します。

environ というライブラリを追加しましょう。これは環境変数や Java のシステムプロパティを同じようにアクセス出来るようにまとめてくれる便利なものです。

(defproject todo-clj "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.7.0"]
                 [ring "1.4.0"]
                 [compojure "1.4.0"]
                 [environ "1.0.1"]]
  :plugins [[lein-environ "1.0.1"]]
  :profiles
  {:dev {:dependencies [[prone "0.8.2"]]
         :env {:dev true}}})

project.clj はこのようになりました。 environ を使って project.clj の dev プロファイルを参照したい場合は、依存性だけでなく Leiningen プラグインを追加する必要があります。さらに dev プロファイルの :env に対してマップを与えておくとプログラム中からこの値を参照することが出来ます。実際にここまで書いたら一度 REPL を再起動して、 REPL 上で次のようなフォームを評価してみましょう。

user> (require '[environ.core :refer [env]])
nil
user> (env :dev)
true
user> (env :java-home)
"/usr/lib/jvm/java-8-oracle/jre"

このように environ.core ネームスペースにある env は環境変数などが詰まったマップデータなので通常のマップと同じように扱うことが出来ます。これを実際に使って先ほどのミドルウェア適用部分を書き換えると次のようになります。

;; src/todo_clj/core.clj
(ns todo-clj.core
  (:require [compojure.core :refer [routes]]
            [ring.adapter.jetty :as server]
            [todo-clj.handler.main :refer [main-routes]]
            [todo-clj.handler.todo :refer [todo-routes]]
            [todo-clj.middleware :refer [wrap-dev]]
            [environ.core :refer [env]]))

(defn- wrap [handler middleware opt]
  (if (true? opt)
    (middleware handler)
    (if opt
      (middleware handler opt)
      handler)))

(def app
  (-> (routes
       todo-routes
       main-routes)
      (wrap wrap-dev (:dev env))))

wrap というヘルパー関数を追加しました。第一引数にハンドラー、第二引数にミドルウェア、第三引数にオプションを受け取ります。第三引数によってこの関数の挙動は変わりますが、 truthy ではなく厳密に true であるならミドルウェアを単純にハンドラーへと適用し、 true ではないがオプションがある場合はミドルウェアの第二引数としてオプションを渡しそれ以外の場合 falsy な値ならハンドラーをそのまま返します。このように書くことによって、設定をベースとしてどのミドルウェアを使うかというのが簡単に切り替えることが出来るので、開発時は使いたいけどプロダクションでは外したいという要望にも答えることができます。ちなみに prone は開発時のみに適用されるようしないといけないので、このように書くべきですね。

ここまで出来たらサーバーを起動して、次のことを確認してみます。

  1. コードを書き換えたら自動的に変更がリロードされているか
  2. 例外を投げたら prone の画面が出てくるか

ひとつめは todo-clj.handler.main/home-view 関数や todo-clj.handler.todo/todo-list を書き換えて変更を保存した後に、ブラウザをリロードして確かめると分かりやすいでしょう。ふたつめは todo-clj.handler.main/home 関数を次のように書き換えると簡単に確かめることが出来ます。

;; src/todo_clj/handler/main.clj
(defn home [req]
  (throw (Exception. "Test Exception!!"))
  #_(-> (home-view req)
        res/response
        res/html))

こうしてホーム画面を見るとエラー画面が綺麗に表示されていると思います。いい感じですね。この prone を確認する変更等はコミットしないようにして元に戻しておきましょう。

このようにミドルウェアを足すことで任意の機能を追加することができることが分かりました。とは言え今回紹介したのは開発用のミドルウェアで、実際にプロジェクト内で使うミドルウェアは多岐に渡りますが、それはまた後で必要になったら登場してきます。改めて次は HTML の話に移りたいと思います。

ここまでで学んだこと

  • 開発用 Ring ミドルウェアの足し方