compojureルートの背後にある「大きなアイデア」とは何ですか?


109

私はClojureを初めて使用し、Compojureを使用して基本的なWebアプリケーションを作成しています。defroutesしかし、私はCompojureの構文で壁にぶつかっています。その背後にある「方法」と「理由」の両方を理解する必要があると思います。

リングスタイルのアプリケーションは、HTTPリクエストマップで始まり、リクエストがレスポンスマップに変換されてブラウザに返されるまで、一連のミドルウェア関数を介してリクエストを渡しているようです。このスタイルは開発者にとって「低レベル」であると思われるため、Compojureなどのツールが必要です。他のソフトウェアエコシステム、特にPythonのWSGIでも、より多くの抽象化が必要であることがわかります。

問題は、Compojureのアプローチが理解できないことです。次のdefroutesS式を見てみましょう。

(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>"))

これらすべてを理解するための鍵はいくつかのマクロブードゥー内にあることは知っていますが、マクロを完全には理解していません(まだ)。私はdefroutes長い間ソースを見つめてきましたが、それを理解しないでください!何が起きてる?「ビッグアイデア」を理解することは、おそらくこれらの特定の質問に答えるのに役立ちます。

  1. ルーティングされた関数(workbench関数など)内からリング環境にアクセスするにはどうすればよいですか?たとえば、HTTP_ACCEPTヘッダーまたはリクエスト/ミドルウェアの他の部分にアクセスしたいとしますか?
  2. 破壊({form-params :form-params})との取り決めは何ですか?解体するときに使用できるキーワードは何ですか?

Clojureは本当に好きですが、とても困惑しています。

回答:


212

Compojureの説明(ある程度)

NB。私はCompojure 0.4.1(で働いています、ここでの0.4.1リリースのGitHub上でコミット)。

どうして?

の最上部にはcompojure/core.clj、Compojureの目的に関する役立つ概要があります。

リングハンドラーを生成するための簡潔な構文。

表面的なレベルでは、「なぜ」の質問は以上です。もう少し深くするために、リングスタイルのアプリがどのように機能するかを見てみましょう:

  1. リクエストが到着し、Ring仕様に従ってClojureマップに変換されます。

  2. このマップは、応答を生成することが期待される、いわゆる「ハンドラー関数」に流し込まれます(これもClojureマップです)。

  3. 応答マップは実際の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代わりに、応答は次のようになりますnilnilここで何を意味するのかという質問にすぐ戻ります(ただし、有効なリングレスポーズではないことに注意してください)。

この例から明らかなように、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-paramswrap-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))

これは、前の例のリクエストに:bodyofで応答し"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>"))

