Compojureは(ある程度)説明しました
NB。私はCompojure0.4.1を使用しています(これはGitHubでの0.4.1リリースコミットです)。
なんで?
の一番上にcompojure/core.clj
、Compojureの目的の次の役立つ要約があります。
リングハンドラーを生成するための簡潔な構文。
表面的なレベルでは、「なぜ」の質問はこれですべてです。もう少し詳しく説明するために、リングスタイルのアプリがどのように機能するかを見てみましょう。
リクエストが到着し、リングの仕様に従って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
で応答します。"foo"
この最新の例では、"/:fst/*"
と空でないバインディングベクトルの2つが新しくなってい[fst]
ます。1つ目は、前述のURIパターンのRails-and-Sinatraのような構文です。URIセグメントの正規表現制約がサポートされているという点で、上記の例から明らかなものよりも少し洗練されています(たとえば["/:fst/*" :fst #"[0-9]+"]
、ルートが上記のすべての数字の値のみを受け入れるように指定できます:fst
)。:params
2つ目は、それ自体がマップであるリクエストマップのエントリを照合する簡単な方法です。リクエスト、クエリ文字列パラメータ、フォームパラメータから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
定義されているハンドラーが常に有効なリング応答マップを返すように、フォームの最後にそのようなルートを常に含めることをお勧めします(ルートマッチングの失敗は結果として生じることを思い出してnil
ください)。
なぜこのように?
Ringミドルウェアの目的の1つは、リクエストマップに情報を追加することです。したがって、Cookie処理ミドルウェアは:cookies
、リクエストにキーを追加、wrap-params
追加:query-params
、および/または:form-params
クエリ文字列/フォームデータが存在する場合など。(厳密に言えば、ミドルウェア関数が追加するすべての情報は、渡されるものであるため、リクエストマップにすでに存在している必要があります。ミドルウェアの仕事は、ラップするハンドラーでの作業がより便利になるように変換することです。)最終的に、「強化された」要求はベースハンドラーに渡されます。ベースハンドラーは、ミドルウェアによって追加されたすべての適切に前処理された情報を使用して要求マップを調べ、応答を生成します。(ミドルウェアはそれよりも複雑なことを行うことができます。たとえば、いくつかの「内部」ハンドラーをラップしてそれらの中から選択したり、ラップされたハンドラーを呼び出すかどうかを決定したりします。ただし、この回答の範囲外です。)
次に、ベースハンドラーは、通常(重要な場合)、要求に関するほんの一握りの情報を必要とする傾向がある関数です。(たとえばring.util.response/file-response
、ほとんどのリクエストは気にせず、ファイル名のみが必要です。)したがって、Ringリクエストの関連部分だけを抽出する簡単な方法が必要です。Compojureは、いわばそれを実行する専用のパターンマッチングエンジンを提供することを目的としています。