LoginSignup
18
18

More than 5 years have passed since last update.

Clojure で Web アプリケーションを作るときに Compojure の代わりに bidi を使う

Posted at

Clojure には Compojure という Rails とか Sinatra を使っていた/使ったことがある人にとっては比較的馴染みやすい DSL を提供してくれる ルーティングライブラリ が存在します 1 。これは比較的簡単に導入出来るのと、日本語の書籍/情報等でも Compojure ばかり紹介されているので、かなりの人がその存在を知っていることでしょう。

さて、 Compojure 悪くないと思うのですが Web アプリケーションを作る上でかなりしんどいことがあります。具体的には Compojure を使って Web アプリケーションを書くときには、 HTML の中に埋め込む URI を全てハードコーディングしなければなりません。 Rails など他のフレームワークに慣れている方にとっては悪夢ですよね。 wtf... と思わず口から出てくるかもしれません。またマクロで記述するため、ルーティングを何かしらのデータから生成するなどということも難しいです。

というところでもう少し便利に使えるルーティングライブラリを紹介します。

bidi

簡単に説明すると Clojure 界隈では有名な JUXT という会社があるのですが、そこが作っているルーティングライブラリです。
これの特徴は URI のディスパッチと形成だけをちゃんとやるように作られていて、かつ Compojure のような DSL を使わずにルーティングをデータとして定義出来ることです。さらに言うと単純なデータなので、 Reader Conditionals を使えば簡単に ClojureScript でも同じデータが使えますし、 bidi は ClojureScript からも使えるように作られているので簡単にサーバー/クライアントサイド両方から使用することが出来ます。

興味がある人は bidi の README をじっくり読んでみてください。今回は実際に bidi を使って簡単に Web アプリケーション的なものを作ってみましょう。

$ lein new bidi-test

さくっとひな形を作成して project.clj を次のように修正します。

(defproject bidi-test "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"]
                 [bidi "1.21.1"] ;; ルーティングライブラリ!!
                 [hiccup "1.0.5"]])

ring と bidi それから hiccup を足しました。 hiccup は HTML を吐く DSL を提供してくれるライブラリです(とはいえ Clojure のベクターデータを HTML に変換するというのが正しいような気もしますが)。

まずは本題のルーティングを定義しましょう。以下のように定義出来ます。

;; src/bidi_test/routes.clj
(ns bidi-test.routes)