各ルートを順番に分析してみましょう:

  1. (GET "/" [] (workbench))-でGETリクエストを処理するとき:uri "/"は、関数workbenchを呼び出して、返されたものをすべてレスポンスマップにレンダリングします。(戻り値はマップである可能性がありますが、文字列などでもあることを思い出してください。)

  2. (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ハンドラー、これ...)

  3. (GET "/test" [& more] (str "<pre> more "</pre>"))-これは、たとえば{"foo" "1"}ユーザーエージェントが要求した場合、マップの文字列表現をエコーバックします"/test?foo=1"

  4. (GET ["/:filename" :filename #".*"] [filename] ...)- :filename #".*"パーツは何もしません(#".*"常に一致するため)。Ringユーティリティ関数ring.util.response/file-responseを呼び出して応答を生成します。{:root "./static"}一部には、どこのファイルを探すためにそれを伝えます。

  5. (ANY "*" [] ...)-キャッチオールルート。defroutes定義されているハンドラーが常に有効なリング応答マップを返すことを保証するために、このようなルートをフォームの最後に含めることは、常にCompojureの習慣として適切です(ルートの照合が失敗するとが発生することを思い出してくださいnil)。

なぜこのように?

Ringミドルウェアの1つの目的は、要求マップに情報を追加することです。したがって、Cookie処理ミドルウェアは:cookies、リクエストにキーをwrap-params追加:query-paramsし、追加および/または:form-paramsクエリ文字列/フォームデータが存在する場合など。(厳密に言えば、ミドルウェア関数が追加するすべての情報は、要求マップにすでに存在している必要があります。それが渡されるものです。彼らの仕事は、ラップするハンドラーで作業するのにより便利になるように変換することです。)最終的に、「強化された」リクエストはベースハンドラーに渡されます。ベースハンドラーは、ミドルウェアによって適切に前処理されたすべての情報を含むリクエストマップを調べ、応答を生成します。(ミドルウェアは、それよりも複雑なことを実行できます。たとえば、いくつかの「内部」ハンドラーをラップしてそれらの間で選択し、ラップされたハンドラーを呼び出すかどうかを決定するなどです。ただし、この回答の範囲外です。)

次に、基本ハンドラーは通常、(自明ではない場合に)要求に関する情報のアイテムをほんの少しだけ必要とする傾向がある関数です。(たとえばring.util.response/file-response、ほとんどの要求は気にせず、ファイル名だけが必要です。)したがって、Ring要求の関連部分だけを抽出する簡単な方法が必要です。Compojureは、特殊な目的であるパターンマッチングエンジンを提供することを目的としています。


3
「追加の便宜として、defroutesで定義されたハンドラーは、暗黙的にwrap-paramsおよびwrap-cookiesにラップされます。」-バージョン0.6.0以降、これらを明示的に追加する必要があります。参照github.com/weavejester/compojure/commit/…–
Dan Midwood

3
とてもよく入れました。この答えはCompojureのホームページにあるはずです。
シッダールタレディ

2
Compojureを初めて使用する人は必ず読む必要があります。このトピックに関するすべてのWikiとブログの投稿が、これへのリンクで始まっていれば幸いです。
jemmons

7

James Reeves(Compojureの作者)によるbooleanknot.comに優れた記事があり、それを読むと「クリック」できるようになったので、ここでその一部を書き直しました(実際に行ったのはそれだけです)。

この正確な質問に答える、同じ著者のスライドデッキもあります。

Compojureは、httpリクエストを抽象化したRingに基づいています。

A concise syntax for generating Ring handlers.

それで、それらのリングハンドラーは何ですか?ドキュメントからの抜粋:

;; Handlers are functions that define your web application.
;; They take one argument, a map representing a HTTP request,
;; and return a map representing the HTTP response.

;; Let's take a look at an example:

(defn what-is-my-ip [request]
  {:status 200
   :headers {"Content-Type" "text/plain"}
   :body (:remote-addr request)})

かなりシンプルですが、かなり低レベルです。上記のハンドラーは、ring/utilライブラリーを使用してより簡潔に定義できます。

(use 'ring.util.response)

(defn handler [request]
  (response "Hello World"))

次に、リクエストに応じて異なるハンドラーを呼び出します。次のような静的ルーティングを行うことができます:

(defn handler [request]
  (or
    (if (= (:uri request) "/a") (response "Alpha"))
    (if (= (:uri request) "/b") (response "Beta"))))

そして、それをこのようにリファクタリングします:

(defn a-route [request]
  (if (= (:uri request) "/a") (response "Alpha")))

(defn b-route [request]
  (if (= (:uri request) "/b") (response "Beta"))))

(defn handler [request]
  (or (a-route request)
      (b-route request)))

James氏が注目する興味深い点は、「2つ以上のルートを組み合わせた結果はそれ自体がルートである」ため、ルートをネストできることです。

(defn ab-routes [request]
  (or (a-route request)
      (b-route request)))

(defn cd-routes [request]
  (or (c-route request)
      (d-route request)))

(defn handler [request]
  (or (ab-routes request)
      (cd-routes request)))

今では、マクロを使用して因数分解できるように見えるいくつかのコードが見え始めています。Compojureはdefroutesマクロを提供します。

(defroutes ab-routes a-route b-route)

;; is identical to

(def ab-routes (routes a-route b-route))

Compojureは、GETマクロなどの他のマクロを提供します。

(GET "/a" [] "Alpha")

;; will expand to

(fn [request#]
  (if (and (= (:request-method request#) ~http-method)
           (= (:uri request#) ~uri))
    (let [~bindings request#]
      ~@body)))

最後に生成された関数は、ハンドラーのように見えます。

詳細については、Jamesの投稿をご覧ください。


4

ルートで何が起こっているのかを見つけるのにまだ苦労している人にとって、私と同じように、解体の考えを理解していないのかもしれません。

実際にのドキュメントをlet読んで「魔法の値はどこから来たのか」全体を明確にするの役立ちました。質問。

以下の関連セクションを貼り付けています:

Clojureは、letバインディングリスト、fnパラメータリスト、およびletまたはfnに展開されるすべてのマクロで、しばしば構造化と呼ばれる抽象的な構造バインディングをサポートします。基本的な考え方は、バインディングフォームは、init-exprのそれぞれの部分にバインドされるシンボルを含むデータ構造リテラルにすることができるということです。バインディングは抽象的であり、ベクトルリテラルは連続するものにバインドできますが、マップリテラルは結合可能なものにバインドできます。

ベクトルbinding-exprsを使用すると、ベクトル、リスト、シーケンス、文字列、配列、およびnthをサポートするあらゆるものなど、連続するもの(ベクトルだけでなく)の部分に名前をバインドできます。基本的なシーケンシャルフォームは、バインディングフォームのベクトルであり、n番目を介して検索されるinit-exprからの連続する要素にバインドされます。さらに、オプションで&の後にbinding-formsを続けると、そのbinding-formがシーケンスの残りの部分にバインドされます(つまり、まだバインドされていない部分がnthnextで検索されます)。最後に、オプションである:asの後に記号が続くと、その記号はinit-expr全体にバインドされます。

(let [[a b c & d :as e] [1 2 3 4 5 6 7]]
  [a b c d e])
->[1 2 3 (4 5 6 7) [1 2 3 4 5 6 7]]

ベクトルbinding-exprsを使用すると、ベクトル、リスト、シーケンス、文字列、配列、およびnthをサポートするあらゆるものなど、連続するもの(ベクトルだけでなく)の部分に名前をバインドできます。基本的なシーケンシャルフォームは、バインディングフォームのベクトルであり、n番目を介して検索されるinit-exprからの連続する要素にバインドされます。さらに、オプションで&の後にbinding-formsを続けると、そのbinding-formがシーケンスの残りの部分にバインドされます(つまり、まだバインドされていない部分がnthnextで検索されます)。最後に、オプションである:asの後に記号が続くと、その記号はinit-expr全体にバインドされます。

(let [[a b c & d :as e] [1 2 3 4 5 6 7]]
  [a b c d e])
->[1 2 3 (4 5 6 7) [1 2 3 4 5 6 7]]

3

ありがとう、これらのリンクは間違いなく役に立ちます。私はこの問題に一日の大半を費やして取り組んでおり、それを使ってより良い場所にいる...私はいつかフォローアップを投稿しようとする。
ショーンウッズ、

1

解体({form-params:form-params})はどうなっていますか?解体するときに利用できるキーワードは何ですか?

使用可能なキーは、入力マップにあるキーです。解体は、letおよびdoseqフォーム内、またはfnまたはdefnのパラメーター内で使用できます。

次のコードは有益であろう:

(let [{a :thing-a
       c :thing-c :as things} {:thing-a 0
                               :thing-b 1
                               :thing-c 2}]
  [a c (keys things)])

=> [0 2 (:thing-b :thing-a :thing-c)]

より高度な例で、ネストされた分解を示しています。

user> (let [{thing-id :id
             {thing-color :color :as props} :properties} {:id 1
                                                          :properties {:shape
                                                                       "square"
                                                                       :color
                                                                       0xffffff}}]
            [thing-id thing-color (keys props)])
=> [1 16777215 (:color :shape)]

賢く使用する場合、デストラクタリングは、ボイラープレートデータアクセスを回避することにより、コードを整理します。:asを使用して結果(または結果のキー)を出力することにより、アクセスできる他のデータをより的確に把握できます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.