Compojure erklärt (bis zu einem gewissen Grad)
NB. Ich arbeite mit Compojure 0.4.1 ( hier ist das Release-Commit von 0.4.1 für GitHub).
Warum?
Ganz oben compojure/core.clj
steht diese hilfreiche Zusammenfassung des Zwecks von Compojure:
Eine übersichtliche Syntax zum Generieren von Ring-Handlern.
Auf oberflächlicher Ebene ist das alles, was es zur "Warum" -Frage gibt. Um etwas tiefer zu gehen, schauen wir uns an, wie eine Ring-App funktioniert:
Eine Anfrage kommt an und wird gemäß der Ring-Spezifikation in eine Clojure-Karte umgewandelt.
Diese Karte wird in eine sogenannte "Handler-Funktion" geleitet, von der erwartet wird, dass sie eine Antwort erzeugt (die auch eine Clojure-Karte ist).
Die Antwortzuordnung wird in eine tatsächliche HTTP-Antwort umgewandelt und an den Client zurückgesendet.
Schritt 2. oben ist am interessantesten, da es in der Verantwortung des Handlers liegt, die in der Anfrage verwendete URI zu überprüfen, Cookies usw. zu untersuchen und letztendlich zu einer angemessenen Antwort zu gelangen. Es ist klar, dass all diese Arbeiten in eine Sammlung klar definierter Stücke einbezogen werden müssen. Dies sind normalerweise eine "Basis" -Handlerfunktion und eine Sammlung von Middleware-Funktionen, die sie umschließen. Der Zweck von Compojure besteht darin, die Generierung der Base-Handler-Funktion zu vereinfachen.
Wie?
Compojure basiert auf dem Begriff "Routen". Diese werden tatsächlich auf einer tieferen Ebene von der Clout- Bibliothek implementiert (ein Spin-off des Compojure-Projekts - viele Dinge wurden beim Übergang 0.3.x -> 0.4.x in separate Bibliotheken verschoben). Eine Route wird definiert durch (1) eine HTTP-Methode (GET, PUT, HEAD ...), (2) ein URI-Muster (angegeben mit einer Syntax, die Webby Rubyists anscheinend bekannt ist), (3) eine Destrukturierungsform, die in verwendet wird Binden von Teilen der Anforderungszuordnung an im Hauptteil verfügbare Namen, (4) eine Reihe von Ausdrücken, die eine gültige Ringantwort erzeugen müssen (in nicht trivialen Fällen ist dies normalerweise nur ein Aufruf einer separaten Funktion).
Dies könnte ein guter Punkt sein, um ein einfaches Beispiel zu betrachten:
(def example-route (GET "/" [] "<html>...</html>"))
Lassen Sie uns dies an der REPL testen (die Anforderungskarte unten ist die minimal gültige Ringanforderungskarte):
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>"}
Wenn :request-method
waren :head
stattdessen würde die Antwort sein nil
. Wir werden auf die Frage zurückkommen, wasnil
hier bedeutet (aber beachten Sie, dass es sich nicht um eine gültige Ring-Antwort handelt!).
Wie aus diesem Beispiel hervorgeht, example-route
handelt es sich nur um eine Funktion, und zwar um eine sehr einfache; Es prüft die Anfrage, bestimmt, ob es daran interessiert ist, sie zu bearbeiten (indem es prüft :request-method
und :uri
), und gibt in diesem Fall eine grundlegende Antwortzuordnung zurück.
Was auch offensichtlich ist, ist, dass der Körper der Route nicht wirklich zu einer richtigen Antwortkarte ausgewertet werden muss; Compojure bietet eine vernünftige Standardbehandlung für Zeichenfolgen (wie oben dargestellt) und eine Reihe anderer Objekttypen. compojure.response/render
Einzelheiten finden Sie in der Multimethode (der Code ist hier vollständig selbstdokumentierend).
Versuchen wir es defroutes
jetzt:
(defroutes example-routes
(GET "/" [] "get")
(HEAD "/" [] "head"))
Die Antworten auf die oben angezeigte Beispielanforderung und auf ihre Variante mit :request-method :head
sind wie erwartet.
Das Innenleben von example-routes
ist so, dass jede Route der Reihe nach versucht wird; Sobald einer von ihnen eine Nichtantwort zurückgibt nil
, wird diese Antwort zum Rückgabewert des gesamten example-routes
Handlers. Als zusätzliche Annehmlichkeit werden defroutes
definierte Handler in wrap-params
und eingeschlossenwrap-cookies
implizit.
Hier ist ein Beispiel für eine komplexere Route:
(def echo-typed-url-route
(GET "*" {:keys [scheme server-name server-port uri]}
(str (name scheme) "://" server-name ":" server-port uri)))
Beachten Sie das Destrukturierungsformular anstelle des zuvor verwendeten leeren Vektors. Die Grundidee hier ist, dass der Hauptteil der Route an einigen Informationen über die Anfrage interessiert sein könnte; Da dies immer in Form einer Karte ankommt, kann ein assoziatives Destrukturierungsformular bereitgestellt werden, um Informationen aus der Anforderung zu extrahieren und sie an lokale Variablen zu binden, die im Umfang der Route enthalten sind.
Ein Test der oben genannten:
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"}
Die brillante Folgeidee zu dem oben Gesagten ist, dass komplexere Routen assoc
in der Matching-Phase zusätzliche Informationen zu der Anfrage enthalten können:
(def echo-first-path-component-route
(GET "/:fst/*" [fst] fst))
Dies antwortet mit einem :body
von "foo"
auf die Anfrage aus dem vorherigen Beispiel.
Bei diesem neuesten Beispiel sind zwei Dinge neu: der "/:fst/*"
und der nicht leere Bindungsvektor [fst]
. Die erste ist die oben erwähnte Rails-and-Sinatra-ähnliche Syntax für URI-Muster. Es ist etwas ausgefeilter als aus dem obigen Beispiel ersichtlich, dass Regex-Einschränkungen für URI-Segmente unterstützt werden (z. B. ["/:fst/*" :fst #"[0-9]+"]
kann angegeben werden, damit die Route nur alle Ziffernwerte von :fst
oben akzeptiert ). Die zweite ist eine vereinfachte Methode zum Abgleichen des :params
Eintrags in der Anforderungskarte, die selbst eine Karte ist. Es ist nützlich, um URI-Segmente aus der Anforderung zu extrahieren, Zeichenfolgenparameter und Formularparameter abzufragen. Ein Beispiel zur Veranschaulichung des letzteren Punktes:
(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\"}"}
Dies wäre ein guter Zeitpunkt, um sich das Beispiel aus dem Fragentext anzusehen:
(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>"))
Lassen Sie uns nacheinander jede Route analysieren:
(GET "/" [] (workbench))
- Wenn Sie eine GET
Anfrage mit bearbeiten :uri "/"
, rufen Sie die Funktion auf workbench
und rendern Sie alles, was sie zurückgibt, in eine Antwortzuordnung. (Denken Sie daran, dass der Rückgabewert eine Karte, aber auch eine Zeichenfolge usw. sein kann.)
(POST "/save" {form-params :form-params} (str form-params))
- :form-params
ist ein Eintrag in der Anforderungszuordnung, die von der wrap-params
Middleware bereitgestellt wird (denken Sie daran, dass er implizit von enthalten ist defroutes
). Die Antwort ist der Standard {:status 200 :headers {"Content-Type" "text/html"} :body ...}
mit (str form-params)
ersetzt ...
. (Ein etwas ungewöhnlicher POST
Handler, dieser ...)
(GET "/test" [& more] (str "<pre> more "</pre>"))
- Dies würde z. B. die Zeichenfolgendarstellung der Karte zurückgeben, {"foo" "1"}
wenn der Benutzeragent danach fragt "/test?foo=1"
.
(GET ["/:filename" :filename #".*"] [filename] ...)
- Der :filename #".*"
Teil macht überhaupt nichts (da #".*"
immer übereinstimmt). Es ruft das Dienstprogramm Ring ring.util.response/file-response
auf, um seine Antwort zu erzeugen. Der {:root "./static"}
Teil gibt an, wo nach der Datei gesucht werden soll.
(ANY "*" [] ...)
- eine Sammelroute. Es ist eine gute Compojure-Praxis, eine solche Route immer am Ende eines defroutes
Formulars einzufügen, um sicherzustellen, dass der zu definierende Handler immer eine gültige Ringantwortkarte zurückgibt (denken Sie daran, dass ein Fehler bei der Routenübereinstimmung dazu führt nil
).
Warum so?
Ein Zweck der Ring-Middleware besteht darin, der Anforderungszuordnung Informationen hinzuzufügen. Somit fügt die Middleware :cookies
für die Cookie-Verarbeitung der Anforderung einen Schlüssel wrap-params
hinzu , fügt hinzu :query-params
und / oder:form-params
wenn eine Abfragezeichenfolge / Formulardaten vorhanden sind und so weiter. (Genau genommen müssen alle Informationen, die die Middleware-Funktionen hinzufügen, bereits in der Anforderungszuordnung vorhanden sein, da diese übergeben werden. Ihre Aufgabe besteht darin, sie so zu transformieren, dass sie in den von ihnen verpackten Handlern bequemer verarbeitet werden können.) Letztendlich wird die "angereicherte" Anforderung an den Basishandler übergeben, der die Anforderungszuordnung mit allen gut vorverarbeiteten Informationen untersucht, die von der Middleware hinzugefügt wurden, und eine Antwort erzeugt. (Middleware kann komplexere Dinge als das tun - wie das Umschließen mehrerer "innerer" Handler und die Auswahl zwischen ihnen, die Entscheidung, ob die umschlossenen Handler überhaupt aufgerufen werden sollen usw. Dies liegt jedoch außerhalb des Rahmens dieser Antwort.)
Der Basishandler wiederum ist normalerweise (in nicht trivialen Fällen) eine Funktion, die dazu neigt, nur eine Handvoll Informationen über die Anforderung zu benötigen. (ZB ring.util.response/file-response
kümmert sich der Großteil der Anfrage nicht darum; es wird nur ein Dateiname benötigt.) Daher ist eine einfache Methode erforderlich, um nur die relevanten Teile einer Ring-Anfrage zu extrahieren. Compojure zielt darauf ab, sozusagen eine spezielle Pattern-Matching-Engine bereitzustellen, die genau das tut.