(def main
  ["/" {"users" :user-index
        ["users/" [#"\d+" :id]] :user-show}])

簡単ですね。詳しいルーティングの定義方法等は README に譲るとして、簡単に説明すると URI が "/users" であれば :user-index を返却し、 "/users/:id" な URI であれば :user-show を返却します。

具体的には bidi でこういう風に使えます。

user> (require '[bidi.bidi :as bidi])
nil
user> (require '[bidi-test.routes :as routes])
nil
user> (bidi/match-route routes/main "/users")
{:handler :user-index}
user> (bidi/match-route routes/main "/users/1")
{:route-params {:id "1"}, :handler :user-show}
user> (bidi/path-for routes/main :user-index)
"/users"
user> (bidi/path-for routes/main :user-show :id 1)
"/users/1"

ルーティングの定義方法が分かったところで次に進みましょう。

;; src/bidi_test/core.clj
(ns bidi-test.core
  (:require [bidi.bidi :as bidi]
            [bidi.ring :as br]
            [bidi-test.handlers]
            [bidi-test.routes :as routes]
            [bidi-test.utils :refer [ring-handlers]]
            [ring.adapter.jetty :as server]))

(defonce server (atom nil))

(defn match-handler [k]
  (->> (ring-handlers)
       (filter #(= (:ring-handler (meta %))
                   k))
       first))

(extend-protocol br/Ring
  clojure.lang.Keyword
  (request [k req _]
    (let [handler (match-handler k)]
      (handler req))))

(def app
  (br/make-handler routes/main))

(defn start-server []
  (when-not @server
    (reset! server (server/run-jetty #'app {:port 3000 :join? false}))))

(defn stop-server []
  (when @server
    (.stop @server)
    (reset! server nil)))

よくある素朴なサーバーを起動/停止する関数と幾つか見慣れないものがありますね。ちょっと説明しましょう。
まず app は Ring ハンドラーです。 bidi.ring/make-handler 関数に先ほど定義しておいたルーティング定義を渡すことで、リクエストの URI にマッチするハンドラーを実行してくれるようになります。ただし、今回ルーティングの定義をした際に URI に対してキーワードを紐付けるようにしています。なのでキーワードを元にハンドラー関数を探して呼び出す部分を実装してやる必要があります。それが extend-protocol br/Ring 部分ですね。ここで clojure.lang.Keyword を拡張し、 request 関数(これは make-handler 内部で呼び出される関数)が呼び出されたときに対応出来るようにします。 match-handler 関数は Ring ハンドラーのリストからキーワードと一致するメタ情報を持った関数を探します。 Ring ハンドラーのリストを返す関数の定義は別のところで行っていて次のようになります。

;; src/bidi_test/utils.clj
(ns bidi-test.utils)

(defn ring-handler? [var]
  (contains? (meta var) :ring-handler))

(defn ring-handlers []
  (->> (all-ns)
       (mapcat ns-interns)
       (map second)
       (filter ring-handler?)))

書いてある通りですが、全てのネームスペースに定義されている Var を持ってきて、特定のメタ情報を保持しているかを見ています。

最後に実際のハンドラーの定義は次のようになります。

;; src/bidi_test/handlers.clj
(ns bidi-test.handlers
  (:require [bidi.bidi :as bidi]
            [bidi-test.routes :as routes]
            [hiccup.core :as html]
            [ring.util.response :as res]))

(def users
  [{:id 1 :name "ayato_p"}
   {:id 2 :name "alea12"}
   {:id 3 :name "zer0_u"}
   {:id 4 :name "gaaamii"}
   {:id 5 :name "karur4n"}])

(defn ^{:ring-handler :user-index} user-index [req]
  (-> [:div
       [:ul
        (for [{:keys [id name]} users]
          [:li [:a {:href (bidi/path-for routes/main :user-show :id id)} name]])]]
      html/html
      res/response
      (res/content-type "text/html; charset=utf-8")))

(defn ^{:ring-handler :user-show} user-show [req]
  (let [id (Long/parseLong (get-in req [:params :id]))
        user (first (filter #(= id (:id %)) users))]
    (-> [:h1 (:name user) "!"]
        html/html
        res/response
        (res/content-type "text/html; charset=utf-8"))))

^{:ring-handler :user-index} というような感じでメタ情報をくっつけています。勿論これは冗長なので Ring ハンドラーを定義するためのマクロを書いても良いと思います。
そして実際に先ほど説明したように (bidi/path-for routes/main :user-show :id id) このように関数を呼び出すことで実際に必要となる URI を得ることが出来ます。ハードコーディングで URI 書いて str で結合とか嫌ですよね :)

実際にこのコードは REPL 上で (require 'bidi-test.core) (bidi-test.core/start-server) とすれば動きますので是非手元で動かしてみてください。

最後に

だーっと書いたので説明が手抜きで分かり難いと思いますが、 Compojure 以外にもイカしたルーティングライブラリがあるんだよ!!というのが伝わればいいかなーと思います。また今回はハンドラー関数をメタ情報から探しましたが適当にキーワードと関数を紐付けるマップデータを作っても良いと思います。

あ、 Compojure は Web フレームワークではなくて ルーティングライブラリ です 2


  1. "Compojure is really just a routing library, not a framework." 

  2. bidi の README にもありますが RouteOne や Pedestal などといったルーティングライブラリが沢山存在するので好みのものを探してみてもいいと思います。 

18
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
18