読者です 読者をやめる 読者になる 読者になる

Arantium Maestum

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

Clojure Web Development勉強 - Ring (その4) モジュール化とGET/POST Form

Clojure Ring Web

次回はルーティングだと言ったな、あれは嘘だ。

その前に、まず今まで書いたコードを

  • handler.clj (responseを実際に返す関数群)
  • middleware.clj (handlerをラップしてログを取ったりする機能を実装する関数群)
  • core.clj (それ以外。main関数っぽいのも含めて)

に分割してみた。

そしてGET/POST formを使ってクライアントからサーバ側により自由な形式で情報を送れるようにしてみた。

そのコードがこれ:

gist.github.com

分割したので、何かのファイルを更新するたびに(require simple-modular-ring.core :reload-all)としてREPLに全部のdependencyをリロードさせる必要が生じている。vim fireplaceだとcore.cljのファイルから:Require!でやってくれるので便利。

コードの説明をすると、まずmiddlewareで

(ns simple-modular-ring.middleware
  (require
    [ring.middleware.params :as mparams]))

(def wrap-params 
  mparams/wrap-params)

ringが提供する便利middlewareの一つであるwrap-paramsを使っている。paramsとはGETやPOSTメソッドでクライアントから渡されるurl以外の情報(例えばフォーム入力)を集めたもの。wrap-paramsを使わないと、特にPOSTメソッドの場合その情報がリクエストマップから失われてしまう。ここら辺の事情についてはringの公式ドキュメントが詳しい。

そして今回は結構リクエストマップを調べるので、リクエスト・レスポンスを@log-atomに入れるだけじゃなくてreplに直接pprintしてくれるmiddlewareを作った。

(defn req-res-displayer [handler]
  (fn [req]
    (let [res (handler req)]
      (println "\nRequest:")
      (clojure.pprint/pprint req)
      (println "\nResponse:")
      (clojure.pprint/pprint res)
      res)))

pprintした後、ちゃんとレスポンスを返り値として書いておくのを忘れないように・・・

さて、次は今回の主役と言っていいGETとPOSTのフォームを表示するハンドラである。

まずはGET:

(defn get-form [req]
  (-> (res/response 
       "<div>
          <h1>Hello GET Form!</h1>
          <form method=\"get\" action=\"form-submit\">
           <input type=\"text\" name=\"name\" />
           <input type=\"submit\" value\"submit\" />
          </form>
        </div>")
      (res/content-type "text/html")))

これをappのハンドラとして設定すると、リクエストマップが大体以下のようになる:

{;いろいろ略
 :protocol "HTTP/1.1",
 :params {},
 :headers 
  {;以下略
  },
 :server-port 8080,
 :form-params {},
 :query-params {},
 :uri "/",
 :server-name "localhost",
 :query-string nil,
 :body
 #object[org.eclipse.jetty.server.HttpInputOverHTTP 0x89db6db "HttpInputOverHTTP@89db6db"],
 :scheme :http,
 :request-method :get}

で、フォームに適当な文字列を入れてSubmitを押すと、アドレスバーのURLが"localhost:8080/form-submit?name=Test%21"となる。REPLを調べてみると新しくこのようなリクエストマップが投げられているのがわかる:

{;さらにいろいろ略
 :params {"name" "Test!"},
 :form-params {},
 :query-params {"name" "Test!"},
 :uri "/form-submit",
 :query-string "name=Test%21",
 :request-method :get}

params、query-params、query-string全部に"Test!"という文字列が含まれている(query-stringの場合アドレスバーに!が表示できないので%21になっているが)。この情報を受け取って、ハンドラはよりカスタムなページを作成・表示させることができる。例えばその文字列をHelloの後に表示するとか。

POSTも大体流儀は同じなのだが、そもそもアドレスバー経由じゃないのでquery-paramsにはならない。

(defn post-form [req]
  (-> (res/response 
       "<div>
          <h1>Hello POST Form!</h1>
          <form method=\"post\" action=\"form-submit\">
           <input type=\"text\" name=\"name\" />
           <input type=\"submit\" value\"submit\" />
          </form>
        </div>")
      (res/content-type "text/html")))

こっちのフォームに文字列を入れるとURLが"localhost:8080/form-submit"に変わり(今度はqueryの内容はURLに反映されない)、サーバの方ではこんなリクエストを投げてくる:

{;またしても省略
 :params {"name" "Test!"},
 :form-params {"name" "Test!"},
 :query-params {},
 :uri "/form-submit",
 :query-string nil,
 :request-method :post}

やはりクエリには何も含まれておらず、paramsとform-paramsに情報が入ってくる。前述の通り、これらはwrap-paramsを使わないとリクエストマップに含まれない。

GETとPOSTで大体同じ機能を実装してみたわけだが、HTMLやウェブ入門的な文書によると、基本的にリクエストに応じて情報を引っ張ってくるだけ&クエリ内容にプライバシーの心配がない場合GETを使い、副作用的な効果のあるリクエスト(掲示板への投稿など)や情報を秘匿した方がいいもの(ユーザ名やパスワード)はPOSTを使う、らしい。POSTを使うのも、今のベストプラクティスかどうかはわからないのでその部分は信用しないでいただきたい。

そして次回は今度こそ、uriやparamsを用いて動的にどのページを表示するか、ringアプリの方で決めるコード、つまりルーティング機能を(とりあえずまずはringだけで)実装していく。