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


109

저는 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를 정말 좋아하지만 너무 당황합니다!

답변:


212

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))-를 사용하여 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 "*" [] ...)-포괄 경로. defroutes정의되는 핸들러가 항상 유효한 링 응답 맵을 반환하도록하려면 양식 끝에 이러한 경로를 항상 포함하는 것이 좋습니다 (경로 일치 실패로 인해 nil).

왜 이렇게?

Ring 미들웨어의 한 가지 목적은 요청 맵에 정보를 추가하는 것입니다. 따라서 쿠키 처리 미들웨어는 :cookies요청에 키를 wrap-params추가 :query-params하고 추가 및 / 또는:form-params쿼리 문자열 / 양식 데이터가있는 경우 등등. (엄밀히 말하면 미들웨어 함수가 추가하는 모든 정보는 전달 된 것이기 때문에 요청 맵에 이미 존재해야합니다. 그들의 임무는 래핑하는 핸들러에서 작업하는 것이 더 편리하도록이를 변환하는 것입니다.) 궁극적으로 "풍부한"요청은 미들웨어에 의해 추가 된 훌륭하게 전처리 된 모든 정보로 요청 맵을 검사하고 응답을 생성하는 기본 핸들러로 전달됩니다. (미들웨어는 여러 "내부"핸들러를 래핑하고 그 중에서 선택하고 래핑 된 핸들러를 호출할지 여부를 결정하는 등 그보다 더 복잡한 작업을 수행 할 수 있습니다. 그러나 이는이 답변의 범위를 벗어납니다.)

차례로 기본 핸들러는 일반적으로 (중요하지 않은 경우) 요청에 대한 몇 가지 정보 항목 만 필요한 경향이있는 함수입니다. (예를 들어 ring.util.response/file-response대부분의 요청은 신경 쓰지 않고 파일 이름 만 필요합니다.) 따라서 Ring 요청의 관련 부분 만 추출하는 간단한 방법이 필요합니다. Compojure는 그 자체로 특별한 목적의 패턴 매칭 엔진을 제공하는 것을 목표로합니다.


3
"추가 된 편의로, defroutes 정의 된 핸들러는 암시 적으로 wrap-params와 wrap-cookies에 래핑됩니다." -버전 0.6.0부터 명시 적으로 추가해야합니다. Ref github.com/weavejester/compojure/commit/…
Dan Midwood

3
아주 잘 했어요. 이 답변은 Compojure의 홈페이지에 있어야합니다.
Siddhartha Reddy 2012

2
Compojure를 처음 접하는 사람이라면 반드시 읽어야합니다. 주제에 대한 모든 위키 및 블로그 게시물이 이에 대한 링크로 시작되기를 바랍니다.
jemmons dec

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가 주목하는 흥미로운 점은 "두 개 이상의 경로를 함께 결합한 결과 자체가 경로"이기 때문에 중첩 경로를 허용한다는 것입니다.

(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 post 를 확인하십시오 .


4

경로에서 무슨 일이 일어나고 있는지 알아 내기 위해 여전히 고군분투하는 사람이라면 나처럼 구조화의 개념을 이해하지 못할 수도 있습니다.

실제로 문서를let 읽는 것은 "마법의 가치는 어디에서 오는가?"전체를 명확하게하는 데 도움이됩니다. 질문.

아래 관련 섹션을 붙여 넣습니다.

Clojure는 let 바인딩 목록, fn 매개 변수 목록 및 let 또는 fn으로 확장되는 모든 매크로에서 종종 destructuring이라고하는 추상 구조 바인딩을 지원합니다. 기본 아이디어는 바인딩 양식이 init-expr의 각 부분에 바인딩되는 기호를 포함하는 데이터 구조 리터럴 일 수 있다는 것입니다. 바인딩은 벡터 리터럴이 순차적 인 모든 것에 바인딩 할 수있는 반면, 맵 리터럴은 연관되는 모든 것에 바인딩 할 수 있다는 점에서 추상적입니다.

Vector binding-exprs를 사용하면 벡터, 목록, 시퀀스, 문자열, 배열 및 n 번째를 지원하는 모든 것과 같은 순차적 인 요소 (벡터뿐만 아니라)의 일부에 이름을 바인딩 할 수 있습니다. 기본 순차 형식은 바인딩 형식의 벡터이며 init-expr의 연속 요소에 바인딩되며 nth를 통해 조회됩니다. 추가로, 그리고 선택적으로 & 뒤에 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]]

Vector binding-exprs를 사용하면 벡터, 목록, 시퀀스, 문자열, 배열 및 n 번째를 지원하는 모든 것과 같은 순차적 인 요소 (벡터뿐만 아니라)의 일부에 이름을 바인딩 할 수 있습니다. 기본 순차 형식은 바인딩 형식의 벡터이며 init-expr의 연속 요소에 바인딩되며 nth를 통해 조회됩니다. 추가로, 그리고 선택적으로 & 뒤에 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

감사합니다.이 링크는 확실히 도움이됩니다. 나는 하루 중 더 나은 시간 동안이 문제를 해결하고 있으며 더 나은 위치에 있습니다 ... 언젠가 후속 조치를 게시하려고 노력할 것입니다.
Sean Woods

1

Destructuring ({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.