Compojure explicado (hasta cierto punto)
NÓTESE BIEN. Estoy trabajando con Compojure 0.4.1 ( aquí está el compromiso de lanzamiento 0.4.1 en GitHub).
¿Por qué?
En la parte superior de compojure/core.clj
, se encuentra este útil resumen del propósito de Compojure:
Una sintaxis concisa para generar controladores Ring.
En un nivel superficial, eso es todo lo que hay en la pregunta del "por qué". Para profundizar un poco más, veamos cómo funciona una aplicación de estilo Ring:
Llega una solicitud y se transforma en un mapa Clojure de acuerdo con la especificación Ring.
Este mapa se canaliza a una llamada "función de controlador", que se espera que produzca una respuesta (que también es un mapa de Clojure).
El mapa de respuesta se transforma en una respuesta HTTP real y se envía de vuelta al cliente.
El paso 2 de lo anterior es el más interesante, ya que es responsabilidad del administrador examinar el URI utilizado en la solicitud, examinar las cookies, etc. y, finalmente, llegar a una respuesta adecuada. Es evidente que es necesario que todo este trabajo se incluya en una colección de piezas bien definidas; normalmente son una función de controlador "base" y una colección de funciones de middleware que la envuelven. El propósito de Compojure es simplificar la generación de la función del controlador base.
¿Cómo?
Compojure se basa en la noción de "rutas". En realidad, estos se implementan a un nivel más profundo mediante la biblioteca Clout (un derivado del proyecto Compojure; muchas cosas se movieron a bibliotecas separadas en la transición 0.3.x -> 0.4.x). Una ruta se define por (1) un método HTTP (GET, PUT, HEAD ...), (2) un patrón URI (especificado con una sintaxis que aparentemente será familiar para Webby Rubyists), (3) una forma de desestructuración utilizada en vincular partes del mapa de solicitud a los nombres disponibles en el cuerpo, (4) un cuerpo de expresiones que necesita producir una respuesta Ring válida (en casos no triviales, esto suele ser solo una llamada a una función separada).
Este podría ser un buen punto para echar un vistazo a un ejemplo simple:
(def example-route (GET "/" [] "<html>...</html>"))
Probemos esto en el REPL (el mapa de solicitud a continuación es el mapa de solicitud de anillo válido mínimo):
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>"}
Si :request-method
fuera en :head
cambio, la respuesta sería nil
. Volveremos a la pregunta de qué nil
significa aquí en un minuto (¡pero observe que no es una respuesta válida de Ring!).
Como se desprende de este ejemplo, example-route
es solo una función, y además muy simple; examina la solicitud, determina si está interesado en manejarla (examinando :request-method
y :uri
) y, de ser así, devuelve un mapa de respuesta básico.
Lo que también es evidente es que el cuerpo de la ruta no necesita realmente evaluarse para obtener un mapa de respuesta adecuado; Compojure proporciona un manejo sano por defecto para cadenas (como se vio arriba) y una serie de otros tipos de objetos; consulte el compojure.response/render
método múltiple para obtener más detalles (el código es completamente autodocumentado aquí).
Intentemos usar defroutes
ahora:
(defroutes example-routes
(GET "/" [] "get")
(HEAD "/" [] "head"))
Las respuestas a la solicitud de ejemplo que se muestra arriba y a su variante con :request-method :head
son las esperadas.
El funcionamiento interno de example-routes
es tal que cada ruta se prueba por turno; tan pronto como uno de ellos devuelve una falta de nil
respuesta, esa respuesta se convierte en el valor de retorno de todo el example-routes
controlador. Para mayor comodidad, defroutes
los manipuladores -definida están envueltos en wrap-params
e wrap-cookies
implícitamente.
A continuación, se muestra un ejemplo de una ruta más compleja:
(def echo-typed-url-route
(GET "*" {:keys [scheme server-name server-port uri]}
(str (name scheme) "://" server-name ":" server-port uri)))
Tenga en cuenta la forma de desestructuración en lugar del vector vacío utilizado anteriormente. La idea básica aquí es que el cuerpo de la ruta podría estar interesado en alguna información sobre la solicitud; dado que este siempre llega en forma de mapa, se puede proporcionar un formulario de desestructuración asociativa para extraer información de la solicitud y vincularla a variables locales que estarán dentro del alcance en el cuerpo de la ruta.
Una prueba de lo anterior:
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 de seguimiento de lo anterior es que las rutas más complejas pueden assoc
agregar información adicional a la solicitud en la etapa de coincidencia:
(def echo-first-path-component-route
(GET "/:fst/*" [fst] fst))
Esto responde con una :body
de "foo"
a la solicitud del ejemplo anterior.
Hay dos cosas nuevas en este último ejemplo: el "/:fst/*"
y el vector de enlace no vacío [fst]
. El primero es la sintaxis similar a Rails y Sinatra para patrones URI antes mencionada. Es un poco más sofisticado de lo que es evidente en el ejemplo anterior, ya que se admiten las restricciones de expresiones regulares en los segmentos de URI (por ejemplo, ["/:fst/*" :fst #"[0-9]+"]
se puede proporcionar para que la ruta acepte solo valores de todos los dígitos de :fst
lo anterior). La segunda es una forma simplificada de hacer coincidir la :params
entrada en el mapa de solicitud, que en sí mismo es un mapa; es útil para extraer segmentos URI de la solicitud, parámetros de cadena de consulta y parámetros de formulario. Un ejemplo para ilustrar este último 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\"}"}
Este sería un buen momento para echar un vistazo al ejemplo del texto de la pregunta:
(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>"))
Analicemos cada ruta por turno:
(GET "/" [] (workbench))
- cuando se trata de una GET
solicitud :uri "/"
, llame a la función workbench
y renderice lo que devuelva en un mapa de respuesta. (Recuerde que el valor de retorno puede ser un mapa, pero también una cadena, etc.)
(POST "/save" {form-params :form-params} (str form-params))
- :form-params
es una entrada en el mapa de solicitud proporcionado por el wrap-params
middleware (recuerde que está implícitamente incluido por defroutes
). La respuesta será el estándar {:status 200 :headers {"Content-Type" "text/html"} :body ...}
con (str form-params)
sustituido ...
. (Un POST
manejador un poco inusual , este ...)
(GET "/test" [& more] (str "<pre> more "</pre>"))
- esto, por ejemplo, haría eco de la representación de cadena del mapa {"foo" "1"}
si el agente de usuario lo solicitara "/test?foo=1"
.
(GET ["/:filename" :filename #".*"] [filename] ...)
- la :filename #".*"
pieza no hace nada en absoluto (ya que #".*"
siempre coincide). Llama a la función de utilidad Ring ring.util.response/file-response
para producir su respuesta; la {:root "./static"}
parte le dice dónde buscar el archivo.
(ANY "*" [] ...)
- una ruta general. Es una buena práctica de Compojure incluir siempre una ruta de este tipo al final de un defroutes
formulario para garantizar que el controlador que se está definiendo siempre devuelva un mapa de respuesta Ring válido (recuerde que se produce una falla de coincidencia de ruta nil
).
¿Por qué de esta manera?
Uno de los propósitos del middleware Ring es agregar información al mapa de solicitudes; así, el middleware de manejo de cookies agrega una :cookies
clave a la solicitud, wrap-params
agrega :query-params
y / o:form-params
si está presente una cadena de consulta / datos de formulario, etc. (Estrictamente hablando, toda la información que agregan las funciones de middleware debe estar ya presente en el mapa de solicitud, ya que eso es lo que se pasa; su trabajo es transformarlo para que sea más conveniente trabajar con los controladores que envuelven). En última instancia, la solicitud "enriquecida" se pasa al controlador base, que examina el mapa de solicitudes con toda la información bien preprocesada agregada por el middleware y produce una respuesta. (El middleware puede hacer cosas más complejas que eso, como envolver varios manejadores "internos" y elegir entre ellos, decidir si llamar a los manejadores envueltos, etc. Eso está, sin embargo, fuera del alcance de esta respuesta).
El manejador base, a su vez, suele ser (en casos no triviales) una función que tiende a necesitar solo un puñado de elementos de información sobre la solicitud. (Por ejemplo, ring.util.response/file-response
no se preocupa por la mayor parte de la solicitud; solo necesita un nombre de archivo). De ahí la necesidad de una forma sencilla de extraer solo las partes relevantes de una solicitud Ring. Compojure tiene como objetivo proporcionar un motor de coincidencia de patrones de propósito especial, por así decirlo, que hace precisamente eso.