developer tip

Compojure 경로의 "큰 아이디어"는 무엇입니까?

optionbox 2020. 8. 9. 10:21
반응형

Compojure 경로의 "큰 아이디어"는 무엇입니까?


저는 Clojure를 처음 접했고 Compojure를 사용하여 기본 웹 애플리케이션을 작성했습니다. 나는 Compojure의 defroutes문법 으로 벽을 치고 있는데 , 그 뒤에있는 "어떻게"와 "왜"를 모두 이해해야한다고 생각합니다.

링 스타일 애플리케이션은 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를 정말 좋아하지만 너무 당황합니다!


Compojure 설명 (어느 정도)

NB. 저는 Compojure 0.4.1로 작업하고 있습니다 ( 여기 GitHub의 0.4.1 릴리스 커밋입니다).

왜?

의 맨 위에 compojure/core.cljCompojure의 목적에 대한 유용한 요약이 있습니다.

링 핸들러를 생성하기위한 간결한 구문.

피상적 인 수준에서 "왜"질문에 대한 모든 것입니다. 좀 더 자세히 알아보기 위해 링 스타일 앱의 작동 방식을 살펴 보겠습니다.

  1. 요청이 도착하고 링 사양에 따라 Clojure 맵으로 변환됩니다.

  2. 이 맵은 응답을 생성 할 것으로 예상되는 소위 "핸들러 함수"(Clojure 맵이기도 함)로 유입됩니다.

  3. 응답 맵은 실제 HTTP 응답으로 변환되어 클라이언트로 다시 전송됩니다.

위의 2 단계는 요청에 사용 된 URI를 검사하고, 쿠키 등을 검사하고, 궁극적으로 적절한 응답에 도달하는 것은 핸들러의 책임이므로 가장 흥미 롭습니다. 분명히이 모든 작업은 잘 정의 된 조각 모음에 포함되어야합니다. 이들은 일반적으로 "기본"핸들러 함수이며이를 래핑하는 미들웨어 함수 모음입니다. 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잠시 후에 여기서 의미 하는 바에 대한 질문으로 돌아갈 것입니다 (그러나 유효한 Ring respose가 아님에 유의하십시오!).

이 예제에서 알 수 있듯이은 example-route함수일 뿐이며 매우 간단한 것입니다. 요청을보고 처리에 관심이 있는지 ( :request-method검사 를 통해 :uri) 결정하고, 그렇다면 기본 응답 맵을 반환합니다.

또한 분명한 것은 경로의 몸체가 실제로 적절한 응답 맵을 평가할 필요가 없다는 것입니다. Compojure는 문자열 (위에서 볼 수 있듯이) 및 기타 여러 객체 유형에 대한 정상적인 기본 처리를 제공합니다. 자세한 내용은 compojure.response/rendermultimethod를 참조하십시오 (코드는 여기에서 완전히 자체 문서화 됨).

defroutes지금 사용해 봅시다 :

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

위에 표시된 예제 요청과 해당 변형에 대한 응답 :request-method :head은 예상과 같습니다.

의 내부 작업은 example-routes각 경로가 차례로 시도됩니다. 그들 중 하나가 비 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"응답합니다 .

