1. ホーム
  2. clojure

[解決済み] コンポジュール・ルートの "ビッグ・アイデア "とは?

2022-09-21 18:56:16

質問

私はClojureの初心者で、基本的なWebアプリケーションを書くためにCompojureを使用しています。 私は、Compojureの defroutes 構文で壁にぶち当たり、その背後にある"how"と"why"の両方を理解する必要があると思います。

Ringスタイルのアプリケーションは、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. ルーティングされた関数内から Ring 環境にアクセスするにはどうすればよいですか (たとえば workbench 関数) の中からどのようにRing環境にアクセスするのでしょうか? たとえば、HTTP_ACCEPTヘッダーや、リクエスト/ミドルウェアの他の部分にアクセスしたいとします?
  2. デストラクチャリング( {form-params :form-params} )? デストラクチャリング時に使用できるキーワードは何ですか?

私は本当にClojureが好きですが、私はとても困っています!

どのように解決するのですか?

Compojureの説明 (ある程度)

NBです。私は Compojure 0.4.1 で作業しています ( ここで はGitHubの0.4.1リリースコミットです)。

なぜですか?

の一番上にある compojure/core.clj の一番上に、Compojureの目的について、このように役に立つ要約があります。

Ringハンドラを生成するための簡潔な構文です。

表面的なレベルでは、これが「なぜ」という疑問に対するすべてです。 もう少し深く掘り下げるために、Ring スタイルのアプリがどのように機能するかを見てみましょう。

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

  2. このマップは、いわゆる "ハンドラ関数" にファネルされ、レスポンス(これもClojureマップです)を生成することが期待されています。

  3. レスポンスマップは実際のHTTPレスポンスに変換され、クライアントに送り返されます。

上記のステップ 2 は最も興味深いもので、リクエストで使用された URI を調べ、クッキーなどを調べ、最終的に適切なレスポンスに到達するのはハンドラの責任だからです。 これらは通常、ハンドラ関数とそれをラップするミドルウェア関数のコレクションです。 Compojureの目的は、ベースハンドラ関数の生成を簡素化することです。

どのように?

Compojureは、quot;routes"の概念を中心に構築されています。 これらは実際には、より深いレベルで クラウト ライブラリ (Compojure プロジェクトのスピンオフで、0.3.x -> 0.4.x の移行時に多くのものが別のライブラリに移動されました) によって、より深いレベルで実装されています。 ルートは、(1)HTTPメソッド(GET、PUT、HEAD...)、(2)URIパターン(Webby Rubyistにはおなじみの構文で指定)、(3)リクエストマップの一部をボディで利用できる名前に結びつける際に使用する破壊形式、(4)有効なRing応答を生成する必要がある式のボディ(非自明のケースでは、これは通常単に別の関数への呼び出しです)により定義されています。

これは、簡単な例を見てみるには良いポイントかもしれません。

(def example-route (GET "/" [] "<html>...</html>"))

これをREPLでテストしてみましょう(以下のリクエストマップはRingの最小限の有効なリクエストマップです)。

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 が何を意味するのかについては、すぐにここで説明します(ただし、これは有効なRingのレスポンスではないことに注意してください!)。

この例から明らかなように example-route は単なる関数で、非常に単純なものです。 リクエストを見て、それを処理することに興味があるかどうかを ( を調べることによって) 決定します。 :request-method:uri を含む)、もしそうなら、基本的なレスポンス・マップを返します。

Compojureは文字列(上で見たように)と他の多くのオブジェクトタイプに対して、適切なデフォルトの処理を提供します。 compojure.response/render マルチメソッドを参照してください(コードはここで完全にセルフドキュメント化されています)。

を使ってみましょう。 defroutes を使ってみましょう。

(defroutes example-routes
  (GET "/" [] "get")
  (HEAD "/" [] "head"))

上に表示された例のリクエストに対するレスポンスと、その変形で :request-method :head への応答は予想通りです。

の内部動作は example-routes の内部動作は、それぞれのルートが順番に試行されるようになっています。 nil でないレスポンスを返すとすぐに、そのレスポンスは全体の example-routes ハンドラ全体の戻り値となります。 さらに便利なことに defroutes -で定義されたハンドラは 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))

これで応答するのは :body"foo" を前の例からのリクエストに変換します。

この最新の例では二つのことが新しくなっています。 "/:fst/*" と空でないバインディングベクター [fst] . 1つ目は、前述のRailsと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.util.response/file-response を呼び出して、その応答を生成します。 {:root "./static"} の部分はファイルを探す場所を示しています。

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

なぜこの方法なのか?

リングミドルウェアの目的の一つは、リクエストマップに情報を追加することです。したがって、クッキーを処理するミドルウェアは、リクエストマップに :cookies キーをリクエストに追加します。 wrap-params が追加されます。 :query-params または :form-params クエリ文字列/フォームデータが存在する場合などです。 (厳密に言うと、ミドルウェア関数が追加するすべての情報はリクエストマップに既に存在していなければなりません。) 最終的に "enriched" リクエストはベースハンドラに渡されます。ベースハンドラは、ミドルウェアによって追加されたすべてのきれいに前処理された情報でリクエストマップを調べ、応答を生成します。 (ミドルウェアはそれよりももっと複雑なことができます。例えば、いくつかの "inner"ハンドラをラップして、それらの間で選択したり、 ラップしたハンドラを呼び出すかどうかを決定したり、などです。 しかし、それは、この回答の範囲外です)。

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