Part4: テンプレートエンジンを使う

Clojure におけるテンプレートエンジン

今までのコードでは HTML を文字列で直接書いてきました。「まさか Clojure では文字列で直接 HTML を書くの?」と心配された方もいるかもしれませんが、そんなことはないので安心してください。他の言語でもあるようにテンプレートエンジンがちゃんとあるので、これからはそれを使っていきます。さて、テンプレートエンジンと一口に言っても色々なものが存在します。とは言えよく名前のあがるものはそんなに多くはないです。 Enlive, Selmer, Hiccup これら 3 つの存在は覚えておいていいかと思います。それぞれ簡単に特徴を説明すると Enlive はセレクタベースのテンプレートエンジンであまり他の言語でも見ない特殊な形のテンプレートエンジンですが、 HTML を読み込み CSS セレクタで特定の部分に Clojure で処理を加え HTML を吐き出すという面白いものです。 Selmar は Django のテンプレート機能と似たようなもので、 HTML ファイルに直接ロジックを書くタイプのものです。最後に Hiccup ですが、これは Clojure のベクタをそのまま HTML へと変換するものです。

さて、どれも一長一短なので一概にどれがいいというのを簡単に決めることは出来ないのですが、今回は学習コストが一番低く(私の勝手な印象ですが) Clojure そのものと親和性が高くて HTML を直接書く必要がない Hiccup を使っていきたいと思います。

Hiccup を導入する

早速 Hiccup をプロジェクトの依存性へと追加しましょう。 project.clj へ次のように足します。

:dependencies [[org.clojure/clojure "1.7.0"]
               [ring "1.4.0"]
               [compojure "1.4.0"]
               [hiccup "1.0.5"]
               [environ "1.0.1"]]

追加したら、早速いつも通り REPL を再起動して以下のフォームを評価してみましょう。

