Compojure rotalarının arkasındaki "büyük fikir" nedir?


109

Clojure'da yeniyim ve Compojure'u temel bir web uygulaması yazmak için kullanıyorum. Yine de Compojure'un defroutessözdizimi ile bir duvara çarpıyorum ve sanırım her şeyin arkasındaki "nasıl" ve "neden" i anlamam gerekiyor.

Halka tarzı bir uygulama bir HTTP istek eşlemi ile başlıyor gibi görünüyor, ardından isteği tarayıcıya geri gönderilen bir yanıt eşlemesine dönüştürülene kadar bir dizi ara yazılım işlevinden geçiriyor. Bu tarz geliştiriciler için çok "düşük seviye" görünüyor, dolayısıyla Compojure gibi bir araca ihtiyaç duyuluyor. Diğer yazılım ekosistemlerinde de, özellikle Python'un WSGI'sinde daha fazla soyutlama ihtiyacını görebiliyorum.

Sorun şu ki Compojure'un yaklaşımını anlamıyorum. Aşağıdaki defroutesS ifadesini alalım :

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

Tüm bunları anlamanın anahtarının makro vududa yattığını biliyorum, ancak makroları tam olarak anlamıyorum (henüz). defroutesUzun zamandır kaynağa baktım , ama anlamayın! Burada neler oluyor? "Büyük fikri" anlamak muhtemelen şu belirli soruları yanıtlamama yardımcı olacaktır:

  1. Ring ortamına yönlendirilmiş bir işlevden (ör. workbenchİşlev) nasıl erişebilirim ? Örneğin, HTTP_ACCEPT başlıklarına veya isteğin / ara yazılımın başka bir kısmına erişmek istediğimi varsayalım.
  2. Destroyturing ( {form-params :form-params}) ile anlaşma nedir ? İmha ederken benim için hangi anahtar kelimeler mevcut?

Clojure'u gerçekten seviyorum ama çok şaşkınım!

Yanıtlar:


212

Compojure açıkladı (bir dereceye kadar)

