Was ist die „große Idee“ hinter Compojure-Routen?

109

Ich bin neu in Clojure und habe Compojure verwendet, um eine grundlegende Webanwendung zu schreiben. Ich stoße jedoch mit der defroutesSyntax von Compojure an eine Wand und denke, ich muss sowohl das "Wie" als auch das "Warum" dahinter verstehen.

Es scheint, als würde eine Anwendung im Ring-Stil mit einer HTTP-Anforderungszuordnung beginnen und die Anforderung dann einfach durch eine Reihe von Middleware-Funktionen weiterleiten, bis sie in eine Antwortzuordnung umgewandelt wird, die an den Browser zurückgesendet wird. Dieser Stil scheint für Entwickler zu "niedrig" zu sein, weshalb ein Tool wie Compojure erforderlich ist. Ich kann diesen Bedarf an mehr Abstraktionen auch in anderen Software-Ökosystemen erkennen, insbesondere bei Pythons WSGI.

Das Problem ist, dass ich den Ansatz von Compojure nicht verstehe. Nehmen wir den folgenden defroutesS-Ausdruck:

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

Ich weiß, dass der Schlüssel zum Verständnis all dessen in einem Makro-Voodoo liegt, aber ich verstehe Makros (noch) nicht vollständig. Ich habe lange auf die defroutesQuelle gestarrt , aber verstehe es einfach nicht! Was ist denn hier los? Das Verständnis der "großen Idee" wird mir wahrscheinlich helfen, diese spezifischen Fragen zu beantworten:

  1. Wie greife ich von einer gerouteten Funktion (z. B. der workbenchFunktion) auf die Ring-Umgebung zu ? Angenommen, ich wollte auf die HTTP_ACCEPT-Header oder einen anderen Teil der Anfrage / Middleware zugreifen.
  2. Was ist mit der Destrukturierung ( {form-params :form-params}) los? Welche Keywords stehen mir bei der Destrukturierung zur Verfügung?

Ich mag Clojure wirklich, aber ich bin so ratlos!

Sean Woods
quelle

Antworten:

212

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.cljsteht 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:

  1. Eine Anfrage kommt an und wird gemäß der Ring-Spezifikation in eine Clojure-Karte umgewandelt.

  2. Diese Karte wird in eine sogenannte "Handler-Funktion" geleitet, von der erwartet wird, dass sie eine Antwort erzeugt (die auch eine Clojure-Karte ist).

  3. 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-methodwaren :headstattdessen 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-routehandelt 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-methodund :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/renderEinzelheiten finden Sie in der Multimethode (der Code ist hier vollständig selbstdokumentierend).

Versuchen wir es defroutesjetzt:

