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:
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-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ę:
(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.)
(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 ...)
(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-responseaby wygenerować odpowiedź; {:root "./static"}część informuje go, gdzie szukać pliku.
(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.