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

109

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

Il semble qu'une application de style Ring commence par une carte de requête HTTP, puis transmet simplement la requête à 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 également voir ce besoin d'abstractions supplémentaires dans d'autres écosystèmes logiciels, notamment avec le WSGI de Python.

Le problème est que je ne comprends pas l'approche de Compojure. Prenons l' defroutesexpression 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 se trouve dans une macro vaudou, mais je ne comprends pas totalement les macros (encore). J'ai regardé la defroutessource pendant un long moment, mais ne comprenez 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 à partir d'une fonction routée (par exemple, la workbenchfonction)? Par exemple, disons que je voulais accéder aux en-têtes HTTP_ACCEPT ou à une autre partie de la requête / 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!

Sean Woods
la source

Réponses:

212

Compojure expliqué (dans une certaine mesure)

NB. Je travaille avec Compojure 0.4.1 ( voici le commit de version 0.4.1 sur GitHub).

Pourquoi?

Tout en haut de compojure/core.clj, il y a ce résumé utile de l'objectif de Compojure:

Une syntaxe concise pour générer des gestionnaires Ring.

Sur un plan 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 une 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 Clojure).

  3. Le mappage de réponse est transformé en une réponse HTTP réelle et renvoyé 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 tous les cookies, etc. et finalement d'arriver à une réponse appropriée. Il est évidemment nécessaire que tout ce travail soit intégré 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». Celles-ci sont en fait implémentées à un niveau plus profond par la bibliothèque Clout (un spin-off du projet Compojure - beaucoup de choses ont été déplacées vers des bibliothèques séparées à la transition 0.3.x -> 0.4.x). Une route est définie par (1) une méthode HTTP (GET, PUT, HEAD ...), (2) un modèle 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 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 au REPL (la carte de requête ci-dessous est la carte de requête 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-methodc'était le cas :head, la réponse serait nil. Nous reviendrons sur la question de savoir ce que nilsignifie ici dans une minute (mais notez que ce n'est pas une réponse Ring valide!).

Comme il ressort de cet exemple, il example-routes'agit simplement d'une fonction, et très simple en plus; il examine la demande, détermine s'il est intéressé à la traiter (en examinant :request-methodet :uri) et, si c'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é à une carte de réponse appropriée; Compojure fournit une gestion par défaut sensée pour les chaînes (comme vu ci-dessus) et un certain nombre d'autres types d'objets; voir la compojure.response/rendermultiméthode pour plus de détails (le code est entièrement auto-documenté ici).

Essayons d'utiliser defroutesmaintenant:

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

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

Le fonctionnement interne de example-routesest tel que chaque voie est essayée à son tour; dès que l'un d'eux retourne une non- nilréponse, cette réponse devient la valeur de retour de l'ensemble du example-routesgestionnaire. Pour plus de commodité, les defroutesgestionnaires -defined sont encapsulés wrap-paramset wrap-cookiesimplicitement.

Voici un exemple d'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 de déstructuration à 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, une forme de déstructuration associative peut être fournie pour extraire les informations de la requête et les lier à des variables locales qui seront dans la portée du corps de l'itinéraire.

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

L'idée géniale de suivi de ce qui précède est que des itinéraires plus complexes peuvent assocfournir des informations supplémentaires sur la demande au stade de la correspondance:

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

Cela répond à une :bodyde "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 de type 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 de regex sur les segments d'URI sont supportées (par exemple ["/:fst/*" :fst #"[0-9]+"]peuvent être fournies pour que la route n'accepte que les valeurs à tous les chiffres :fstci-dessus). Le second est une manière simplifiée de faire correspondre l' :paramsentrée dans la mappe de demande, qui est elle-même une mappe; il est utile pour extraire des segments URI de la requête, 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 chaque itinéraire tour à tour:

  1. (GET "/" [] (workbench))- lors du traitement d'une GETrequête avec :uri "/", appelez la fonction workbenchet restituez tout ce qu'elle renvoie 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-paramsest une entrée dans la table des requêtes fournie par le wrap-paramsmiddleware (rappelons qu'elle est implicitement incluse par defroutes). La réponse sera la norme {:status 200 :headers {"Content-Type" "text/html"} :body ...}avec (str form-params)substitué .... (Un POSTgestionnaire un peu inhabituel , ce ...)

  3. (GET "/test" [& more] (str "<pre> more "</pre>"))- cela ferait par exemple écho à la représentation sous forme de chaîne de la carte {"foo" "1"}si l'agent utilisateur le demandait "/test?foo=1".

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

  5. (ANY "*" [] ...)- un itinéraire fourre-tout. Il est conseillé à Compojure de toujours inclure une telle route à la fin d'un defroutesformulaire pour s'assurer que le gestionnaire en cours de définition renvoie toujours une carte de réponse Ring valide (rappelez-vous qu'un échec de correspondance d'itinéraire entraîne nil).

Pourquoi de cette façon?

L'un des objectifs du middleware Ring est d'ajouter des informations à la carte des requêtes; ainsi le middleware de gestion des cookies ajoute une :cookiesclé à la requête,wrap-params ajoute :query-paramset / ou:form-paramssi une chaîne de requête / des données de formulaire sont présentes et ainsi de suite. (Strictement parlant, toutes les informations que les fonctions middleware ajoutent doivent déjà être présentes dans la mappe de requêtes, car c'est ce qu'elles sont transmises; leur travail consiste à les transformer pour qu'il soit plus pratique de travailler avec les gestionnaires qu'elles enveloppent.) Finalement, la demande "enrichie" est transmise au gestionnaire de base, qui examine la carte de demande avec toutes les informations bien prétraitées ajoutées par le middleware et produit une réponse. (L'intergiciel peut faire des choses plus complexes que cela - comme envelopper plusieurs gestionnaires "internes" et choisir entre eux, décider d'appeler ou non le ou les gestionnaires encapsulés du tout, etc. Cela sort cependant 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 requête. (Par exemple, ring.util.response/file-responsene se soucie pas de la plupart de la requête; 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 requête Ring. Compojure vise à fournir un moteur de correspondance de modèles à usage spécial, pour ainsi dire, qui fait exactement cela.

Michał Marczyk
la source
3
«Pour plus de commodité, les gestionnaires définis par les défroutes sont enveloppés implicitement dans des paramètres wraps et des cookies enveloppants. - Depuis la version 0.6.0, vous devez les ajouter explicitement. Ref github.com/weavejester/compojure/commit/…
Dan Midwood
3
Très bien mis. Cette réponse devrait être sur la page d'accueil de Compojure.
Siddhartha Reddy
2
Lecture obligatoire pour tous les nouveaux utilisateurs de Compojure. Je souhaite que chaque wiki et article de blog sur le sujet commence par un lien vers celui-ci.
jemmons
7

Il y a un excellent article sur booleanknot.com de James Reeves (auteur de Compojure), et sa lecture 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'anneau ? Extrait de la 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 niveau. Le gestionnaire ci-dessus peut être défini de manière plus concise à l'aide de la ring/utilbibliothè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 du 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 itinéraires, car "le résultat de la combinaison de deux ou plusieurs itinéraires ensemble est lui-même un itinéraire".

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

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

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

;; is identical to

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

Compojure fournit d'autres macros, comme le GET macro:

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

Veuillez vous assurer de consulter le message de James , car il donne des explications plus détaillées.

nha
la source
4

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

En fait, la lecture de la documentation alet aidé à clarifier le tout "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 sont liés aux parties respectives de init-expr. La liaison est abstraite en ce qu'un littéral vectoriel 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 vectorielles vous permettent de lier des noms à des parties de choses séquentielles (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 seront liées à des éléments successifs de init-expr, recherchés via nth. De plus, et facultativement, & suivi par un binding-forms entraînera la liaison de cette forme de liaison au reste de la séquence, c'est-à-dire à cette partie non encore liée, recherchée via nthnext. Enfin, également facultatif,: comme suivi d'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 vectorielles vous permettent de lier des noms à des parties de choses séquentielles (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 seront liées à des éléments successifs de init-expr, recherchés via nth. De plus, et facultativement, & suivi par un binding-forms entraînera la liaison de cette forme de liaison au reste de la séquence, c'est-à-dire à cette partie non encore liée, recherchée via nthnext. Enfin, également facultatif,: comme suivi d'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]]
Race Pieter
la source
3

Je n'ai pas encore commencé sur les trucs Web clojure mais, je le ferai, voici les trucs que j'ai mis en signet.

Nickik
la source
Merci, ces liens sont vraiment utiles. J'ai travaillé sur ce problème pendant une bonne partie de la journée et je suis mieux placé avec lui ... Je vais essayer de poster un suivi à un moment donné.
Sean Woods
1

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 dans les formulaires let et doseq, ou dans les 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.

bruitsmith
la source