NB. Compojure 0.4.1 ile çalışıyorum ( GitHub'daki 0.4.1 sürüm kaydı burada ).

Neden?

En tepesinde compojure/core.clj, Compojure'un amacının şu yararlı özeti var:

Halka işleyicileri oluşturmak için kısa bir sözdizimi.

Yüzeysel bir düzeyde, "neden" sorusunun hepsi bu kadar. Biraz daha derine inmek için, Ring tarzı bir uygulamanın nasıl çalıştığına bir göz atalım:

  1. Ring spesifikasyonuna göre bir istek gelir ve Clojure haritasına dönüştürülür.

  2. Bu harita, bir yanıt üretmesi beklenen (aynı zamanda bir Clojure haritasıdır) "işleyici işlevi" olarak adlandırılan bir işleve dönüştürülür.

  3. Yanıt haritası, gerçek bir HTTP yanıtına dönüştürülür ve istemciye geri gönderilir.

Yukarıdaki 2. Adım en ilginç olanıdır, çünkü istekte kullanılan URI'yi incelemek, herhangi bir çerezi incelemek vb. Ve nihayetinde uygun bir yanıta ulaşmak işleyicinin sorumluluğundadır. Açıkça görülüyor ki, tüm bu çalışmaların iyi tanımlanmış parçalardan oluşan bir koleksiyona dahil edilmesi gerekiyor; bunlar normalde bir "temel" işleyici işlevi ve onu saran bir ara yazılım işlevleri koleksiyonudur. Compojure'un amacı, temel işleyici işlevinin oluşturulmasını basitleştirmektir.

Nasıl?

Compojure, "rotalar" kavramı etrafında inşa edilmiştir. Bunlar aslında Clout kitaplığı (Compojure projesinin bir yan ürünü - 0.3.x -> 0.4.x geçişinde birçok şey ayrı kitaplıklara taşındı) Clout kitaplığı tarafından daha derin bir düzeyde uygulanır . Bir rota, (1) bir HTTP yöntemi (GET, PUT, HEAD ...), (2) bir URI modeli (Webby Rubyistlerine aşina olacak olan sözdizimi ile belirtilir), (3) kullanılan bir yıkıcı form ile tanımlanır. istek eşlemesinin kısımlarını gövdede bulunan isimlere bağlamak, (4) geçerli bir Halka yanıtı üretmesi gereken bir ifade gövdesi (önemsiz olmayan durumlarda bu genellikle sadece ayrı bir işleve çağrıdır).

Bu, basit bir örneğe bakmak için iyi bir nokta olabilir:

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

Bunu REPL'de test edelim (aşağıdaki istek haritası, minimum geçerli Halka istek haritasıdır):

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

Eğer :request-methodvardı :headbunun yerine, tepki olacaktır nil. nilBir dakika içinde burada ne anlama geldiği sorusuna döneceğiz (ancak bunun geçerli bir Yüzük tepkisi olmadığına dikkat edin!).

Bu örnekten de anlaşılacağı gibi, example-routesadece bir fonksiyondur ve bunda çok basittir; o, istek bakar o (inceleyerek kullanırken bazı ilgilendiği belirler :request-methodve :uriböylece, temel bir tepki harita dönerse,) ve.

Aynı zamanda aşikar olan şey, yolun gövdesinin gerçekten uygun bir yanıt haritasına değerlendirmeye ihtiyaç duymamasıdır; Compojure, dizeler (yukarıda görüldüğü gibi) ve bir dizi başka nesne türü için mantıklı varsayılan işlem sağlar; ayrıntılar için compojure.response/renderçoklu yönteme bakın (kod burada tamamen kendi kendini belgelemektedir).

Şimdi kullanmayı deneyelim defroutes:

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

Yukarıda görüntülenen örnek talebe ve varyantına verilen yanıtlar :request-method :headbeklendiği gibidir.

İç işleyişi example-routesöyle ki, her bir yol sırayla denenecek; bunlardan biri nilyanıt vermeyen döndürür döndürmez , bu yanıt tüm example-routesişleyicinin dönüş değeri olur . Ek bir kolaylık olarak, defroutes-defined işleyicileri sarılır wrap-paramsve wrap-cookiesdolaylı.

İşte daha karmaşık bir rota örneği:

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

Önceden kullanılan boş vektörün yerine yıkıcı forma dikkat edin. Buradaki temel fikir, rotanın gövdesinin taleple ilgili bazı bilgilerle ilgilenebileceğidir; bu her zaman bir harita biçiminde geldiği için, talepten bilgi çıkarmak ve onu yolun gövdesinde kapsam dahilinde olacak yerel değişkenlere bağlamak için ilişkisel bir yok etme formu sağlanabilir.

Yukarıdakilerin bir testi:

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

Yukarıdakilere yönelik parlak takip fikri, daha karmaşık rotaların assoc, eşleştirme aşamasında isteğe ek bilgi verebilmesidir :

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

Bu , önceki örnekteki isteğe bir :body/ ile yanıt verir "foo".

Bu son örnekte iki şey yenidir: "/:fst/*"ve boş olmayan bağlanma vektörü [fst]. Birincisi, URI modelleri için yukarıda bahsedilen Rails ve Sinatra benzeri sözdizimidir. Yukarıdaki örnekten anlaşılandan biraz daha karmaşıktır, çünkü URI segmentlerindeki düzenli ifade kısıtlamaları desteklenir (örneğin ["/:fst/*" :fst #"[0-9]+"], rotanın sadece :fstyukarıdakinin tüm rakamlı değerlerini kabul etmesini sağlamak için sağlanabilir ). İkincisi, :paramskendisi bir harita olan istek eşlemesindeki girdiyi eşleştirmenin basitleştirilmiş bir yoludur ; istek, sorgu dizesi parametreleri ve form parametrelerinden URI segmentlerini çıkarmak için kullanışlıdır. İkinci noktayı açıklamak için bir örnek:

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

Soru metnindeki örneğe bir göz atmak için iyi bir zaman olabilir:

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

Her rotayı sırayla inceleyelim:

  1. (GET "/" [] (workbench))- ile bir GETistekle uğraşırken :uri "/", işlevi çağırın workbenchve döndürdüğü şeyi bir yanıt haritasına işleyin. (Dönüş değerinin bir harita, aynı zamanda bir dize vb. Olabileceğini hatırlayın.)

  2. (POST "/save" {form-params :form-params} (str form-params))- ara yazılım :form-paramstarafından sağlanan istek eşlemesindeki bir giriştir wrap-params(örtük olarak tarafından dahil edildiğini hatırlayın defroutes). Yanıtı standart olacak {:status 200 :headers {"Content-Type" "text/html"} :body ...}ile (str form-params)ikame .... (Biraz alışılmadık bir POSTişleyici, bu ...)

  3. (GET "/test" [& more] (str "<pre> more "</pre>"))- bu, örneğin {"foo" "1"}kullanıcı aracısı isterse haritanın dizgi temsilini geri yansıtır "/test?foo=1".

  4. (GET ["/:filename" :filename #".*"] [filename] ...)- :filename #".*"parça hiçbir şey yapmaz (çünkü #".*"her zaman eşleşir). ring.util.response/file-responseYanıtını üretmek için Ring yardımcı işlevi işlevini çağırır ; {:root "./static"}bölüm nerede dosyaya bakmak için söyler.

  5. (ANY "*" [] ...)- tümünü kapsayan bir rota. Compojure uygulaması defroutes, tanımlanmakta olan işleyicinin her zaman geçerli bir Halka yanıt haritası döndürmesini sağlamak için bir formun sonuna her zaman böyle bir yol eklemek iyi bir yöntemdir (bir yol eşleştirme başarısızlığının neden olduğunu hatırlayın nil).

Neden bu şekilde?

Ring ara yazılımının bir amacı, istek haritasına bilgi eklemektir; böylece tanımlama bilgisi işleme ara yazılımı :cookiesisteğe bir anahtar wrap-paramsekler , ekler :query-paramsve / veya:form-paramsbir sorgu dizesi / form verisi varsa vb. (Daha doğrusu, ara yazılım işlevlerinin eklediği tüm bilgiler istek haritasında zaten mevcut olmalıdır, çünkü geçtikleri şey budur; görevleri, onu, sardıkları işleyicilerle çalışmak için daha uygun hale getirmektir.) Nihayetinde, "zenginleştirilmiş" istek, ara yazılım tarafından eklenen güzel bir şekilde önceden işlenmiş tüm bilgilerle istek haritasını inceleyen ve bir yanıt üreten temel işleyiciye iletilir. (Ara yazılım, bundan daha karmaşık şeyler yapabilir - birkaç "iç" işleyiciyi sarmak ve aralarında seçim yapmak, sarmalanmış işleyicileri çağırıp çağırmamaya karar vermek vb. Ancak, bu yanıtın kapsamı dışındadır.)

Temel işleyici, genellikle (önemsiz olmayan durumlarda) istek hakkında yalnızca bir avuç bilgi öğesine ihtiyaç duyan bir işlevdir. (Örneğin ring.util.response/file-response, talebin çoğu umurunda değildir; yalnızca bir dosya adına ihtiyaç duyar.) Bu nedenle, bir Ring isteğinin yalnızca ilgili kısımlarını çıkarmanın basit bir yoluna ihtiyaç vardır. Compojure, tam da bunu yapan özel amaçlı bir kalıp eşleştirme motoru sağlamayı amaçlamaktadır.


3
"Ek bir kolaylık olarak, defroutes tanımlı işleyiciler sarma parametrelerine ve örtülü çerezlere sarılır." - 0.6.0 sürümünden itibaren bunları açıkça eklemeniz gerekmektedir. Ref github.com/weavejester/compojure/commit/…
Dan Midwood

3
Çok iyi dedin. Bu cevap Compojure'un ana sayfasında olmalıdır.
Siddhartha Reddy

2
Compojure'da yeni olan herkes için gerekli okuma. Konuyla ilgili her wiki ve blog gönderisinin buna bir bağlantıyla başlamasını dilerim.
jemmons

7

Booleanknot.com'da James Reeves'den (Compojure'un yazarı) mükemmel bir makale var ve onu okumak benim için "tık" oldu, bu yüzden bir kısmını buraya yeniden yazdım (gerçekten yaptığım şey buydu).

Burada aynı yazarın tam da bu soruyu yanıtlayan bir kayar tablası da var .

Compojure, http istekleri için bir soyutlama olan Ring'e dayanmaktadır .

A concise syntax for generating Ring handlers.

Peki bu Yüzük işleyicileri nedir? Belgeden çıkartın:

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

Oldukça basit, ama aynı zamanda oldukça düşük seviyeli. Yukarıdaki işleyici, ring/utilkitaplık kullanılarak daha net bir şekilde tanımlanabilir .

(use 'ring.util.response)

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

Şimdi isteğe bağlı olarak farklı işleyicileri aramak istiyoruz. Bunun gibi bazı statik yönlendirme yapabiliriz:

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

Ve şu şekilde yeniden düzenleyin:

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

O zaman James'in not ettiği ilginç şey, bunun iç içe geçmiş rotalara izin vermesidir, çünkü "iki veya daha fazla rotayı bir araya getirmenin sonucunun kendisi bir rotadır".

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

Şimdiye kadar, makro kullanılarak çarpanlarına ayrılabilecek gibi görünen bazı kodlar görmeye başlıyoruz. Compojure bir defroutesmakro sağlar:

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

;; is identical to

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

Compojure, GETmakro gibi diğer makroları sağlar :

(GET "/a" [] "Alpha")

;; will expand to

(fn [request#]
  (if (and (= (:request-method request#) ~http-method)
           (= (:uri request#) ~uri))
    (let [~bindings request#]
      ~@body)))

Oluşturulan son işlev bizim işleyicimize benziyor!

Daha ayrıntılı açıklamalara girdiği için lütfen James gönderisine göz attığınızdan emin olun .


4

Hala rotalarda neler olup bittiğini öğrenmekte zorlanan biri için, benim gibi yıkım fikrini anlamıyor olabilirsiniz.

Aslında dokümanlarılet okumak , "sihirli değerler nereden geliyor?" soru.

Aşağıdaki ilgili bölümleri yapıştırıyorum:

Clojure, let bağlama listelerinde, fn parametre listelerinde ve bir let veya fn'ye genişleyen herhangi bir makro içinde genellikle yıkım olarak adlandırılan soyut yapısal bağlamayı destekler. Temel fikir, bir bağlama formunun, init ifadesinin ilgili bölümlerine bağlanan semboller içeren bir veri yapısı olabileceğidir. Bağlama soyuttur, çünkü bir vektör değişmezi sıralı olan herhangi bir şeye bağlanabilirken, bir harita değişmezi birleşik olan herhangi bir şeye bağlanabilir.

Vektör bağlama ifadeleri, adları vektörler, listeler, sıralar, dizeler, diziler ve nth'yi destekleyen herhangi bir şey gibi sıralı şeylerin (yalnızca vektörlerin değil) bölümlerine bağlamanıza izin verir. Temel ardışık form, nth aracılığıyla bakılan init-expr'den ardışık öğelere bağlanacak olan bağlanma formlarının bir vektörüdür. Ek olarak ve isteğe bağlı olarak ve ardından bir bağlanma formları, bu bağlanma formunun sekansın geri kalanına bağlanmasına, yani henüz bağlanmamış olan kısmın nthnext aracılığıyla aranmasına neden olacaktır. Son olarak, isteğe bağlı olarak da: Bir sembolün ardından geldiği gibi, bu sembolün tüm init-expr'a bağlanmasına neden olur:

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

Vektör bağlama ifadeleri, adları vektörler, listeler, sıralar, dizeler, diziler ve nth'yi destekleyen herhangi bir şey gibi sıralı şeylerin (yalnızca vektörlerin değil) bölümlerine bağlamanıza izin verir. Temel ardışık form, nth aracılığıyla bakılan init-expr'den ardışık öğelere bağlanacak olan bağlanma formlarının bir vektörüdür. Ek olarak ve isteğe bağlı olarak ve ardından bir bağlanma formları, bu bağlanma formunun sekansın geri kalanına bağlanmasına, yani henüz bağlanmamış olan kısmın nthnext aracılığıyla aranmasına neden olacaktır. Son olarak, isteğe bağlı olarak da: Bir sembolün ardından geldiği gibi, bu sembolün tüm init-expr'a bağlanmasına neden olur:

(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

Teşekkürler, bu bağlantılar kesinlikle faydalıdır. Günün daha iyi bir bölümünde bu problem üzerinde çalışıyorum ve onunla daha iyi bir yerdeyim ... Bir noktada bir takip yazısı yayınlamaya çalışacağım.
Sean Woods

1

Yıkımla ilgili anlaşma nedir ({form-params: form-params})? İmha ederken benim için hangi anahtar kelimeler mevcut?

Mevcut tuşlar, giriş haritasındakilerdir. İmha etme, let ve dozq formlarında veya fn veya tanımlı parametrelerin içinde mevcuttur

Aşağıdaki kod umarız bilgilendirici olacaktır:

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

iç içe geçmiş yıkımı gösteren daha gelişmiş bir örnek:

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

Akıllıca kullanıldığında, yok etme, standart veri erişiminden kaçınarak kodunuzu düzene sokar. : kullanarak ve sonucu (veya sonucun anahtarlarını) yazdırarak başka hangi verilere erişebileceğiniz hakkında daha iyi bir fikir edinebilirsiniz.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.