Qual è la "grande idea" dietro le vie composte?


109

Sono nuovo di Clojure e ho utilizzato Compojure per scrivere un'applicazione web di base. Sto sbattendo contro un muro con la defroutessintassi di Compojure , però, e penso di aver bisogno di capire sia il "come" che il "perché" dietro a tutto questo.

Sembra che un'applicazione in stile Ring inizi con una mappa di richiesta HTTP, quindi passa semplicemente la richiesta attraverso una serie di funzioni middleware fino a quando non viene trasformata in una mappa di risposta, che viene rimandata al browser. Questo stile sembra troppo "di basso livello" per gli sviluppatori, quindi la necessità di uno strumento come Compojure. Riesco a vedere questo bisogno di più astrazioni anche in altri ecosistemi software, in particolare con WSGI di Python.

Il problema è che non capisco l'approccio di Compojure. Prendiamo la seguente defroutesespressione 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>"))

So che la chiave per capire tutto questo risiede in qualche macro voodoo, ma non capisco completamente le macro (ancora). Ho fissato la defroutesfonte per molto tempo, ma non capisco! Cosa sta succedendo qui? Capire la "grande idea" probabilmente mi aiuterà a rispondere a queste domande specifiche:

  1. Come si accede all'ambiente Ring dall'interno di una funzione instradata (es. La workbenchfunzione)? Ad esempio, diciamo che volevo accedere alle intestazioni HTTP_ACCEPT o qualche altra parte della richiesta / middleware?
  2. Qual è il problema con la destrutturazione ( {form-params :form-params})? Quali parole chiave sono disponibili per me durante la destrutturazione?

Mi piace molto Clojure ma sono così perplesso!

Risposte:


212

Compojure spiegato (in una certa misura)

NB. Sto lavorando con Compojure 0.4.1 ( qui il commit della versione 0.4.1 su GitHub).

Perché?

In cima compojure/core.clj, c'è questo utile riepilogo dello scopo di Compojure:

Una sintassi concisa per la generazione di gestori dell'anello.

A un livello superficiale, questo è tutto ciò che c'è da fare alla domanda "perché". Per andare un po 'più in profondità, diamo un'occhiata a come funziona un'app in stile Ring:

  1. Una richiesta arriva e viene trasformata in una mappa Clojure secondo le specifiche dell'anello.

  2. Questa mappa è incanalata in una cosiddetta "funzione gestore", che dovrebbe produrre una risposta (che è anche una mappa Clojure).

  3. La mappa di risposta viene trasformata in una risposta HTTP effettiva e rinviata al client.

Il passaggio 2. di cui sopra è il più interessante, in quanto è responsabilità del gestore esaminare l'URI utilizzato nella richiesta, esaminare eventuali cookie ecc. E infine arrivare a una risposta appropriata. Chiaramente è necessario che tutto questo lavoro sia scomposto in una raccolta di pezzi ben definiti; queste sono normalmente una funzione handler "base" e una raccolta di funzioni middleware che la racchiudono. Lo scopo di Compojure è semplificare la generazione della funzione del gestore di base.

Come?

Compojure si basa sulla nozione di "rotte". Questi sono effettivamente implementati a un livello più profondo dal Clout libreria (uno spinoff del progetto Compojure - molte cose sono state spostate in librerie separate alla transizione 0.3.x -> 0.4.x). Una rotta è definita da (1) un metodo HTTP (GET, PUT, HEAD ...), (2) un pattern URI (specificato con una sintassi che apparentemente sarà familiare ai Webby Rubyists), (3) una forma destrutturante usata in legare parti della mappa della richiesta ai nomi disponibili nel corpo, (4) un corpo di espressioni che necessita di produrre una risposta Ring valida (in casi non banali questa di solito è solo una chiamata a una funzione separata).

Questo potrebbe essere un buon punto per dare un'occhiata a un semplice esempio:

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

Proviamo questo al REPL (la mappa di richiesta di seguito è la mappa di richiesta Ring minima valida):

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

Se :request-methodfosse :headinvece, la risposta sarebbe nil. Torneremo alla domanda su cosa nilsignifichi qui tra un minuto (ma nota che non è un respose Ring valido!).

Come risulta evidente da questo esempio, example-routeè solo una funzione, e per di più molto semplice; esamina la richiesta, determina se è interessato a gestirla (esaminando :request-methode :uri) e, in tal caso, restituisce una mappa di risposta di base.

Ciò che è anche evidente è che il corpo del percorso non ha realmente bisogno di valutare una corretta mappa di risposta; Compojure fornisce una sana gestione predefinita per le stringhe (come visto sopra) e una serie di altri tipi di oggetti; vedere il compojure.response/rendermultimetodo per i dettagli (il codice è completamente auto-documentante qui).

Proviamo a utilizzare defroutesora:

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

Le risposte alla richiesta di esempio mostrata sopra e alla sua variante con :request-method :headsono come previste.