(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-routesist 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-routesHandlers. Als zusätzliche Annehmlichkeit werden defroutesdefinierte Handler in wrap-paramsund 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 associn 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 :bodyvon "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 :fstoben akzeptiert ). Die zweite ist eine vereinfachte Methode zum Abgleichen des :paramsEintrags 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:

  1. (GET "/" [] (workbench))- Wenn Sie eine GETAnfrage mit bearbeiten :uri "/", rufen Sie die Funktion auf workbenchund 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.)

  2. (POST "/save" {form-params :form-params} (str form-params))- :form-paramsist ein Eintrag in der Anforderungszuordnung, die von der wrap-paramsMiddleware 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 POSTHandler, dieser ...)

  3. (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".

  4. (GET ["/:filename" :filename #".*"] [filename] ...)- Der :filename #".*"Teil macht überhaupt nichts (da #".*"immer übereinstimmt). Es ruft das Dienstprogramm Ring ring.util.response/file-responseauf, um seine Antwort zu erzeugen. Der {:root "./static"}Teil gibt an, wo nach der Datei gesucht werden soll.

  5. (ANY "*" [] ...)- eine Sammelroute. Es ist eine gute Compojure-Praxis, eine solche Route immer am Ende eines defroutesFormulars 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 :cookiesfür die Cookie-Verarbeitung der Anforderung einen Schlüssel wrap-paramshinzu , fügt hinzu :query-paramsund / oder:form-paramswenn 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-responsekü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.

Michał Marczyk
quelle
3
"Als zusätzliche Annehmlichkeit werden Defroutes-definierte Handler implizit in Wrap-Parameter und Wrap-Cookies eingeschlossen." - Ab Version 0.6.0 müssen Sie diese explizit hinzufügen. Ref github.com/weavejester/compojure/commit/…
Dan Midwood
3
Sehr gut ausgedrückt. Diese Antwort sollte auf der Homepage von Compojure sein.
Siddhartha Reddy
2
Erforderliche Lektüre für alle, die neu bei Compojure sind. Ich wünschte, jeder Wiki- und Blog-Beitrag zum Thema würde mit einem Link dazu beginnen.
Jemmons
7

Es gibt einen ausgezeichneten Artikel bei booleanknot.com von James Reeves (Autor von Compojure), und das Lesen hat es für mich zum "Klicken" gebracht, daher habe ich einige davon hier neu transkribiert (das ist wirklich alles, was ich getan habe).

Es gibt auch ein Slidedeck des gleichen Autors , das genau diese Frage beantwortet.

Compojure basiert auf Ring , einer Abstraktion für http-Anfragen.

A concise syntax for generating Ring handlers.

Also, was sind diese Ringhandler ? Auszug aus dem Dokument:

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

Ziemlich einfach, aber auch ziemlich niedrig. Der obige Handler kann mithilfe der ring/utilBibliothek präziser definiert werden .

(use 'ring.util.response)

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

Jetzt wollen wir je nach Anfrage verschiedene Handler aufrufen. Wir könnten statisches Routing wie folgt durchführen:

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

Und umgestalten Sie es so:

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

Das Interessante, das James dann bemerkt, ist, dass dies Verschachtelungsrouten ermöglicht, weil "das Ergebnis der Kombination von zwei oder mehr Routen selbst eine Route ist".

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

Inzwischen sehen wir Code, der mithilfe eines Makros berücksichtigt werden könnte. Compojure bietet ein defroutesMakro:

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

;; is identical to

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

Compojure bietet andere Makros wie das GETMakro:

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

;; will expand to

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

Diese zuletzt generierte Funktion sieht aus wie unser Handler!

Bitte lesen Sie unbedingt den James-Beitrag , da er ausführlichere Erklärungen enthält.

nha
quelle
4

Für jeden, der immer noch Schwierigkeiten hatte herauszufinden, was mit den Routen los ist, könnte es sein, dass Sie wie ich die Idee der Destrukturierung nicht verstehen.

Das Lesen der Dokumente fürlet half, das Ganze zu klären: "Woher kommen die magischen Werte?" Frage.

Ich füge die relevanten Abschnitte unten ein:

Clojure unterstützt abstrakte strukturelle Bindungen, die oft als Destrukturierung bezeichnet werden, in Let-Bindungslisten, Fn-Parameterlisten und allen Makros, die zu Let oder Fn erweitert werden. Die Grundidee ist, dass eine Bindungsform ein Datenstrukturliteral sein kann, das Symbole enthält, die an die jeweiligen Teile des init-Ausdrucks gebunden werden. Die Bindung ist insofern abstrakt, als ein Vektorliteral an alles sequentielle binden kann, während ein Kartenliteral an alles assoziieren kann, was assoziativ ist.

Mit Vector Binding-Exprs können Sie Namen an Teile sequentieller Dinge (nicht nur an Vektoren) binden, z. B. an Vektoren, Listen, Seqs, Strings, Arrays und alles, was nth unterstützt. Die grundlegende sequentielle Form ist ein Vektor von Bindungsformen, der an aufeinanderfolgende Elemente aus dem init-Ausdruck gebunden wird, der über n-ter nachgeschlagen wird. Zusätzlich und optional wird & gefolgt von einer Bindungsform dazu führen, dass diese Bindungsform an den Rest der Sequenz gebunden wird, dh dass der noch nicht gebundene Teil über nthnext nachgeschlagen wird. Schließlich auch optional: Wenn ein Symbol folgt, wird dieses Symbol an den gesamten init-Ausdruck gebunden:

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

Mit Vector Binding-Exprs können Sie Namen an Teile sequentieller Dinge (nicht nur an Vektoren) binden, z. B. an Vektoren, Listen, Seqs, Strings, Arrays und alles, was nth unterstützt. Die grundlegende sequentielle Form ist ein Vektor von Bindungsformen, der an aufeinanderfolgende Elemente aus dem init-Ausdruck gebunden wird, der über n-ter nachgeschlagen wird. Zusätzlich und optional wird & gefolgt von einer Bindungsform dazu führen, dass diese Bindungsform an den Rest der Sequenz gebunden wird, dh dass der noch nicht gebundene Teil über nthnext nachgeschlagen wird. Schließlich auch optional: Wenn ein Symbol folgt, wird dieses Symbol an den gesamten init-Ausdruck gebunden:

(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
quelle
3

Ich habe noch nicht mit Clojure-Web-Sachen angefangen, aber ich werde hier die Sachen, die ich mit einem Lesezeichen versehen habe.

Nickik
quelle
Danke, diese Links sind auf jeden Fall hilfreich. Ich habe den größten Teil des Tages an diesem Problem gearbeitet und bin damit an einem besseren Ort ... Ich werde versuchen, irgendwann ein Follow-up zu veröffentlichen.
Sean Woods
1

Was ist mit der Destrukturierung los ({form-params: form-params})? Welche Keywords stehen mir bei der Destrukturierung zur Verfügung?

Die verfügbaren Schlüssel befinden sich in der Eingabekarte. Die Destrukturierung ist in let- und doseq-Formularen oder innerhalb der Parameter für fn oder defn verfügbar

Der folgende Code wird hoffentlich informativ sein:

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

Ein fortgeschritteneres Beispiel, das verschachtelte Destrukturierung zeigt:

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

Wenn Sie es mit Bedacht einsetzen, wird Ihr Code durch die Destrukturierung dekodiert, indem der Datenzugriff auf Boilerplate vermieden wird. Wenn Sie: as verwenden und das Ergebnis (oder die Schlüssel des Ergebnisses) drucken, erhalten Sie eine bessere Vorstellung davon, auf welche anderen Daten Sie zugreifen können.

Geräuschschmied
quelle