clojure compojure

clojure - ¿Cuál es la "gran idea" detrás de las rutas de compojure?



(5)

Soy nuevo en Clojure y he usado Compojure para escribir una aplicación web básica. defroutes embargo, estoy chocando contra la pared con la sintaxis de defragmentaciones de defroutes , y creo que necesito entender tanto el "cómo" como el "por qué" detrás de todo.

Parece que una aplicación tipo anillo comienza con un mapa de solicitud HTTP, luego pasa la solicitud a través de una serie de funciones de middleware hasta que se transforma en un mapa de respuesta, que se envía de vuelta al navegador. Este estilo parece demasiado "bajo nivel" para los desarrolladores, por lo tanto, la necesidad de una herramienta como Compojure. También puedo ver esta necesidad de más abstracciones en otros ecosistemas de software, sobre todo con WSGI de Python.

El problema es que no entiendo el enfoque de Compojure. Tomemos las siguientes defroutes S-expresión:

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

Sé que la clave para entender todo esto radica en el macro vudú, pero todavía no entiendo totalmente las macros. Me he defroutes mirando la fuente de defroutes durante mucho tiempo, ¡pero simplemente no lo entiendo! ¿Que está pasando aqui? Comprender la "gran idea" probablemente me ayude a responder estas preguntas específicas:

  1. ¿Cómo accedo al entorno Ring desde dentro de una función enrutada (por ejemplo, la función workbench )? Por ejemplo, supongamos que quería acceder a los encabezados HTTP_ACCEPT o alguna otra parte de la solicitud / middleware?
  2. ¿Cuál es el problema con la desestructuración ( {form-params :form-params} )? ¿Qué palabras clave están disponibles para mí cuando estoy desestructurando?

Realmente me gusta Clojure, ¡pero estoy tan perplejo!


¿Cuál es el problema con la desestructuración ({form-params: form-params})? ¿Qué palabras clave están disponibles para mí cuando estoy desestructurando?

Las claves disponibles son las que están en el mapa de entrada. La desestructuración está disponible dentro de las formas let y doseq, o dentro de los parámetros para fn o defn

El siguiente código será 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 ejemplo más avanzado, que muestra la desestructuración anidada:

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

Cuando se usa sabiamente, la desestructuración declutters su código al evitar el acceso a datos repetitivos. al usar: como e imprimir el resultado (o las claves del resultado) puede tener una mejor idea de a qué otros datos puede acceder.


Compojure explicó (hasta cierto punto)

NÓTESE BIEN. Estoy trabajando con Compojure 0.4.1 ( here está el commit de la versión 0.4.1 en GitHub).

¿Por qué?

En la parte superior de compojure/core.clj , está este útil resumen del propósito de Compojure:

Una sintaxis concisa para generar controladores de anillo.

En un nivel superficial, eso es todo lo que hay para la pregunta "por qué". Para ir un poco más profundo, echemos un vistazo a cómo funciona una aplicación de estilo Ring:

  1. Una solicitud llega y se transforma en un mapa de Clojure de acuerdo con la especificación del anillo.

  2. Este mapa se canaliza a la denominada "función de controlador", que se espera que produzca una respuesta (que también es un mapa de Clojure).

  3. El mapa de respuesta se transforma en una respuesta HTTP real y se envía de vuelta al cliente.

