web-dev-qa-db-fra.com

Quelle est la «grande idée» derrière les itinéraires compojure?

Je suis nouveau sur Clojure et j'utilise Compojure pour écrire une application Web de base. Je frappe un mur avec la syntaxe defroutes de Compojure, cependant, et je pense que je dois comprendre à la fois le "comment" et le "pourquoi" derrière tout cela.

Il semble qu'une application de type Ring commence par une carte de requête HTTP, puis passe simplement la demande à travers une série de fonctions middleware jusqu'à ce qu'elle soit transformée en une carte de réponse, qui est renvoyée au navigateur. Ce style semble trop "bas niveau" pour les développeurs, d'où la nécessité d'un outil comme Compojure. Je peux voir ce besoin de plus d'abstractions dans d'autres écosystèmes logiciels, notamment avec WSGI de Python.

Le problème est que je ne comprends pas l'approche de Compojure. Prenons l'expression defroutes S suivante:

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

Je sais que la clé pour comprendre tout cela réside dans certains macro vaudou, mais je ne comprends pas encore totalement les macros. Je regarde la source defroutes depuis longtemps, mais je ne comprends pas! Que se passe t-il ici? Comprendre la "grande idée" m'aidera probablement à répondre à ces questions spécifiques:

  1. Comment accéder à l'environnement Ring depuis une fonction routée (par exemple la fonction workbench)? Par exemple, disons que je voulais accéder aux en-têtes HTTP_ACCEPT ou à une autre partie de la demande/du middleware?
  2. Quel est le problème avec la déstructuration ({form-params :form-params})? Quels mots clés sont disponibles pour moi lors de la déstructuration?

J'aime vraiment Clojure mais je suis tellement perplexe!

106
Sean Woods

Compojure a expliqué (dans une certaine mesure)

NB. Je travaille avec Compojure 0.4.1 ( ici est la validation de la version 0.4.1 sur GitHub).

Pourquoi?

Tout en haut de compojure/core.clj, voici ce résumé utile du but de Compojure:

Une syntaxe concise pour générer des gestionnaires d'anneau.

À un niveau superficiel, c'est tout ce qu'il y a à la question "pourquoi". Pour aller un peu plus loin, voyons comment fonctionne une application de style Ring:

  1. Une demande arrive et est transformée en carte Clojure conformément à la spécification Ring.

  2. Cette carte est canalisée dans une soi-disant "fonction de gestionnaire", qui devrait produire une réponse (qui est également une carte de Clojure).

  3. La carte de réponse est transformée en une réponse HTTP réelle et renvoyée au client.

L'étape 2 ci-dessus est la plus intéressante, car il est de la responsabilité du gestionnaire d'examiner l'URI utilisé dans la demande, d'examiner les cookies, etc., et d'arriver finalement à une réponse appropriée. Il est clair qu'il est nécessaire que tout ce travail soit pris en compte dans une collection de pièces bien définies; il s'agit normalement d'une fonction de gestionnaire "de base" et d'une collection de fonctions middleware qui l'encapsulent. Le but de Compojure est de simplifier la génération de la fonction de gestionnaire de base.

Comment?

Compojure est construit autour de la notion de "routes". Ceux-ci sont en fait implémentés à un niveau plus profond par la bibliothèque Clout (une retombée du projet Compojure - beaucoup de choses ont été déplacées vers des bibliothèques distinctes à la transition 0.3.x -> 0.4.x). Un itinéraire est défini par (1) une méthode HTTP (GET, PUT, HEAD ...), (2) un modèle d'URI (spécifié avec une syntaxe qui sera apparemment familière aux Webby Rubyists), (3) une forme de déstructuration utilisée dans lier des parties de la mappe de requête aux noms disponibles dans le corps, (4) un corps d'expressions qui doit produire une réponse Ring valide (dans les cas non triviaux, il s'agit généralement d'un simple appel à une fonction distincte).

Cela pourrait être un bon point pour jeter un œil à un exemple simple:

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

