Compojureの説明(ある程度)
NB。私はCompojure 0.4.1(で働いています、ここでの0.4.1リリースのGitHub上でコミット)。
どうして?
の最上部にはcompojure/core.clj
、Compojureの目的に関する役立つ概要があります。
リングハンドラーを生成するための簡潔な構文。
表面的なレベルでは、「なぜ」の質問は以上です。もう少し深くするために、リングスタイルのアプリがどのように機能するかを見てみましょう:
リクエストが到着し、Ring仕様に従ってClojureマップに変換されます。
このマップは、応答を生成することが期待される、いわゆる「ハンドラー関数」に流し込まれます(これもClojureマップです)。
応答マップは実際のHTTP応答に変換され、クライアントに送り返されます。
上記のステップ2.は、リクエストで使用されたURIを調べ、Cookieなどを調べ、最終的に適切な応答に到達するのはハンドラーの責任であるため、最も興味深いものです。明らかに、このすべての作業を明確に定義された要素のコレクションに組み込む必要があります。これらは通常「ベース」ハンドラー関数とそれをラップするミドルウェア関数のコレクションです。 Compojureの目的は、基本ハンドラー関数の生成を単純化することです。
どうやって?
Compojureは「ルート」の概念を中心に構築されています。これらは実際には、Cloutライブラリー(Compojureプロジェクトのスピンオフ-0.3.x-> 0.4.xの移行で多くのものが個別のライブラリーに移動されました)によって、より深いレベルで実装されます。ルートは、(1)HTTPメソッド(GET、PUT、HEAD ...)、(2)URIパターン(Webby Rubyistsにはおなじみの構文で指定)、(3)で使用される分解フォームによって定義されます。リクエストマップの一部を本文で使用可能な名前にバインドします。(4)有効なリング応答を生成する必要がある式の本文(重要な場合、これは通常、別の関数の呼び出しです)。
これは、簡単な例を見るのに良いポイントかもしれません:
(def example-route (GET "/" [] "<html>...</html>"))
REPLでこれをテストしてみましょう(以下の要求マップは、最小の有効なリング要求マップです)。
user> (example-route {:server-port 80
:server-name "127.0.0.1"
:remote-addr "127.0.0.1"
:uri "/"
:scheme :http
:headers {}
:request-method :get})
{:status 200,
:headers {"Content-Type" "text/html"},
:body "<html>...</html>"}
場合は:request-method
なかった:head
代わりに、応答は次のようになりますnil
。nil
ここで何を意味するのかという質問にすぐ戻ります(ただし、有効なリングレスポーズではないことに注意してください)。
この例から明らかなように、example-route
は単なる関数であり、非常に単純なものです。リクエストを見て、それを処理することに関心があるかどうかを調べ(とを調べ:request-method
て:uri
)、そうである場合は、基本的な応答マップを返します。
また、ルートの本体は実際に適切な応答マップを評価する必要がないことも明らかです。Compojureは、文字列(上記を参照)と他の多くのオブジェクトタイプに対して適切なデフォルト処理を提供します。詳細はcompojure.response/render
マルチメソッドを参照してください(コードは完全に自己文書化されています)。
defroutes
今使ってみましょう:
(defroutes example-routes
(GET "/" [] "get")
(HEAD "/" [] "head"))
上に表示されたリクエストの例とそのバリアントへの応答:request-method :head
は期待どおりです。
の内部動作はexample-routes
、各ルートが順番に試行されるようなものです。それらの1つが非nil
応答を返すとすぐに、その応答はexample-routes
ハンドラー全体の戻り値になります。追加の便宜として、defroutes
-definedハンドラーはwrap-params
、wrap-cookies
暗黙のうちにラップされています。
次に、より複雑なルートの例を示します。
(def echo-typed-url-route
(GET "*" {:keys [scheme server-name server-port uri]}
(str (name scheme) "://" server-name ":" server-port uri)))
以前に使用された空のベクトルの代わりに破壊フォームに注意してください。ここでの基本的な考え方は、ルートの本体がリクエストに関するいくつかの情報に関心を持つ可能性があるということです。これは常にマップの形で届くため、リクエストから情報を抽出し、それをルートの本体のスコープ内にあるローカル変数にバインドするための連想分解フォームを提供できます。
上記のテスト:
user> (echo-typed-url-route {:server-port 80
:server-name "127.0.0.1"
:remote-addr "127.0.0.1"
:uri "/foo/bar"
:scheme :http
:headers {}
:request-method :get})
{:status 200,
:headers {"Content-Type" "text/html"},
:body "http://127.0.0.1:80/foo/bar"}
上記の素晴らしいフォローアップのアイデアは、より複雑なルートがassoc
一致する段階でリクエストに情報を追加する可能性があるということです:
(def echo-first-path-component-route
(GET "/:fst/*" [fst] fst))
これは、前の例のリクエストに:body
ofで応答し"foo"
ます。
この最新の例では、2つの点が新しくなっています。"/:fst/*"
および空でないバインディングベクトル[fst]
です。1つ目は、前述のRails-and-Sinatraに似たURIパターンの構文です。URIセグメントの正規表現制約がサポート["/:fst/*" :fst #"[0-9]+"]
されるという点で、上記の例から明らかなものよりも少し洗練されています(たとえば、ルート:fst
に上記のすべての数字の値のみを受け入れるように指定できます)。2つ目は:params
、リクエストマップのエントリを照合する簡単な方法です。これは、それ自体がマップです。リクエスト、クエリ文字列パラメータ、フォームパラメータからURIセグメントを抽出するのに役立ちます。後者の点を説明する例:
(defroutes echo-params
(GET "/" [& more]
(str more)))
user> (echo-params
{:server-port 80
:server-name "127.0.0.1"
:remote-addr "127.0.0.1"
:uri "/"
:query-string "foo=1"
:scheme :http
:headers {}
:request-method :get})
{:status 200,
:headers {"Content-Type" "text/html"},
:body "{\"foo\" \"1\"}"}
これは、質問テキストの例を見る良い機会です。
(defroutes main-routes
(GET "/" [] (workbench))
(POST "/save" {form-params :form-params} (str form-params))
(GET "/test" [& more] (str "<pre>" more "</pre>"))
(GET ["/:filename" :filename #".*"] [filename]
(response/file-response filename {:root "./static"}))
(ANY "*" [] "<h1>Page not found.</h1>"))
各ルートを順番に分析してみましょう:
(GET "/" [] (workbench))
-でGET
リクエストを処理するとき:uri "/"
は、関数workbench
を呼び出して、返されたものをすべてレスポンスマップにレンダリングします。(戻り値はマップである可能性がありますが、文字列などでもあることを思い出してください。)
(POST "/save" {form-params :form-params} (str form-params))
- ミドルウェア:form-params
によって提供されるリクエストマップのエントリwrap-params
です(これはによって暗黙的にインクルードされることを思い出してくださいdefroutes
)。応答は{:status 200 :headers {"Content-Type" "text/html"} :body ...}
、に(str form-params)
置き換えられた標準になり...
ます。(少し変わったPOST
ハンドラー、これ...)
(GET "/test" [& more] (str "<pre> more "</pre>"))
-これは、たとえば{"foo" "1"}
ユーザーエージェントが要求した場合、マップの文字列表現をエコーバックします"/test?foo=1"
。
(GET ["/:filename" :filename #".*"] [filename] ...)
- :filename #".*"
パーツは何もしません(#".*"
常に一致するため)。Ringユーティリティ関数ring.util.response/file-response
を呼び出して応答を生成します。{:root "./static"}
一部には、どこのファイルを探すためにそれを伝えます。
(ANY "*" [] ...)
-キャッチオールルート。defroutes
定義されているハンドラーが常に有効なリング応答マップを返すことを保証するために、このようなルートをフォームの最後に含めることは、常にCompojureの習慣として適切です(ルートの照合が失敗するとが発生することを思い出してくださいnil
)。
なぜこのように?
Ringミドルウェアの1つの目的は、要求マップに情報を追加することです。したがって、Cookie処理ミドルウェアは:cookies
、リクエストにキーをwrap-params
追加:query-params
し、追加および/または:form-params
クエリ文字列/フォームデータが存在する場合など。(厳密に言えば、ミドルウェア関数が追加するすべての情報は、要求マップにすでに存在している必要があります。それが渡されるものです。彼らの仕事は、ラップするハンドラーで作業するのにより便利になるように変換することです。)最終的に、「強化された」リクエストはベースハンドラーに渡されます。ベースハンドラーは、ミドルウェアによって適切に前処理されたすべての情報を含むリクエストマップを調べ、応答を生成します。(ミドルウェアは、それよりも複雑なことを実行できます。たとえば、いくつかの「内部」ハンドラーをラップしてそれらの間で選択し、ラップされたハンドラーを呼び出すかどうかを決定するなどです。ただし、この回答の範囲外です。)
次に、基本ハンドラーは通常、(自明ではない場合に)要求に関する情報のアイテムをほんの少しだけ必要とする傾向がある関数です。(たとえばring.util.response/file-response
、ほとんどの要求は気にせず、ファイル名だけが必要です。)したがって、Ring要求の関連部分だけを抽出する簡単な方法が必要です。Compojureは、特殊な目的であるパターンマッチングエンジンを提供することを目的としています。