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:
Una richiesta arriva e viene trasformata in una mappa Clojure secondo le specifiche dell'anello.
Questa mappa è incanalata in una cosiddetta "funzione gestore", che dovrebbe produrre una risposta (che è anche una mappa Clojure).
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-method
fosse :head
invece, la risposta sarebbe nil
. Torneremo alla domanda su cosa nil
significhi 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-method
e :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/render
multimetodo per i dettagli (il codice è completamente auto-documentante qui).
Proviamo a utilizzare defroutes
ora:
(defroutes example-routes
(GET "/" [] "get")
(HEAD "/" [] "head"))
Le risposte alla richiesta di esempio mostrata sopra e alla sua variante con :request-method :head
sono come previste.
I meccanismi interni di example-routes
sono tali che ogni percorso viene provato a turno; non appena uno di loro restituisce una mancata nil
risposta, quella risposta diventa il valore di ritorno dell'intero example-routes
gestore. Come ulteriore comodità, i defroutes
gestori -defined sono inclusi wrap-params
e wrap-cookies
implicitamente.
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 assoc
aggiungere informazioni sulla richiesta nella fase di abbinamento:
(def echo-first-path-component-route
(GET "/:fst/*" [fst] fst))
Questo risponde con un :body
di "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 :fst
sopra). Il secondo è un modo semplificato di abbinare la :params
voce 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:
(GET "/" [] (workbench))
- quando si ha a che fare con una GET
richiesta :uri "/"
, chiamare la funzione workbench
e rendere tutto ciò che restituisce in una mappa di risposta. (Ricorda che il valore restituito potrebbe essere una mappa, ma anche una stringa ecc.)
(POST "/save" {form-params :form-params} (str form-params))
- :form-params
è una voce nella mappa delle richieste fornita dal wrap-params
middleware (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 POST
gestore leggermente insolito , questo ...)
(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"
.
(GET ["/:filename" :filename #".*"] [filename] ...)
- la :filename #".*"
parte non fa nulla (poiché #".*"
corrisponde sempre). Chiama la funzione di utilità Ring ring.util.response/file-response
per produrre la sua risposta; la {:root "./static"}
parte gli dice dove cercare il file.
(ANY "*" [] ...)
- un percorso universale. È buona pratica Compojure includere sempre tale route alla fine di un defroutes
modulo 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 :cookies
chiave alla richiesta, wrap-params
aggiunge :query-params
e / o:form-params
se è 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-response
non 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.