¿Cuál es la “gran idea” detrás de las rutas compuestas?

109

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

Parece que una aplicación de estilo Ring comienza con un mapa de solicitud HTTP, luego simplemente 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 "de bajo nivel" para los desarrolladores, de ahí 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 la siguiente defroutesexpresión 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>"))

Sé que la clave para entender todo esto se encuentra en algún macro vudú, pero no entiendo totalmente las macros (todavía). He mirado la defroutesfuente durante mucho tiempo, ¡pero no lo entiendo! ¿Que está pasando aqui? Entender la "gran idea" probablemente me ayudará a responder estas preguntas específicas:

  1. ¿Cómo accedo al entorno Ring desde una función enrutada (por ejemplo, la workbenchfunción)? Por ejemplo, digamos que quería acceder a los encabezados HTTP_ACCEPT o alguna otra parte de la solicitud / middleware.
  2. ¿Qué pasa con la desestructuración ( {form-params :form-params})? ¿Qué palabras clave están disponibles para mí al desestructurar?

¡Realmente me gusta Clojure pero estoy tan perplejo!

Sean Woods
fuente

Respuestas:

212

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:

  1. Llega una solicitud y se transforma en un mapa Clojure de acuerdo con la especificación Ring.

  2. 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).

  3. 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-methodfuera en :headcambio, la respuesta sería nil. Volveremos a la pregunta de qué nilsignifica aquí en un minuto (¡pero observe que no es una respuesta válida de Ring!).

Como se desprende de este ejemplo, example-routees solo una función, y además muy simple; examina la solicitud, determina si está interesado en manejarla (examinando :request-methody :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/rendermétodo múltiple para obtener más detalles (el código es completamente autodocumentado aquí).

Intentemos usar defroutesahora:

(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 :headson las esperadas.

El funcionamiento interno de example-routeses tal que cada ruta se prueba por turno; tan pronto como uno de ellos devuelve una falta de nilrespuesta, esa respuesta se convierte en el valor de retorno de todo el example-routescontrolador. Para mayor comodidad, defrouteslos manipuladores -definida están envueltos en wrap-paramse wrap-cookiesimplí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 assocagregar 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 :bodyde "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 :fstlo anterior). La segunda es una forma simplificada de hacer coincidir la :paramsentrada 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:

  1. (GET "/" [] (workbench))- cuando se trata de una GETsolicitud :uri "/", llame a la función workbenchy 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.)

  2. (POST "/save" {form-params :form-params} (str form-params))- :form-paramses una entrada en el mapa de solicitud proporcionado por el wrap-paramsmiddleware (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 POSTmanejador un poco inusual , este ...)

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

  4. (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-responsepara producir su respuesta; la {:root "./static"}parte le dice dónde buscar el archivo.

  5. (ANY "*" [] ...)- una ruta general. Es una buena práctica de Compojure incluir siempre una ruta de este tipo al final de un defroutesformulario 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 :cookiesclave a la solicitud, wrap-paramsagrega :query-paramsy / o:form-paramssi 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-responseno 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.

Michał Marczyk
fuente
3
"Como una conveniencia adicional, los manejadores definidos por defroutes están envueltos en wrap-params y wrap-cookies implícitamente". - A partir de la versión 0.6.0 debe agregarlos explícitamente. Ref github.com/weavejester/compojure/commit/…
Dan Midwood
3
Muy bien dicho. Esta respuesta debería estar en la página de inicio de Compojure.
Siddhartha Reddy
2
Lectura obligatoria para cualquier persona nueva en Compojure. Deseo que cada wiki y publicación de blog sobre el tema comience con un enlace a esto.
jemmons
7

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

También hay una plataforma de diapositivas aquí del mismo autor , que responde 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 manejadores de Ring ? 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 de bajo nivel. El controlador anterior se puede definir de forma más concisa utilizando la ring/utilbiblioteca.

(use 'ring.util.response)

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

Ahora queremos llamar a diferentes manejadores según la solicitud. Podríamos hacer un enrutamiento estático como este:

(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 observa James entonces es que esto permite anidar rutas, 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)))

A estas alturas, estamos empezando a ver un código que parece que se podría factorizar mediante una macro. Compojure proporciona una defroutesmacro:

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

;; is identical to

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

Compojure proporciona otras macros, como la GETmacro:

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

Asegúrese de consultar la publicación de James , ya que incluye explicaciones más detalladas.

nha
fuente
4

Para cualquiera que todavía haya luchado por averiguar qué está pasando con las rutas, puede ser que, como yo, no entiendas la idea de la desestructuración.

De hecho, leer los documentoslet ayudó a aclarar todo "¿de dónde vienen los valores mágicos?" pregunta.

Estoy pegando las secciones relevantes a continuación:

Clojure admite enlaces estructurales abstractos, a menudo llamados desestructuración, en listas de enlaces let, listas de parámetros fn y cualquier macro que se expanda en let o fn. La idea básica es que una forma de enlace puede ser un literal de estructura de datos que contiene símbolos que se enlazan a las partes respectivas de init-expr. La vinculación es abstracta en el sentido de que un literal vectorial puede vincularse a cualquier cosa que sea secuencial, mientras que un literal de mapa puede vincularse a cualquier cosa que sea asociativa.

Vector binding-exprs le permite vincular nombres a partes de cosas secuenciales (no solo vectores), como vectores, listas, secuencias, cadenas, matrices y cualquier cosa que admita nth. La forma secuencial básica es un vector de formas vinculantes, que se vincularán a elementos sucesivos de init-expr, buscados mediante nth. Además, y opcionalmente, & seguido de una forma de unión hará que esa forma de unión se una al resto de la secuencia, es decir, esa parte aún no unida, buscada a través de nthsiguiente. Finalmente, también opcional,: seguido de un símbolo hará que ese símbolo se vincule a la totalidad de 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 le permite vincular nombres a partes de cosas secuenciales (no solo vectores), como vectores, listas, secuencias, cadenas, matrices y cualquier cosa que admita nth. La forma secuencial básica es un vector de formas vinculantes, que se vincularán a elementos sucesivos de init-expr, buscados mediante nth. Además, y opcionalmente, & seguido de una forma de unión hará que esa forma de unión se una al resto de la secuencia, es decir, esa parte aún no unida, buscada a través de nthsiguiente. Finalmente, también opcional,: seguido de un símbolo hará que ese símbolo se vincule a la totalidad de 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]]
Pieter Breed
fuente
3

Todavía no he comenzado con las cosas web de clojure pero, lo haré, aquí están las cosas que marqué.

Nickik
fuente
Gracias, estos enlaces son definitivamente útiles. He estado trabajando en este problema la mayor parte del día y estoy en un mejor lugar con él ... intentaré publicar un seguimiento en algún momento.
Sean Woods
1

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

Las claves disponibles son las que se encuentran 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

Se espera que el siguiente código sea 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 con prudencia, la desestructuración ordena el código al evitar el acceso a datos estándar. al usar: as e imprimir el resultado (o las claves del resultado) puede tener una mejor idea de a qué otros datos puede acceder.

herrero
fuente