Testons cela à REPL (la carte de demande ci-dessous est la carte de demande Ring valide minimale):

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 étaient :head à la place, la réponse serait nil. Nous reviendrons sur la question de ce que nil signifie ici dans une minute (mais notez que ce n'est pas une réponse Ring valide!).

Comme il ressort de cet exemple, example-route n'est qu'une fonction, et très simple à cela; il examine la demande, détermine s'il est intéressé à la traiter (en examinant :request-method et :uri) et, si tel est le cas, renvoie une carte de réponse de base.

Ce qui est également évident, c'est que le corps de l'itinéraire n'a pas vraiment besoin d'être évalué pour une carte de réponse appropriée; Compojure fournit une gestion par défaut saine pour les chaînes (comme vu ci-dessus) et un certain nombre d'autres types d'objets; voir le compojure.response/render multiméthode pour plus de détails (le code est entièrement auto-documenté ici).

Essayons d'utiliser defroutes maintenant:

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

Les réponses à l'exemple de requête affiché ci-dessus et à sa variante avec :request-method :head sont comme prévu.

Le fonctionnement interne de example-routes sont tels que chaque route est essayée tour à tour; dès que l'un d'eux renvoie une réponse nonnil, cette réponse devient la valeur de retour de l'ensemble example-routes gestionnaire. Pour plus de commodité, les gestionnaires définis par defroutes sont enveloppés dans wrap-params et wrap-cookies implicitement.

Voici un exemple d'un itinéraire plus complexe:

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

Notez la forme déstructurante à la place du vecteur vide précédemment utilisé. L'idée de base ici est que le corps de l'itinéraire pourrait être intéressé par certaines informations sur la demande; comme cela arrive toujours sous la forme d'une carte, un formulaire de déstructuration associative peut être fourni pour extraire des informations de la requête et les lier à des variables locales qui seront à portée dans le corps de la route.

Un test de ce qui précède:

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 idée de suivi de ce qui précède est que des itinéraires plus complexes peuvent assoc des informations supplémentaires sur la demande à l'étape de correspondance:

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

Cela répond par un :body de "foo" à la demande de l'exemple précédent.

Deux choses sont nouvelles dans ce dernier exemple: le "/:fst/*" et le vecteur de liaison non vide [fst]. Le premier est la syntaxe Rails-and-Sinatra susmentionnée pour les modèles d'URI. C'est un peu plus sophistiqué que ce qui ressort de l'exemple ci-dessus en ce que les contraintes d'expression régulière sur les segments URI sont prises en charge (par exemple ["/:fst/*" :fst #"[0-9]+"] peut être fourni pour que l'itinéraire accepte uniquement les valeurs à tous les chiffres de :fst Au dessus). La seconde est une façon simplifiée de faire correspondre le :params entrée dans la mappe de requête, qui est elle-même une mappe; il est utile pour extraire des segments URI de la demande, des paramètres de chaîne de requête et des paramètres de formulaire. Un exemple pour illustrer ce dernier point:

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

Ce serait le bon moment pour jeter un œil à l'exemple du texte de la question:

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

Analysons tour à tour chaque itinéraire:

  1. (GET "/" [] (workbench)) - lors du traitement d'une demande GET avec :uri "/", appelez la fonction workbench et restituez ce qu'elle retourne dans une carte de réponse. (Rappelez-vous que la valeur de retour peut être une carte, mais aussi une chaîne, etc.)

  2. (POST "/save" {form-params :form-params} (str form-params)) - :form-params est une entrée dans la mappe de demande fournie par le wrap-params middleware (rappelons qu'il est implicitement inclus par defroutes). La réponse sera la norme {:status 200 :headers {"Content-Type" "text/html"} :body ...} avec (str form-params) substitué à .... (Un gestionnaire POST légèrement inhabituel, ceci ...)

  3. (GET "/test" [& more] (str "<pre> more "</pre>")) - ce serait par exemple renvoie la représentation sous forme de chaîne de la carte {"foo" "1"} si l'agent utilisateur a demandé "/test?foo=1".

  4. (GET ["/:filename" :filename #".*"] [filename] ...) -- les :filename #".*" part ne fait rien du tout (puisque #".*" correspond toujours). Il appelle la fonction utilitaire Ring ring.util.response/file-response pour produire sa réponse; les {:root "./static"} part lui indique où chercher le fichier.

  5. (ANY "*" [] ...) - une route fourre-tout. Il est recommandé de toujours inclure une telle route à la fin d'un formulaire defroutes pour garantir que le gestionnaire en cours de définition renvoie toujours une carte de réponse Ring valide (rappelez-vous qu'un échec de correspondance de route se traduit par nil).

Pourquoi de cette façon?

L'un des objectifs du middleware Ring est d'ajouter des informations à la carte de demande; ainsi, le middleware de gestion des cookies ajoute un :cookies clé de la demande, wrap-params ajoute :query-params et/ou :form-params si une chaîne de requête/des données de formulaire sont présentes, etc. (À strictement parler, toutes les informations que les fonctions middleware ajoutent doivent déjà être présentes dans la carte de demande, car c'est ce qu'elles obtiennent; leur travail consiste à le transformer pour qu'il soit plus pratique de travailler avec les gestionnaires qu'elles encapsulent.) En fin de compte, la demande "enrichie" est transmise au gestionnaire de base, qui examine le mappage de demande avec toutes les informations bien prétraitées ajoutées par le middleware et produit une réponse. (Le middleware peut faire des choses plus complexes que cela - comme encapsuler plusieurs gestionnaires "internes" et choisir entre eux, décider s'il faut appeler le ou les gestionnaires encapsulés, etc., mais cela sort du cadre de cette réponse.)

Le gestionnaire de base, à son tour, est généralement (dans des cas non triviaux) une fonction qui a tendance à ne nécessiter qu'une poignée d'informations sur la demande. (Par exemple. ring.util.response/file-response ne se soucie pas de la majeure partie de la demande; il n'a besoin que d'un nom de fichier.) D'où la nécessité d'un moyen simple d'extraire uniquement les parties pertinentes d'une demande Ring. Compojure vise à fournir un moteur de correspondance de motifs à usage spécial, pour ainsi dire, qui fait exactement cela.

208
Michał Marczyk

Il y a un excellent article sur booleanknot.com de James Reeves (auteur de Compojure), et le lire l'a fait "cliquer" pour moi, donc j'en ai retranscrit une partie ici (vraiment c'est tout ce que j'ai fait ).

Il y a aussi un slidedeck ici du même auteur , qui répond à cette question exacte.

Compojure est basé sur Ring , qui est une abstraction pour les requêtes http.

A concise syntax for generating Ring handlers.

Alors, quels sont ces gestionnaires d'annea ? Extrait du doc:

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

Assez simple, mais aussi assez bas. Le gestionnaire ci-dessus peut être défini de manière plus concise à l'aide de ring/util bibliothèque.

(use 'ring.util.response)

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

Maintenant, nous voulons appeler différents gestionnaires en fonction de la demande. Nous pourrions faire un routage statique comme ceci:

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

Et refactorisez-le comme ceci:

(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 chose intéressante que James note alors est que cela permet d'imbriquer des routes, car "le résultat de la combinaison de deux routes ou plus est lui-même une route".

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

À l'heure actuelle, nous commençons à voir du code qui semble pouvoir être factorisé, à l'aide d'une macro. Compojure fournit une macro defroutes:

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

;; is identical to

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

Compojure fournit d'autres macros, comme 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)))

Cette dernière fonction générée ressemble à notre gestionnaire!

S'il vous plaît assurez-vous de vérifier James post , car il va dans des explications plus détaillées.

7
nha
3
nickik

Pour quiconque a encore du mal à découvrir ce qui se passe avec les itinéraires, il se peut que, comme moi, vous ne compreniez pas l'idée de la déstructuration.

En fait, lire la documentation de let a aidé à clarifier l'ensemble "d'où viennent les valeurs magiques?" question.

Je colle les sections pertinentes ci-dessous:

Clojure prend en charge la liaison structurelle abstraite, souvent appelée déstructuration, dans les listes de liaisons let, les listes de paramètres fn et toute macro qui se développe en let ou fn. L'idée de base est qu'une forme de liaison peut être un littéral de structure de données contenant des symboles qui se lient aux parties respectives de l'init-expr. La liaison est abstraite en ce sens qu'un littéral de vecteur peut se lier à tout ce qui est séquentiel, tandis qu'un littéral de carte peut se lier à tout ce qui est associatif.

Les expressions de liaison de vecteur vous permettent de lier des noms à des parties d'éléments séquentiels (pas seulement des vecteurs), comme des vecteurs, des listes, des séquences, des chaînes, des tableaux et tout ce qui prend en charge nth. La forme séquentielle de base est un vecteur de formes de liaison, qui sera lié aux éléments successifs de l'init-expr, recherché via nth. De plus, et facultativement, & suivi d'une forme de liaison entraînera cette forme de liaison à être liée au reste de la séquence, c'est-à-dire cette partie non encore liée, recherchée via nthnext. Enfin, également facultatif,: tel qu'il est suivi par un symbole, ce symbole sera lié à l'ensemble 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]]

Les expressions de liaison de vecteur vous permettent de lier des noms à des parties d'éléments séquentiels (pas seulement des vecteurs), comme des vecteurs, des listes, des séquences, des chaînes, des tableaux et tout ce qui prend en charge nth. La forme séquentielle de base est un vecteur de formes de liaison, qui sera lié aux éléments successifs de l'init-expr, recherché via nth. De plus, et facultativement, & suivi d'une forme de liaison entraînera cette forme de liaison à être liée au reste de la séquence, c'est-à-dire cette partie non encore liée, recherchée via nthnext. Enfin, également facultatif,: tel qu'il est suivi par un symbole, ce symbole sera lié à l'ensemble 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]]
3
Pieter Breed

Quel est le problème avec la déstructuration ({form-params: form-params})? Quels mots clés sont disponibles pour moi lors de la déstructuration?

Les clés disponibles sont celles qui se trouvent dans la carte d'entrée. La déstructuration est disponible à l'intérieur des formes let et doseq, ou à l'intérieur des paramètres fn ou defn

Nous espérons que le code suivant sera informatif:

(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 exemple plus avancé, montrant la déstructuration imbriquée:

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

Lorsqu'elle est utilisée à bon escient, la déstructuration désencombre votre code en évitant l'accès aux données standard. en utilisant: as et en imprimant le résultat (ou les clés du résultat), vous pouvez avoir une meilleure idée des autres données auxquelles vous pourriez accéder.

1
noisesmith