112

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

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

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

(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が本当に好きですが、とても困惑しています!

4

5 に答える 5

216

Compojureは(ある程度)説明しました

NB。私はCompojure0.4.1を使用しています(これGitHubでの0.4.1リリースコミットです)。

なんで?

の一番上にcompojure/core.clj、Compojureの目的の次の役立つ要約があります。

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

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

  1. リクエストが到着し、リングの仕様に従って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応答はになります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)。:params2つ目は、それ自体がマップであるリクエストマップのエントリを照合する簡単な方法です。リクエスト、クエリ文字列パラメータ、フォームパラメータから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定義されているハンドラーが常に有効なリング応答マップを返すように、フォームの最後にそのようなルートを常に含めることをお勧めします(ルートマッチングの失敗は結果として生じることを思い出してnilください)。

なぜこのように?

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

次に、ベースハンドラーは、通常(重要な場合)、要求に関するほんの一握りの情報を必要とする傾向がある関数です。(たとえばring.util.response/file-response、ほとんどのリクエストは気にせず、ファイル名のみが必要です。)したがって、Ringリクエストの関連部分だけを抽出する簡単な方法が必要です。Compojureは、いわばそれを実行する専用のパターンマッチングエンジンを提供することを目的としています。

于 2010-08-16T03:51:04.073 に答える
9

booleanknot.comにJamesReeves (Compojureの作者)からの優れた記事があり、それを読むと「クリック」されたので、ここでその一部を再転写しました(実際に私が行ったのはそれだけです)。

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

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

ジェームズが指摘する興味深い点は、「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)))

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

より詳細な説明がありますので、必ずジェームズの投稿をチェックしてください。

于 2014-11-29T13:40:29.190 に答える
4

ルートで何が起こっているのかを知るのにまだ苦労している人にとっては、私のように、破壊するという考えを理解していないのかもしれません。

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

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

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

ベクトルバインディング-exprsを使用すると、ベクトル、リスト、seq、文字列、配列、およびn番目をサポートするものなどのシーケンシャルなもの(ベクトルだけでなく)の一部に名前をバインドできます。基本的なシーケンシャルフォームはバインディングフォームのベクトルであり、init-exprからの連続する要素にバインドされ、n番目を介して検索されます。さらに、オプションで、&の後にバインディングフォームが続くと、そのバインディングフォームがシーケンスの残りの部分にバインドされます。つまり、まだバインドされていない部分は、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]]

ベクトルバインディング-exprsを使用すると、ベクトル、リスト、seq、文字列、配列、およびn番目をサポートするものなどのシーケンシャルなもの(ベクトルだけでなく)の一部に名前をバインドできます。基本的なシーケンシャルフォームはバインディングフォームのベクトルであり、init-exprからの連続する要素にバインドされ、n番目を介して検索されます。さらに、オプションで、&の後にバインディングフォームが続くと、そのバインディングフォームがシーケンスの残りの部分にバインドされます。つまり、まだバインドされていない部分は、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]]
于 2010-11-29T17:36:10.507 に答える
3

私はまだclojureWebのものを始めていませんが、これが私がブックマークしたものです。

于 2010-08-16T00:39:46.170 に答える
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を使用し、結果(または結果のキー)を出力することで、アクセスできる他のデータをより正確に把握できます。

于 2013-04-23T18:50:16.437 に答える