I meccanismi interni di example-routessono tali che ogni percorso viene provato a turno; non appena uno di loro restituisce una mancata nilrisposta, quella risposta diventa il valore di ritorno dell'intero example-routesgestore. Come ulteriore comodità, i defroutesgestori -defined sono inclusi wrap-paramse wrap-cookiesimplicitamente.

Ecco un esempio di percorso più complesso:

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

Notare la forma destrutturante al posto del vettore vuoto utilizzato in precedenza. L'idea di base qui è che il corpo del percorso potrebbe essere interessato ad alcune informazioni sulla richiesta; poiché questa arriva sempre sotto forma di mappa, può essere fornita una forma di destrutturazione associativa per estrarre informazioni dalla richiesta e legarle a variabili locali che saranno nell'ambito del corpo del percorso.

Una prova di quanto sopra:

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

La brillante idea di follow-up di quanto sopra è che percorsi più complessi possono assocaggiungere informazioni sulla richiesta nella fase di abbinamento:

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

Questo risponde con un :bodydi "foo"alla richiesta dell'esempio precedente.

Due cose sono nuove in questo ultimo esempio: il "/:fst/*"e il vettore di associazione non vuoto [fst]. Il primo è la suddetta sintassi simile a Rails e Sinatra per i pattern URI. È un po 'più sofisticato di quanto è evidente dall'esempio sopra in quanto sono supportati i vincoli di regex sui segmenti URI (ad esempio, ["/:fst/*" :fst #"[0-9]+"]può essere fornito per fare in modo che il percorso accetti solo valori di tutte le cifre di :fstsopra). Il secondo è un modo semplificato di abbinare la :paramsvoce nella mappa di richiesta, che è essa stessa una mappa; è utile per estrarre segmenti URI dalla richiesta, parametri della stringa di query e parametri del modulo. Un esempio per illustrare quest'ultimo punto:

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

Questo sarebbe un buon momento per dare un'occhiata all'esempio dal testo della domanda:

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

Analizziamo a turno ogni percorso:

  1. (GET "/" [] (workbench))- quando si ha a che fare con una GETrichiesta :uri "/", chiamare la funzione workbenche rendere tutto ciò che restituisce in una mappa di risposta. (Ricorda che il valore restituito potrebbe essere una mappa, ma anche una stringa ecc.)

  2. (POST "/save" {form-params :form-params} (str form-params))- :form-paramsè una voce nella mappa delle richieste fornita dal wrap-paramsmiddleware (ricorda che è implicitamente inclusa da defroutes). La risposta sarà lo standard {:status 200 :headers {"Content-Type" "text/html"} :body ...}con (str form-params)sostituito .... (Un POSTgestore leggermente insolito , questo ...)

  3. (GET "/test" [& more] (str "<pre> more "</pre>"))- questo, ad esempio, farebbe eco alla rappresentazione della stringa della mappa {"foo" "1"}se il programma utente lo richiede "/test?foo=1".

  4. (GET ["/:filename" :filename #".*"] [filename] ...)- la :filename #".*"parte non fa nulla (poiché #".*"corrisponde sempre). Chiama la funzione di utilità Ring ring.util.response/file-responseper produrre la sua risposta; la {:root "./static"}parte gli dice dove cercare il file.

  5. (ANY "*" [] ...)- un percorso universale. È buona pratica Compojure includere sempre tale route alla fine di un defroutesmodulo per garantire che il gestore che viene definito restituisca sempre una mappa di risposta Ring valida (ricorda che si verifica un errore di corrispondenza del percorso nil).

Perché in questo modo?

Uno degli scopi del middleware Ring è aggiungere informazioni alla mappa delle richieste; quindi il middleware di gestione dei cookie aggiunge una :cookieschiave alla richiesta, wrap-paramsaggiunge :query-paramse / o:form-paramsse è presente una stringa di query / dati del modulo e così via. (A rigor di termini, tutte le informazioni che le funzioni middleware stanno aggiungendo devono essere già presenti nella mappa delle richieste, poiché è ciò che vengono passate; il loro compito è trasformarle in modo che sia più conveniente lavorare con i gestori che racchiudono.) Alla fine la richiesta "arricchita" viene passata al gestore di base, che esamina la mappa della richiesta con tutte le informazioni ben preelaborate aggiunte dal middleware e produce una risposta. (Il middleware può fare cose più complesse di così, come avvolgere diversi gestori "interni" e scegliere tra di loro, decidere se chiamare o meno i gestori avvolti, ecc. Ciò è, tuttavia, al di fuori dello scopo di questa risposta.)

Il gestore di base, a sua volta, è solitamente (in casi non banali) una funzione che tende a richiedere solo una manciata di elementi di informazione sulla richiesta. (Ad esempio, ring.util.response/file-responsenon si preoccupa della maggior parte della richiesta; ha solo bisogno di un nome di file.) Da qui la necessità di un modo semplice per estrarre solo le parti rilevanti di una richiesta Ring. Compojure mira a fornire un motore di corrispondenza dei modelli per scopi speciali, per così dire, che fa proprio questo.


3
"Come ulteriore comodità, i gestori defroutes sono inclusi implicitamente in wrap-params e wrap-cookies". - A partire dalla versione 0.6.0 è necessario aggiungerli esplicitamente. Ref github.com/weavejester/compojure/commit/…
Dan Midwood

3
Molto ben messo. Questa risposta dovrebbe essere sulla homepage di Compojure.
Siddhartha Reddy,

2
Lettura richiesta per chiunque non conosca Compojure. Vorrei che ogni wiki e post sul blog sull'argomento iniziasse con un collegamento a questo.
jemmons

7

C'è un eccellente articolo su booleanknot.com di James Reeves (autore di Compojure), e leggerlo mi ha fatto "clic", quindi ne ho riscritto parte qui (in realtà è tutto quello che ho fatto).

C'è anche uno slidedeck qui dello stesso autore , che risponde a questa domanda esatta.

Compojure è basato su Ring , che è un'astrazione per le richieste http.

A concise syntax for generating Ring handlers.

Allora, cosa sono quei gestori dell'anello ? Estratto dal documento:

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

Abbastanza semplice, ma anche piuttosto di basso livello. Il gestore di cui sopra può essere definito in modo più conciso utilizzando la ring/utillibreria.

(use 'ring.util.response)

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

Ora vogliamo chiamare diversi gestori a seconda della richiesta. Potremmo fare un routing statico in questo modo:

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

E il refactoring in questo modo:

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

La cosa interessante che James nota allora è che questo permette di nidificare le rotte, perché "il risultato della combinazione di due o più rotte insieme è essa stessa una rotta".

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

A questo punto, stiamo iniziando a vedere del codice che sembra possa essere scomposto, utilizzando una macro. Compojure fornisce una defroutesmacro:

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

;; is identical to

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

Compojure fornisce altre macro, come la GETmacro:

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

;; will expand to

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

L'ultima funzione generata assomiglia al nostro gestore!

Assicurati di controllare il post di James , in quanto fornisce spiegazioni più dettagliate.


4

Per chiunque abbia ancora faticato a scoprire cosa sta succedendo con le vie, potrebbe essere che, come me, non capisci l'idea della destrutturazione.

In realtà leggere i documenti perlet aiutare a chiarire tutto "da dove vengono i valori magici?" domanda.

Sto incollando le sezioni pertinenti di seguito:

Clojure supporta il binding strutturale astratto, spesso chiamato destrutturazione, negli elenchi di binding let, negli elenchi di parametri fn e in qualsiasi macro che si espande in let o fn. L'idea di base è che un binding-form può essere una struttura dati letterale contenente simboli che vengono associati alle rispettive parti di init-expr. Il legame è astratto in quanto un letterale vettoriale può legarsi a tutto ciò che è sequenziale, mentre un letterale mappa può legarsi a tutto ciò che è associativo.

Vector binding-exprs ti consente di associare nomi a parti di cose sequenziali (non solo vettori), come vettori, elenchi, sequenze, stringhe, array e qualsiasi cosa che supporti nth. La forma sequenziale di base è un vettore di forme vincolanti, che saranno legate a elementi successivi da init-expr, cercati tramite nth. Inoltre, e opzionalmente, & seguito da una forma di legame farà sì che quella forma di legame sia legata al resto della sequenza, cioè quella parte non ancora legata, cercata via nthnext. Infine, anche facoltativo,: se seguito da un simbolo, quel simbolo sarà associato all'intero init-expr:

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

Vector binding-exprs ti consente di associare nomi a parti di cose sequenziali (non solo vettori), come vettori, elenchi, sequenze, stringhe, array e qualsiasi cosa che supporti nth. La forma sequenziale di base è un vettore di forme vincolanti, che saranno legate a elementi successivi da init-expr, cercati tramite nth. Inoltre, e opzionalmente, & seguito da una forma di legame farà sì che quella forma di legame sia legata al resto della sequenza, cioè quella parte non ancora legata, cercata via nthnext. Infine, anche facoltativo,: se seguito da un simbolo, quel simbolo sarà associato all'intero init-expr:

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

3

Grazie, questi link sono decisamente utili. Ho lavorato su questo problema per la parte migliore della giornata e sono in un posto migliore con esso ... cercherò di pubblicare un follow-up ad un certo punto.
Sean Woods

1

Qual è il problema con la destrutturazione ({form-params: form-params})? Quali parole chiave sono disponibili per me durante la destrutturazione?

Le chiavi disponibili sono quelle che si trovano nella mappa di input. La destrutturazione è disponibile nelle forme let e doseq, oppure nei parametri fn o defn

Si spera che il codice seguente sia informativo:

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

un esempio più avanzato, che mostra la destrutturazione annidata:

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

Se usata con saggezza, la destrutturazione elimina il codice evitando l'accesso ai dati boilerplate. usando: as e stampando il risultato (o le chiavi del risultato) puoi avere un'idea migliore di quali altri dati potresti accedere.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.