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.clj
znajduje 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:
Nadchodzi żądanie i jest przekształcane w mapę Clojure zgodnie ze specyfikacją Ring.
Ta mapa jest kierowana do tak zwanej „funkcji obsługi”, która ma wygenerować odpowiedź (która jest również mapą Clojure).
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-method
były :head
, a nie, odpowiedź byłaby nil
. Wrócimy do pytania o conil
Za chwilę znaczy (ale zauważ, że nie jest to poprawna odpowiedź na Ring!).
Jak widać z tego przykładu, example-route
jest to tylko funkcja, i to bardzo prosta; sprawdza żądanie, określa, czy jest zainteresowany jego obsługą (poprzez badanie :request-method
i :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/render
szczegółowe informacje można znaleźć w metodzie multimetrycznej (kod jest tutaj całkowicie samodokumentujący).
Spróbujmy defroutes
teraz użyć :
(defroutes example-routes
(GET "/" [] "get")
(HEAD "/" [] "head"))
Odpowiedzi na przykładowe żądanie wyświetlone powyżej i jego wariant z :request-method :head
są zgodne z oczekiwaniami.
Wewnętrzne działanie example-routes
jest takie, że każda trasa jest sprawdzana po kolei; jak tylko jeden z nich zwróci brak nil
odpowiedzi, ta odpowiedź staje się wartością zwracaną przez cały example-routes
program obsługi. Dla dodatkowej wygody defroutes
-definiowane procedury obsługi są zawijane wrap-params
i wrap-cookies
niejawnie.
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ć assoc
dodatkowe informacje na żądanie na etapie dopasowania:
(def echo-first-path-component-route
(GET "/:fst/*" [fst] fst))
Ten odpowiada, :body
z"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 :fst
w powyższym). Drugi to uproszczony sposób dopasowywania :params
wpisu 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ę:
(GET "/" [] (workbench))
- gdy mamy do czynienia z GET
żądaniem :uri "/"
, wywołaj funkcję workbench
i wyrenderuj wszystko, co zwraca, do mapy odpowiedzi. (Przypomnij sobie, że zwracana wartość może być mapą, ale także ciągiem znaków itp.)
(POST "/save" {form-params :form-params} (str form-params))
- :form-params
to wpis w mapie żądań dostarczonej przez wrap-params
oprogramowanie 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 ...)
(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"
.
(GET ["/:filename" :filename #".*"] [filename] ...)
- :filename #".*"
część nic nie robi (ponieważ #".*"
zawsze pasuje). Wywołuje funkcję narzędzia Ring, ring.util.response/file-response
aby wygenerować odpowiedź; {:root "./static"}
część informuje go, gdzie szukać pliku.
(ANY "*" [] ...)
- trasa uniwersalna. Dobrą praktyką Compojure jest zawsze umieszczanie takiej trasy na końcu defroutes
formularza, 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 :cookies
klucz do żądania, wrap-params
dodaje:query-params
i / lub:form-params
jeś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-response
Nie 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.