Jaka jest „wielka idea” stojąca za trasami kompozycyjnymi?

109

Jestem nowy w Clojure i używam Compojure do pisania podstawowej aplikacji internetowej. Uderzam jednak w ścianę ze składnią Compojure defroutesi myślę, że muszę zrozumieć zarówno „jak”, jak i „dlaczego” za tym wszystkim.

Wygląda na to, że aplikacja w stylu pierścienia zaczyna się od mapy żądań HTTP, a następnie przekazuje żądanie przez szereg funkcji oprogramowania pośredniego, aż zostanie przekształcone w mapę odpowiedzi, która zostanie wysłana z powrotem do przeglądarki. Ten styl wydaje się zbyt niski dla programistów, stąd potrzeba narzędzia takiego jak Compojure. Widzę tę potrzebę większej abstrakcji również w innych ekosystemach oprogramowania, zwłaszcza w WSGI Pythona.

Problem w tym, że nie rozumiem podejścia Compojure. Weźmy następujące defrouteswyrażenie 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>"))

Wiem, że klucz do zrozumienia tego wszystkiego leży w jakimś makro voodoo, ale nie do końca rozumiem makra (jeszcze). Patrzyłem na defroutesźródło przez długi czas, ale po prostu go nie rozumiem! Co tu się dzieje? Zrozumienie „wielkiej idei” prawdopodobnie pomoże mi odpowiedzieć na następujące pytania:

  1. Jak uzyskać dostęp do środowiska Ring z poziomu funkcji routowanej (np workbench Funkcji)? Na przykład, powiedzmy, że chcę uzyskać dostęp do nagłówków HTTP_ACCEPT lub innej części żądania / oprogramowania pośredniego?
  2. O co chodzi z destrukturyzacją ( {form-params :form-params})? Jakie słowa kluczowe są dostępne dla mnie podczas destrukturyzacji?

Bardzo lubię Clojure, ale jestem tak zdumiony!

Sean Woods
źródło

Odpowiedzi:

212

Kompozycja wyjaśniona (do pewnego stopnia)

NB. Pracuję z Compojure 0.4.1 ( tutaj jest zatwierdzenie wydania 0.4.1 na GitHub).

Czemu?

Na samym początku compojure/core.cljznajduje się pomocne podsumowanie celu Compojure:

Zwięzła składnia do generowania programów obsługi pierścienia.

Na pozór to wszystko, co dotyczy pytania „dlaczego”. Aby przejść nieco głębiej, przyjrzyjmy się, jak działa aplikacja w stylu Ring:

  1. Nadchodzi żądanie i jest przekształcane w mapę Clojure zgodnie ze specyfikacją Ring.

  2. Ta mapa jest kierowana do tak zwanej „funkcji obsługi”, która ma wygenerować odpowiedź (która jest również mapą Clojure).

  3. Mapa odpowiedzi jest przekształcana w rzeczywistą odpowiedź HTTP i wysyłana z powrotem do klienta.

Krok 2. z powyższego jest najbardziej interesujący, ponieważ obowiązkiem osoby obsługującej jest sprawdzenie identyfikatora URI użytego w żądaniu, zbadanie wszelkich plików cookie itp., A ostatecznie uzyskanie odpowiedniej odpowiedzi. Oczywiście konieczne jest, aby cała ta praca została uwzględniona w kolekcji dobrze zdefiniowanych utworów; są to zwykle „podstawowa” funkcja obsługi i zbiór funkcji oprogramowania pośredniego, które ją opakowują. Celem Compojure jest uproszczenie generowania podstawowej funkcji obsługi.

W jaki sposób?

Compojure jest zbudowane wokół pojęcia „tras”. W rzeczywistości są one wdrażane na głębszym poziomie przez Clout bibliotekę (spinoff projektu Compojure - wiele rzeczy zostało przeniesionych do oddzielnych bibliotek przy przejściu 0,3.x -> 0,4.x). Trasa jest definiowana przez (1) metodę HTTP (GET, PUT, HEAD ...), (2) wzorzec URI (określony składnią, która będzie najwyraźniej znana Webby Rubyists), (3) forma destrukturyzacji używana w powiązanie części żądania z nazwami dostępnymi w treści, (4) zbiór wyrażeń, które muszą wygenerować prawidłową odpowiedź Ring (w nietrywialnych przypadkach jest to zwykle po prostu wywołanie oddzielnej funkcji).

Warto rzucić okiem na prosty przykład:

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