El paso 2. de arriba es el más interesante, ya que es responsabilidad del manejador examinar el URI utilizado en la solicitud, examinar las cookies, etc. y finalmente llegar a una respuesta adecuada. Claramente, es necesario que todo este trabajo se tenga en cuenta en una colección de piezas bien definidas; estos son normalmente una función de controlador "base" y una colección de funciones de middleware que lo 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 son implementados en un nivel más profundo por la biblioteca Clout (un spin-off 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 de URI (especificado con una sintaxis que al parecer será conocida por Webby Rubyists), (3) una forma de desestructuración utilizada en partes vinculantes del mapa de solicitud a los nombres disponibles en el cuerpo, (4) un cuerpo de expresiones que necesita producir una respuesta de timbre válida (en casos no triviales esto generalmente es 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 REPL (el mapa de solicitud a continuación es el mapa de solicitud de timbre 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 were :head cambio, la respuesta sería nil . Volveremos a la pregunta de qué significa nil aquí en un minuto (¡pero tenga en cuenta que no es un anillo válido resuspender!).

Como se desprende de este ejemplo, example-route es solo una función, y muy simple; mira 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 realmente no necesita evaluar a un mapa de respuesta adecuado; Compojure proporciona un manejo por defecto de las cuerdas (como se ve arriba) y un número de otros tipos de objetos; ver el compojure.response/render multimethod para más detalles (el código es completamente auto-documentado aquí).

Tratemos de usar defroutes ahora:

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

Las respuestas a la solicitud de ejemplo que se muestra arriba y su variante con :request-method :head son como las esperadas.

El funcionamiento interno de example-routes de example-routes es tal que cada ruta se prueba por turnos; tan pronto como uno de ellos devuelva una respuesta no nil , esa respuesta se convierte en el valor de retorno de todo el manejador de example-routes . Como una ventaja adicional, los manejadores defroutes defroutes están envueltos en wrap-params y wrap-cookies implícitamente.

Aquí hay 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 cierta información sobre la solicitud; dado que esto siempre llega en la forma de un mapa, se puede suministrar una forma asociativa de desestructuración para extraer información de la solicitud y vincularla a las 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 información adicional a la solicitud en la etapa de coincidencia:

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

Esto responde con un :body de "foo" a la solicitud del ejemplo anterior.

Hay dos cosas nuevas acerca de este último ejemplo: "/:fst/*" y el vector de enlace no vacío [fst] . La primera es la sintaxis de Rails-and-Sinatra antes mencionada para patrones de URI. Es un poco más sofisticado de lo que se desprende del ejemplo anterior en que las restricciones de expresiones regulares en los segmentos URI son compatibles (por ejemplo, ["/:fst/*" :fst #"[0-9]+"] se pueden suministrar para la ruta solo acepta valores de todos los dígitos de :fst en el punto anterior). El segundo es una forma simplificada de emparejar en la entrada :params en el mapa de solicitud, que a su vez es un mapa; es útil para extraer segmentos URI de la solicitud, consultar parámetros de cadena y parámetros de formulario. Un ejemplo para ilustrar el ú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 a su vez:

  1. (GET "/" [] (workbench)) - cuando se trata de una solicitud GET con :uri "/" , llame a la función workbench y renderice lo que devuelve en un mapa de respuesta. (Recuerde que el valor de retorno puede ser un mapa, pero también una cadena, etc.)

  2. (POST "/save" {form-params :form-params} (str form-params)) - :form-params es una entrada en el mapa de solicitudes proporcionada por el middleware wrap-params (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 por ... (Un controlador de POST ligeramente inusual, esto ...)

  3. (GET "/test" [& more] (str "<pre> more "</pre>")) - esto sería, por ejemplo, eco de la representación de cadena del mapa {"foo" "1"} si el agente de usuario preguntó por "/test?foo=1" .

  4. (GET ["/:filename" :filename #".*"] [filename] ...) - the :filename #".*" No hace nada (ya que #".*" Siempre coincide). Llama a la función de utilidad Ring ring.util.response/file-response para producir su respuesta; la parte {:root "./static"} le dice dónde buscar el archivo.

  5. (ANY "*" [] ...) - una ruta general. Es una buena práctica de Compojure incluir siempre dicha ruta al final de una forma de defroutes para garantizar que el manejador que se está definiendo siempre devuelva un mapa de respuesta de timbre válido (recuerde que una falla de coincidencia de ruta da como resultado nil ).

¿Por qué de esta manera?

Uno de los propósitos del middleware Ring es agregar información al mapa de solicitudes; por lo tanto, el middleware de manejo de :cookies agrega una clave :cookies a la solicitud, wrap-params agrega :query-params y / o :form-params si una cadena de consulta / datos de formulario está presente y así sucesivamente. (Estrictamente hablando, toda la información que las funciones de middleware están agregando debe estar ya presente en el mapa de solicitudes, ya que eso es lo que se pasa, su trabajo es transformarlo para que sea más conveniente trabajar con los manejadores que envuelven). En última instancia, la solicitud "enriquecida" se pasa al manejador de base, que examina el mapa de solicitud con toda la información preprocesada muy bien 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 en absoluto, etc. Eso está, sin embargo, fuera del alcance de esta respuesta).

El manejador de bases, 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 ocupa de la mayor parte de la solicitud, solo necesita un nombre de archivo). De ahí la necesidad de una forma simple de extraer solo las partes relevantes de una solicitud de timbre. Compojure tiene como objetivo proporcionar un motor de combinación de patrones de propósito especial, por así decirlo, que hace precisamente eso.


Hay un excelente artículo en booleanknot.com de James Reeves (autor de Compojure), y leerlo lo hizo "clic" para mí, así que lo he retranscrito aquí (realmente eso es todo lo que hice).

También hay una plataforma deslizante aquí del mismo autor , que responde a esta pregunta exacta.

Compojure se basa en Ring , que es una abstracción para solicitudes http.

A concise syntax for generating Ring handlers.

Entonces, ¿qué son esos controladores de anillo ? Extracto del 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)})

Bastante simple, pero también bastante bajo nivel. El manejador anterior se puede definir de forma más concisa utilizando la biblioteca ring/util .

(use ''ring.util.response) (defn handler [request] (response "Hello World"))

Ahora queremos llamar a diferentes manejadores dependiendo de la solicitud. Podríamos hacer un enrutamiento estático como ese:

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

Y refactorizarlo así:

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

Lo interesante que James nota es que esto permite rutas de anidamiento, porque "el resultado de combinar dos o más rutas juntas es en sí mismo una ruta".

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

Por ahora, estamos comenzando a ver algún código que parece que podría ser factorizado, usando una macro. Compojure proporciona una macro de defroutes :

(defroutes ab-routes a-route b-route) ;; is identical to (def ab-routes (routes a-route b-route))

Compojure proporciona otras macros, como la macro GET :

(GET "/a" [] "Alpha") ;; will expand to (fn [request#] (if (and (= (:request-method request#) ~http-method) (= (:uri request#) ~uri)) (let [~bindings request#] ~@body)))

¡Esa última función generada se parece a nuestro controlador!

Por favor, asegúrese de revisar booleanknot.com , ya que entra en explicaciones más detalladas.



Para cualquiera que todavía tenga dificultades para descubrir qué está pasando con las rutas, es posible que, como yo, no comprenda la idea de desestructurar.

En realidad, leer los documentos para let ayudó a aclarar todo "¿de dónde vienen los valores mágicos?" pregunta.

Estoy pegando las secciones relevantes a continuación:

Clojure admite uniones estructurales abstractas, a menudo denominadas desestructuración, en listas de enlaces, listas de parámetros fn y cualquier macro que se expanda en un let o fn. La idea básica es que una forma de enlace puede ser una estructura de datos literal que contiene símbolos que se unen a las partes respectivas de la inicialización-expr. El enlace es abstracto en el sentido de que un literal de vector se puede unir a todo lo que es secuencial, mientras que un literal de un mapa se puede unir a todo lo que es asociativo.

Los vectores vinculantes-exprs le permiten vincular nombres a partes de elementos secuenciales (no solo vectores), como vectores, listas, secuencias, cadenas, matrices y cualquier elemento que admita nth. La forma secuencial básica es un vector de formas vinculantes, que se vinculará a elementos sucesivos de init-expr, buscados a través de enésimo. Además, y opcionalmente, y seguido por un binding-forms hará que ese binding-form se vincule con el resto de la secuencia, es decir, la parte que aún no está unida, buscada mediante nthnext. Finalmente, también es opcional:: como sigue un símbolo, ese símbolo se vinculará a todo el 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]]

Los vectores vinculantes-exprs le permiten vincular nombres a partes de elementos secuenciales (no solo vectores), como vectores, listas, secuencias, cadenas, matrices y cualquier elemento que admita nth. La forma secuencial básica es un vector de formas vinculantes, que se vinculará a elementos sucesivos de init-expr, buscados a través de enésimo. Además, y opcionalmente, y seguido por un binding-forms hará que ese binding-form se vincule con el resto de la secuencia, es decir, la parte que aún no está unida, buscada mediante nthnext. Finalmente, también es opcional:: como sigue un símbolo, ese símbolo se vinculará a todo el 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]]