이 최신 예제에서 두 가지 새로운 점이 있습니다 : "/:fst/*"비어 있지 않은 바인딩 벡터 [fst]. 첫 번째는 앞서 언급 한 URI 패턴에 대한 Rails 및 Sinatra와 유사한 구문입니다. URI 세그먼트에 대한 정규식 제약이 지원된다는 점에서 위의 예에서 명백한 것보다 약간 더 정교합니다 (예 : ["/:fst/*" :fst #"[0-9]+"]경로 :fst가 위 의 모든 숫자 값만 허용하도록 제공 할 수 있음 ). 두 번째는 :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)) -- when dealing with a GET request with :uri "/", call the function workbench and render whatever it returns into a response map. (Recall that the return value might be a map, but also a string etc.)

  2. (POST "/save" {form-params :form-params} (str form-params)) -- :form-params is an entry in the request map provided by the wrap-params middleware (recall that it is implicitly included by defroutes). The response will be the standard {:status 200 :headers {"Content-Type" "text/html"} :body ...} with (str form-params) substituted for .... (A slightly unusual POST handler, this...)

  3. (GET "/test" [& more] (str "<pre> more "</pre>")) -- this would e.g. echo back the string representation of the map {"foo" "1"} if the user agent asked for "/test?foo=1".

  4. (GET ["/:filename" :filename #".*"] [filename] ...) -- the :filename #".*" part does nothing at all (since #".*" always matches). It calls the Ring utility function ring.util.response/file-response to produce its response; the {:root "./static"} part tells it where to look for the file.

  5. (ANY "*" [] ...) -- a catch-all route. It is good Compojure practice always to include such a route at the end of a defroutes form to ensure that the handler being defined always returns a valid Ring response map (recall that a route matching failure results in nil).

Why this way?

One purpose of the Ring middleware is to add information to the request map; thus cookie-handling middleware adds a :cookies key to the request, wrap-params adds :query-params and/or :form-params if a query string / form data is present and so on. (Strictly speaking, all the information the middleware functions are adding must be already present in the request map, since that is what they get passed; their job is to transform it to be it more convenient to work with in the handlers they wrap.) Ultimately the "enriched" request is passed to the base handler, which examines the request map with all the nicely preprocessed information added by the middleware and produces a response. (Middleware can do more complex things than that -- like wrapping several "inner" handlers and choosing between them, deciding whether to call the wrapped handler(s) at all etc. That is, however, outside the scope of this answer.)

The base handler, in turn, is usually (in non-trivial cases) a function which tends to need just a handful of items of information about the request. (E.g. ring.util.response/file-response doesn't care about most of the request; it only needs a filename.) Hence the need for a simple way of extracting just the relevant parts of a Ring request. Compojure aims to provide a special-purpose pattern matching engine, as it were, which does just that.


There is an excellent article at booleanknot.com from James Reeves (author of Compojure), and reading it made it "click" for me, so I have retranscribed some of it here (really that's all I did).

There is also a slidedeck here from the same author, that answers this exact question.

Compojure is based on Ring, which is an abstraction for http requests.

A concise syntax for generating Ring handlers.

So, what are those Ring handlers ? Extract from the doc :

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

Pretty simple, but also quite low-level. The above handler can be defined more concisely using the ring/util library.

(use 'ring.util.response)

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

Now we want to call different handlers depending on the request. We could do some static routing like so :

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

And refactor it like this :

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

The interesting thing that James notes then is that this allows nesting routes, because "the result of combining two or more routes together is itself a route".

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

By now, we are beginning to see some code that looks like it could be factored, using a macro. Compojure provides a defroutes macro:

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

;; is identical to

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

Compojure provides other macros, like the GET macro:

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

;; will expand to

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

That last function generated looks like our handler !

Please make sure to check out James post, as it goes into more detailed explanations.


For anybody who still struggled to find out what is going on with the routes, it might be that, like me, you don't understand the idea of destructuring.

Actually reading the docs for let helped clear up the whole "where do the magic values come from?" question.

I'm pasting the relevant sections below:

Clojure supports abstract structural binding, often called destructuring, in let binding lists, fn parameter lists, and any macro that expands into a let or fn. The basic idea is that a binding-form can be a data structure literal containing symbols that get bound to the respective parts of the init-expr. The binding is abstract in that a vector literal can bind to anything that is sequential, while a map literal can bind to anything that is associative.

Vector binding-exprs allow you to bind names to parts of sequential things (not just vectors), like vectors, lists, seqs, strings, arrays, and anything that supports nth. The basic sequential form is a vector of binding-forms, which will be bound to successive elements from the init-expr, looked up via nth. In addition, and optionally, & followed by a binding-forms will cause that binding-form to be bound to the remainder of the sequence, i.e. that part not yet bound, looked up via nthnext . Finally, also optional, :as followed by a symbol will cause that symbol to be bound to the entire 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]]

Vector binding-exprs allow you to bind names to parts of sequential things (not just vectors), like vectors, lists, seqs, strings, arrays, and anything that supports nth. The basic sequential form is a vector of binding-forms, which will be bound to successive elements from the init-expr, looked up via nth. In addition, and optionally, & followed by a binding-forms will cause that binding-form to be bound to the remainder of the sequence, i.e. that part not yet bound, looked up via nthnext . Finally, also optional, :as followed by a symbol will cause that symbol to be bound to the entire 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]]

I haven't started on clojure web stuff yet but, I will, here's the stuff I bookmarked.


What's the deal with the destructuring ({form-params :form-params})? What keywords are available for me when destructuring?

The keys available are those that are in the input map. Destructuring is available inside let and doseq forms, or inside the parameters to fn or defn

The following code will hopefully be informative:

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

a more advanced example, showing nested destructuring:

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

When used wisely, destructuring declutters your code by avoiding boilerplate data access. by using :as and printing the result (or the result's keys) you can get a better idea of what other data you could access.

참고URL : https://stackoverflow.com/questions/3488353/whats-the-big-idea-behind-compojure-routes

반응형