user> (require '[hiccup.core :as hc])
;; => nil
user> (hc/html [:div [:h1 "Hello"]])
;; => "<div><h1>Hello</h1></div>"

hiccup.corehtml マクロが主に使う機会が多いものになるとは思いますが、このようにベクターデータ(以下、タグベクター)を渡すことで HTML へと変換し吐き出してくれます。この Hiccup のタグベクターは次のような構造になっています。

[tag & body]
[tag attributes & body]

タグベクターのひとつめは必ず HTML のタグ名がきますが、これはキーワード、シンボル、文字列のどれでも使うことが出来るようになっています。またタグの属性をマップデータとしてふたつめに渡すことが出来ますがこれは省略可能です。属性を除いたタグベクターの残りは全てタグのボディとして扱われますが、これは文字列や他のタグベクターを含めることが可能です。

user> (hc/html [:h1 {:class "black-bold"} "Hello " [:em "world"]])
;; => "<h1 class=\"black-bold\">Hello <em>world</em></h1>"

また、 Hiccup は idclass 属性に対して CSS スタイルのシンタックスシュガーを用意しているため、マップデータを使わずに次のように表すことが出来ます。

user> (hc/html [:p#headline.red.bold "Welcome to Clojure"])
;; => "<p class=\"red bold\" id=\"headline\">Welcome to Clojure</p>"

CSS と同じように # の後が ID 、 各 . の後がクラスになります。ただし、 :div.foo.bar#baz のような # が後ろにくるような書き方は出来ないので、必ず # はひとつめに持ってくる必要があります。

さらに、タグベクターのボディではシーケンスが展開されるようになっているため次のようなコードが書けます。

user> (hc/html [:ul
                (for [x (range 4)]
                  [:li x])])
;; => "<ul><li>0</li><li>1</li><li>2</li><li>3</li></ul>"

ただし、展開されるのはシーケンスだけでベクターやセットなどのデータ型は展開されないので注意が必要です。

ここまででなんとなく Hiccup の雰囲気が掴めたと思います。また他にも関数が幾つかありますが、出てきたタイミングでそれぞれ説明したいと思います。

文字列で書いていた HTML を Hiccup で書きなおしてみる

今まで HTML を文字列で書いていたのは次のふたつの関数でした。

(defn home-view [req]
  "<h1>ホーム画面</h1>
   <a href=\"/todo\">TODO 一覧</a>")

(defn todo-index-view [req]
  `("<h1>TODO 一覧</h1>"
    "<ul>"
    ~@(for [{:keys [title]} todo-list]
        (str "<li>" title "</li>"))
    "</ul>"))

これを Hiccup で書き直します。それから本格的に画面を作りこんでいくので、ネームスペースもついでに新しく作りましょう。 src/todo_clj/view/main.cljsrc/todo_clj/view/todo.clj を作成します。まずは src/todo_clj/view/main.clj にホーム画面を表示する関数を書いていきます。

;; src/todo_clj/view/main.clj
(ns todo-clj.view.main
  (:require [hiccup.core :as hc]))

(defn home-view [req]
  (-> (list
       [:h1 "ホーム画面"]
       [:a {:href "/todo"} "TODO 一覧"])
      hc/html))

何をしているか簡単に分かるようになったと思います。ちなみにここではリストを hiccup.core/html へと渡していますが、タグベクターのボディでなくても展開できるのでこのように書くことが可能です。

次は src/todo_clj/view/todo.clj を書いてみます。

;; src/todo_clj/view/todo.clj
(ns todo-clj.view.todo
  (:require [hiccup.core :as hc]))

(defn todo-index-view [req todo-list]
  (-> `([:h1 "TODO 一覧"]
        [:ul
         ~@(for [{:keys [title]} todo-list]
             [:li title])])
      hc/html))

todo-list はハンドラーから呼び出されるときに受け取るようにしました。こちらも前より読みやすくなったんじゃないんでしょうか。またふたつの関数のネームスペースを変える際に、これらを呼び出している方も少々書き換えています。

;; src/todo_clj/handler/main.clj
(ns todo-clj.handler.main
  (:require [compojure.core :refer [defroutes GET]]
            [compojure.route :as route]
            [todo-clj.util.response :as res]
            [todo-clj.view.main :as view]))

(defn home [req]
  (-> (view/home-view req)
      res/response
      res/html))
;; src/todo_clj/handler/todo.clj
(ns todo-clj.handler.todo
  (:require [compojure.core :refer [defroutes context GET POST]]
            [todo-clj.util.response :as res]
            [todo-clj.view.todo :as view]))

(defn todo-index [req]
  (-> (view/todo-index-view req todo-list)
      res/response
      res/html))

サーバーを起動して、画面をリロードしたら今までと変わらない画面が表示出来ているかを確認し、出来ていたらここまでは大丈夫です。

さらに Web アプリケーションらしくなるように装飾していく

今まで素っ気ない画面だったのでこの辺で少々手の込んだ画面を作っていくことにしましょう。まずは全体での統一感を出すためにページのレイアウトを作っていきます。 todo-clj.view.layout というネームスペースを新たに作成して、そこに全ての画面で共通して使えるレイアウトを定義します。

;; src/todo_clj/view/layout.clj
(ns todo-clj.view.layout
  (:require [hiccup.page :refer [html5 include-css include-js]]))

(defn common [req & body]
  (html5
   [:head
    [:title "TODO-clj"]
    (include-css "/css/normalize.css"
                 "/css/papier-1.3.1.min.css"
                 "/css/style.css")
    (include-js "/js/main.js")]
   [:body
    [:header.top-bar.bg-green.depth-3 "TODO-clj"]
    [:main body]]))

common という全ての画面で共通となるレイアウトを定義する関数を作りました。ひとつめにリクエストマップを受け取り、残りは全てボディとして受け取り HTML の内部でそのまま展開されます。ちなみに今まで特に説明もなくビューに関連する関数の第一引数にリクエストマップを指定して、今のところ使っていないので本当に必要なのか気になっている方もいるかもしれませんが、後々使う予定なので今はとりあえず書いてあると思ってもらえればいいです。

それから include-css, include-js 関数で幾つか定義した覚えのないファイル名が出てきていますが、 normalize.csspapier-1.3.1.min.css は以下の URI からダウンロードして、 resources/public/css ディレクトリ以下に配置しておいてください。

また同様に resources/public/css/style.css を作成して以下の記述をします。

header.top-bar {
    padding: 5px;
}

resources/public/js/main.js は空のファイルを置いておくだけで今回はいいです。後々中身を書いていきます。

さて hiccup.page というネームスペースが新たに出てきました。 hiccup.page ネームスペースは HTML のページを素早く構築するための関数を提供するネームスペースで、 html5 以外にも xhtml, html4 などというマクロがあり、それに加え CSS と JavaScript 用に link タグと script タグのヘルパー関数がそれぞれ用意されています( include-css, include-js )。そして、このネームスペースが提供する html5 マクロは内部で hiccup.core/html マクロを呼び出すため、明示的に hiccup.core/html を使用する必要がありません。

user> (require '[hiccup.page :as hp])
;; => nil
user> (hp/html5 [:p "Hello"])
;; => "<!DOCTYPE html>\n<html><p>Hello</p></html>"

このように html5 マクロは hiccup.core/html マクロと同じように使うことができます。今回は HTML5 で良いので html5 マクロを使っています。

この定義したレイアウトを次のようにホーム画面へと適用してみます。

;; src/todo_clj/view/main.clj
(ns todo-clj.view.main
  (:require [todo-clj.view.layout :as layout]))

(defn home-view [req]
  (->> [:section.card
        [:h2 "ホーム画面"] ;; ちょっと H1 タグだとうるさいので小さくしました
        [:a {:href "/todo"} "TODO 一覧"]]
       (layout/common req)))

ここで一度、画面をリロードしてみましょう。どうでしょう、今までと何か変わった気がしますか?レイアウトで追加したヘッダーが新しく表示されてますが、 CSS が適用されていない気がしますよね( [:header.top-bar.bg-green.depth-3 "TODO-clj"] と書いてあるのでヘッダーの TODO-clj が緑色の背景になって少々影が付くのを期待しています)。

この原因は resources 配下のファイルに対してリクエストを処理できていないためです。例えば画面を表示したときに http://localhost:3000/css/normalize.css というリクエストが投げられるんですが、それを解決する方法をサーバーが知らないので CSS ファイルを取得出来ずに読み込めないという状態になっています。これを解決するためにミドルウェアをひとつ追加しましょう。

Ring ライブラリの中に最初からあるミドルウェアを使います。この問題に対応できそうなものに ring.middleware.filering.middleware.resource というミドルウェアがあるんですが、 ring.middleware.resource を使うことにします。 ring.middleware.file は jar や war にしたときにその内部にあるファイルに対してアクセスすることが出来ないので、あまり利用する意味がありません。 ring.middleware.resource ミドルウェアを次のように追加してみます。

;; src/todo_clj/core.clj
(ns todo-clj.core
  (:require [compojure.core :refer [routes]]
            [environ.core :refer [env]]
            [ring.adapter.jetty :as server]
            [ring.middleware.resource :as resource]
            [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 wrap-dev (:dev env))
      (wrap resource/wrap-resource "public"))) ;; 足しました

ring.middleware.resource/wrap-resource ミドルウェアは他のミドルウェアと同様に第一引数としてハンドラーを受け取り、第二引数にリソースを解決する際のルートパスを受け取ります。ここでは第二引数として "public" を渡すことで resources/public がリソースを解決するときのルートになるようにしました。

注釈

resources ディレクトリはデフォルトで Leiningen プロジェクトのリソースディレクトリになっているため、 public を指定すると resources/public をルートパスにするということになるんですが、リソースディレクトリ自体をデフォルトから変えたい場合は project.clj:resource-paths を指定すれば変更することが出来ます。

さて、ミドルウェアを足したらブラウザをリロードしてみましょう。緑色のヘッダーが見えるようになったと思います。レイアウトがちゃんと適用されるようになったところで TODO 一覧にも同じようにレイアウトを適用してみます。

;; src/todo_clj/view/todo.clj
(ns todo-clj.view.todo
  (:require [todo-clj.view.layout :as layout]))

(defn todo-index-view [req todo-list]
  (->> `([:h1 "TODO 一覧"]
         [:ul
          ~@(for [{:keys [title]} todo-list]
              [:li title])])
       (layout/common req)))

TODO 一覧を表示するとホーム画面同様に緑色のヘッダーが表示されるようになりました。これでようやく Web アプリケーションらしさが出てきました。

次の Part では実際にデータベースを使って現実の Web アプリケーションにより近いものを作っていくことにしましょう。

ここまでで学んだこと

  • Clojure で使えるテンプレートエンジンは幾つかある
  • Hiccup は Clojure のデータ構造をそのまま HTML へ変換できる
  • Hiccup を使った共通的なレイアウトの適用方法