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:
Une demande arrive et est transformée en une carte Clojure conformément à la spécification Ring.
Cette carte est canalisée dans une soi-disant «fonction de gestionnaire», qui devrait produire une réponse (qui est également une carte Clojure).
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-method
c'était le cas :head
, la réponse serait nil
. Nous reviendrons sur la question de savoir 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, il example-route
s'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-method
et :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/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
est tel que chaque voie est essayée à son tour; dès que l'un d'eux retourne une non- nil
réponse, cette réponse devient la valeur de retour de l'ensemble du example-routes
gestionnaire. Pour plus de commodité, les defroutes
gestionnaires -defined sont encapsulés wrap-params
et wrap-cookies
implicitement.
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 assoc
fournir 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 :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 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 :fst
ci-dessus). Le second est une manière simplifiée de faire correspondre l' :params
entré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:
(GET "/" [] (workbench))
- lors du traitement d'une GET
requête avec :uri "/"
, appelez la fonction workbench
et 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.)
(POST "/save" {form-params :form-params} (str form-params))
- :form-params
est une entrée dans la table des requêtes fournie par le wrap-params
middleware (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 POST
gestionnaire un peu inhabituel , ce ...)
(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"
.
(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-response
pour produire sa réponse; la {:root "./static"}
partie lui indique où chercher le fichier.
(ANY "*" [] ...)
- un itinéraire fourre-tout. Il est conseillé à Compojure de toujours inclure une telle route à la fin d'un defroutes
formulaire 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 :cookies
clé à la requête,wrap-params
ajoute :query-params
et / ou:form-params
si 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-response
ne 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.
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.
Alors, quels sont ces gestionnaires d'anneau ? Extrait de la doc:
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/util
bibliothèque.Maintenant, nous voulons appeler différents gestionnaires en fonction de la demande. Nous pourrions faire du routage statique comme ceci:
Et refactorisez-le comme ceci:
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".
À présent, nous commençons à voir du code qui semble pouvoir être factorisé, à l'aide d'une macro. Compojure fournit un
defroutes
macro:Compojure fournit d'autres macros, comme le
GET
macro: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.
la source
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 a
let
aidé à clarifier le tout "d'où viennent les valeurs magiques?" question.Je colle les sections pertinentes ci-dessous:
la source
Je n'ai pas encore commencé sur les trucs Web clojure mais, je le ferai, voici les trucs que j'ai mis en signet.
la source
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:
un exemple plus avancé, montrant la déstructuration imbriquée:
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.
la source