Przetestujmy to w REPL (mapa żądań poniżej jest minimalną poprawną mapą żądań 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>"}

Jeśli :request-methodbyły :head, a nie, odpowiedź byłaby nil. Wrócimy do pytania o conilZa chwilę znaczy (ale zauważ, że nie jest to poprawna odpowiedź na Ring!).

Jak widać z tego przykładu, example-routejest to tylko funkcja, i to bardzo prosta; sprawdza żądanie, określa, czy jest zainteresowany jego obsługą (poprzez badanie :request-methodi :uri), a jeśli tak, zwraca podstawową mapę odpowiedzi.

Oczywiste jest również, że główna część trasy nie musi tak naprawdę oceniać właściwej mapy odpowiedzi; Compojure zapewnia rozsądną domyślną obsługę łańcuchów (jak widać powyżej) i wielu innych typów obiektów; compojure.response/renderszczegółowe informacje można znaleźć w metodzie multimetrycznej (kod jest tutaj całkowicie samodokumentujący).

Spróbujmy defroutesteraz użyć :

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

Odpowiedzi na przykładowe żądanie wyświetlone powyżej i jego wariant z :request-method :headsą zgodne z oczekiwaniami.

Wewnętrzne działanie example-routesjest takie, że każda trasa jest sprawdzana po kolei; jak tylko jeden z nich zwróci brak nilodpowiedzi, ta odpowiedź staje się wartością zwracaną przez cały example-routesprogram obsługi. Dla dodatkowej wygody defroutes-definiowane procedury obsługi są zawijane wrap-paramsi wrap-cookiesniejawnie.

Oto przykład bardziej złożonej trasy:

(def echo-typed-url-route
  (GET "*" {:keys [scheme server-name server-port uri]}
    (str (name scheme) "://" server-name ":" server-port uri)))

Zwróć uwagę na formę destrukturyzacji zamiast wcześniej używanego pustego wektora. Podstawowa idea jest taka, że ​​treść trasy może być zainteresowana pewnymi informacjami o żądaniu; ponieważ zawsze pojawia się w formie mapy, można dostarczyć asocjacyjny formularz destrukturyzacji, aby wyodrębnić informacje z żądania i powiązać je ze zmiennymi lokalnymi, które będą znajdować się w zakresie w treści trasy.

Test powyższego:

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"}

Doskonałym pomysłem kontynuacji powyższego jest to, że bardziej złożone trasy mogą zawierać assocdodatkowe informacje na żądanie na etapie dopasowania:

(def echo-first-path-component-route
  (GET "/:fst/*" [fst] fst))

Ten odpowiada, :bodyz"foo" na żądanie z poprzedniego przykładu.

Dwie rzeczy są nowe w tym najnowszym przykładzie: "/:fst/*"i niepusty wektor wiążący [fst]. Pierwszą jest wspomniana powyżej składnia podobna do Rails-and-Sinatra dla wzorców URI. Jest to nieco bardziej wyrafinowane niż to, co wynika z powyższego przykładu, ponieważ obsługiwane są ograniczenia wyrażeń regularnych w segmentach URI (np. ["/:fst/*" :fst #"[0-9]+"]Można je podać, aby trasa akceptowała tylko wartości pełnocyfrowe :fstw powyższym). Drugi to uproszczony sposób dopasowywania :paramswpisu w mapie żądań, która sama jest mapą; przydaje się do wyodrębniania segmentów URI z żądania, parametrów ciągu zapytania i parametrów formularza. Przykład ilustrujący ten ostatni punkt:

(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\"}"}

To byłby dobry moment, aby spojrzeć na przykład z tekstu pytania:

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

Przeanalizujmy kolejno każdą trasę:

  1. (GET "/" [] (workbench))- gdy mamy do czynienia z GETżądaniem :uri "/", wywołaj funkcję workbenchi wyrenderuj wszystko, co zwraca, do mapy odpowiedzi. (Przypomnij sobie, że zwracana wartość może być mapą, ale także ciągiem znaków itp.)

  2. (POST "/save" {form-params :form-params} (str form-params))- :form-paramsto wpis w mapie żądań dostarczonej przez wrap-paramsoprogramowanie pośredniczące (pamiętaj, że jest on niejawnie uwzględniony przez defroutes). Odpowiedzią będzie standard {:status 200 :headers {"Content-Type" "text/html"} :body ...}z (str form-params)podstawioną .... (Trochę nietypowy przewodnik POST, to ...)

  3. (GET "/test" [& more] (str "<pre> more "</pre>"))- spowodowałoby to np. odtworzenie echa reprezentacji ciągu mapy, {"foo" "1"}gdyby zażądał o to klient użytkownika "/test?foo=1".

  4. (GET ["/:filename" :filename #".*"] [filename] ...)- :filename #".*"część nic nie robi (ponieważ #".*"zawsze pasuje). Wywołuje funkcję narzędzia Ring, ring.util.response/file-responseaby wygenerować odpowiedź; {:root "./static"}część informuje go, gdzie szukać pliku.

  5. (ANY "*" [] ...)- trasa uniwersalna. Dobrą praktyką Compojure jest zawsze umieszczanie takiej trasy na końcu defroutesformularza, aby mieć pewność, że definiowany program obsługi zawsze zwraca prawidłową mapę odpowiedzi Ring (pamiętaj, że wynikiem tego jest błąd dopasowania trasy nil).

Dlaczego w ten sposób?

Jednym z celów oprogramowania pośredniego Ring jest dodawanie informacji do mapy żądań; w ten sposób oprogramowanie pośredniczące do obsługi plików cookie dodaje :cookiesklucz do żądania, wrap-paramsdodaje:query-params i / lub:form-paramsjeśli ciąg zapytania / dane formularza są obecne i tak dalej. (Ściśle mówiąc, wszystkie informacje, które dodają funkcje oprogramowania pośredniego, muszą już znajdować się w mapie żądań, ponieważ to właśnie są przekazywane; ich zadaniem jest przekształcenie tego, aby wygodniej było pracować z opakowanymi przez siebie programami obsługi). Ostatecznie „wzbogacone” żądanie jest przekazywane do programu obsługi podstawowej, który sprawdza mapę żądań ze wszystkimi ładnie przetworzonymi informacjami dodanymi przez oprogramowanie pośredniczące i generuje odpowiedź. (Oprogramowanie pośredniczące może robić bardziej złożone rzeczy - takie jak pakowanie kilku "wewnętrznych" programów obsługi i wybieranie między nimi, decydowanie, czy w ogóle wywołać opakowane procedury obsługi itp. Jest to jednak poza zakresem tej odpowiedzi).

Z kolei program obsługi bazowej jest zwykle (w nietrywialnych przypadkach) funkcją, która zwykle potrzebuje tylko kilku informacji o żądaniu. (Np. ring.util.response/file-responseNie dba o większość żądania; potrzebuje tylko nazwy pliku). Stąd potrzeba prostego sposobu wyodrębnienia tylko odpowiednich części żądania Ring. Compojure ma na celu dostarczenie specjalnego mechanizmu dopasowywania wzorców, który właśnie to robi.

Michał Marczyk
źródło
3
„Dla dodatkowej wygody procedury obsługi zdefiniowane przez defroutes są niejawnie opakowane w parametry zawijania i pliki cookie zawijania”. - Od wersji 0.6.0 musisz dodać je jawnie. Ref. Github.com/weavejester/compojure/commit/…
Dan Midwood
3
Bardzo dobrze. Ta odpowiedź powinna znajdować się na stronie głównej Compojure.
Siddhartha Reddy
2
Lektura wymagana dla wszystkich nowych użytkowników Compojure. Życzę każdemu wiki i wpisowi na blogu na temat zaczynający się od linku do tego.
jemmons
7

Na booleanknot.com jest świetny artykuł Jamesa Reevesa (autora Compojure). Przeczytanie go sprawiło, że „kliknęło” dla mnie, więc przepisałem część z niego tutaj (tak naprawdę to wszystko, co zrobiłem).

Jest tu również slidedeck od tego samego autora , który odpowiada dokładnie na to pytanie.

Compojure jest oparty na Ring , który jest abstrakcją dla żądań http.

A concise syntax for generating Ring handlers.

Więc, co to za obsługa Ringów ? Wyciąg z dokumentu:

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

Całkiem proste, ale też dość niskopoziomowe. Powyższy program obsługi można zdefiniować bardziej zwięźle za pomocą ring/utilbiblioteki.

(use 'ring.util.response)

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

Teraz chcemy wywołać różne programy obsługi w zależności od żądania. Moglibyśmy zrobić taki statyczny routing:

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

I refaktoryzuj to w ten sposób:

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

Ciekawą rzeczą, na którą zwraca uwagę James, jest to, że umożliwia to zagnieżdżanie tras, ponieważ „rezultatem połączenia dwóch lub więcej tras jest sama trasa”.

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

Do tej pory zaczynamy widzieć kod, który wygląda tak, jakby można go było rozłożyć na czynniki przy użyciu makra. Compojure udostępnia defroutesmakro:

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

;; is identical to

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

Compojure udostępnia inne makra, takie jak GETmakro:

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

;; will expand to

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

Ta ostatnia wygenerowana funkcja wygląda jak nasz program obsługi!

Koniecznie sprawdź post Jamesa , ponieważ zawiera bardziej szczegółowe wyjaśnienia.

nha
źródło
4

Dla każdego, kto wciąż walczył, aby dowiedzieć się, co się dzieje z trasami, może być tak, że tak jak ja nie rozumiesz idei destrukcji.

Właściwie przeczytanie dokumentacjilet pomogło wyjaśnić całe „skąd się biorą magiczne wartości?” pytanie.

Wklejam odpowiednie sekcje poniżej:

Clojure obsługuje abstrakcyjne wiązanie strukturalne, często nazywane destrukturyzacją, w listach powiązań let, listach parametrów fn i wszelkich makrach, które rozwijają się do let lub fn. Podstawową ideą jest to, że forma-powiązania może być literałem struktury danych zawierającym symbole, które są powiązane z odpowiednimi częściami wyrażenia init. Wiązanie jest abstrakcyjne, ponieważ literał wektora może wiązać się z wszystkim, co jest sekwencyjne, podczas gdy literał mapy może wiązać się z wszystkim, co jest asocjacyjne.

Wyrażenia wiązania wektorów pozwalają na wiązanie nazw z częściami elementów sekwencyjnych (nie tylko wektorami), takimi jak wektory, listy, sekwencje, łańcuchy, tablice i wszystko, co obsługuje n-ty. Podstawową formą sekwencyjną jest wektor form wiążących, który będzie powiązany z kolejnymi elementami z wyrażenia init, wyszukanego przez nth. Dodatkowo i opcjonalnie &, po którym następują formy wiążące, spowoduje, że ta forma wiążąca zostanie związana z pozostałą częścią sekwencji, tj. Ta część, która jeszcze nie jest związana, wyszukana za pośrednictwem nthnext. Wreszcie, również opcjonalne,: po którym następuje symbol, spowoduje to, że ten symbol zostanie powiązany z całym wyrażeniem init:

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

Wyrażenia wiązania wektorów pozwalają na wiązanie nazw z częściami elementów sekwencyjnych (nie tylko wektorami), takimi jak wektory, listy, sekwencje, łańcuchy, tablice i wszystko, co obsługuje n-ty. Podstawową formą sekwencyjną jest wektor form wiążących, który będzie powiązany z kolejnymi elementami z wyrażenia init, wyszukanego przez nth. Dodatkowo i opcjonalnie &, po którym następują formy wiążące, spowoduje, że ta forma wiążąca zostanie związana z pozostałą częścią sekwencji, tj. Ta część, która jeszcze nie jest związana, wyszukana za pośrednictwem nthnext. Wreszcie, również opcjonalne,: po którym następuje symbol, spowoduje to, że ten symbol zostanie powiązany z całym wyrażeniem init:

(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]]
Pieter Breed
źródło
3

Nie zacząłem jeszcze od clojure internetowych rzeczy, ale, zrobię, oto rzeczy, które dodałem do zakładek.

nickik
źródło
Dzięki, te linki są zdecydowanie pomocne. Pracowałem nad tym problemem przez większą część dnia i jestem z nim w lepszym miejscu ... W pewnym momencie spróbuję opublikować podsumowanie.
Sean Woods
1

O co chodzi z destrukturyzacją ({form-params: form-params})? Jakie słowa kluczowe są dostępne dla mnie podczas destrukturyzacji?

Dostępne klucze to te, które znajdują się na mapie wejściowej. Destrukturyzacja jest dostępna w formularzach let i dawkiq lub wewnątrz parametrów do fn lub defn

Mamy nadzieję, że poniższy kod będzie zawierał wiele informacji:

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

bardziej zaawansowany przykład, pokazujący zagnieżdżoną destrukturyzację:

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

Rozsądnie stosowana destrukturyzacja porządkuje kod, unikając standardowego dostępu do danych. używając: as i drukując wynik (lub klucze wyniku), możesz lepiej zrozumieć, do jakich innych danych masz dostęp.

noisesmith
źródło