Apa "ide besar" di balik rute compojure?

109

Saya baru mengenal Clojure dan telah menggunakan Compojure untuk menulis aplikasi web dasar. Saya mengalami masalah dengan defroutessintaks Compojure , dan saya pikir saya perlu memahami "bagaimana" dan "mengapa" di balik itu semua.

Sepertinya aplikasi Ring-style dimulai dengan peta permintaan HTTP, lalu meneruskan permintaan melalui serangkaian fungsi middleware hingga diubah menjadi peta respons, yang dikirim kembali ke browser. Gaya ini sepertinya terlalu "level rendah" bagi developer, oleh karena itu dibutuhkan alat seperti Compojure. Saya dapat melihat kebutuhan ini untuk lebih banyak abstraksi di ekosistem perangkat lunak lain juga, terutama dengan WSGI Python.

Masalahnya adalah saya tidak mengerti pendekatan Compojure. Mari kita ambil defroutesekspresi-S berikut :

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

Saya tahu bahwa kunci untuk memahami semua ini terletak di dalam beberapa makro voodoo, tetapi saya belum sepenuhnya memahami makro. Saya telah menatap defroutessumbernya untuk waktu yang lama, tetapi jangan mengerti! Apa yang terjadi di sini? Memahami "ide besar" mungkin akan membantu saya menjawab pertanyaan khusus berikut:

  1. Bagaimana cara mengakses lingkungan Dering dari dalam fungsi yang dirutekan (mis. workbenchFungsi)? Misalnya, saya ingin mengakses header HTTP_ACCEPT atau bagian lain dari request / middleware?
  2. Apa masalahnya dengan destructuring ( {form-params :form-params})? Kata kunci apa yang tersedia untuk saya saat merusak?

Saya sangat suka Clojure tetapi saya sangat bingung!

Sean Woods
sumber

Jawaban:

212

Compojure menjelaskan (sampai taraf tertentu)

NB. Saya bekerja dengan Compojure 0.4.1 ( inilah rilis 0.4.1 commit di GitHub).

Mengapa?

Di bagian paling atas compojure/core.clj, ada ringkasan berguna tentang tujuan Compojure:

Sintaks yang ringkas untuk menghasilkan penangan Ring.

Pada tingkat yang dangkal, hanya itu yang ada pada pertanyaan "mengapa". Untuk lebih mendalami, mari kita lihat bagaimana aplikasi Ring-style berfungsi:

  1. Permintaan tiba dan diubah menjadi peta Clojure sesuai dengan spesifikasi Ring.

  2. Peta ini disalurkan ke dalam apa yang disebut "fungsi penangan", yang diharapkan menghasilkan respons (yang juga merupakan peta Clojure).

  3. Peta respons diubah menjadi respons HTTP aktual dan dikirim kembali ke klien.

Langkah 2. di atas adalah yang paling menarik, karena merupakan tanggung jawab pawang untuk memeriksa URI yang digunakan dalam permintaan, memeriksa cookie apa pun, dll. Dan akhirnya sampai pada respons yang sesuai. Jelaslah bahwa semua pekerjaan ini perlu difaktorkan ke dalam kumpulan potongan-potongan yang terdefinisi dengan baik; ini biasanya merupakan fungsi penangan "dasar" dan kumpulan fungsi middleware yang membungkusnya. Tujuan Compojure adalah untuk menyederhanakan pembuatan fungsi base handler.

Bagaimana?

Compojure dibangun di sekitar gagasan "rute". Ini sebenarnya diterapkan pada tingkat yang lebih dalam oleh Pengaruh perpustakaan (spin-off dari proyek Compojure - banyak hal dipindahkan ke perpustakaan terpisah di 0.3.x -> 0.4.x transisi). Rute didefinisikan oleh (1) metode HTTP (GET, PUT, HEAD ...), (2) pola URI (ditentukan dengan sintaks yang tampaknya familier bagi Webby Rubyists), (3) bentuk destrukturisasi yang digunakan dalam mengikat bagian dari peta permintaan ke nama yang tersedia di tubuh, (4) badan ekspresi yang perlu menghasilkan respons Dering yang valid (dalam kasus non-sepele ini biasanya hanya panggilan ke fungsi terpisah).

Ini mungkin poin yang bagus untuk melihat contoh sederhana:

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

Mari kita uji ini di REPL (peta permintaan di bawah ini adalah peta permintaan Dering minimal yang valid):

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

Jika :request-methodyang :headsebaliknya, respon akan nil. Kami akan kembali ke pertanyaan tentang apa nilartinya di sini sebentar lagi (tetapi perhatikan bahwa ini bukan Ring respose yang valid!).

Seperti yang terlihat dari contoh ini, example-routeini hanyalah sebuah fungsi, dan yang sangat sederhana; itu melihat permintaan, menentukan apakah tertarik untuk menanganinya (dengan memeriksa :request-methoddan :uri) dan, jika demikian, mengembalikan peta respons dasar.

Yang juga jelas adalah bahwa badan rute tidak perlu dievaluasi ke peta respons yang tepat; Compojure menyediakan penanganan default yang waras untuk string (seperti yang terlihat di atas) dan sejumlah tipe objek lainnya; lihat compojure.response/rendermultimetode untuk mengetahui detailnya (kode sepenuhnya mendokumentasikan sendiri di sini).

Ayo coba gunakan defroutessekarang:

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

Tanggapan untuk permintaan contoh yang ditampilkan di atas dan variannya dengan :request-method :headseperti yang diharapkan.

Cara kerja bagian dalam example-routessedemikian rupa sehingga setiap rute dicoba secara bergantian; segera setelah salah satu dari mereka mengembalikan non- nilrespons, respons itu menjadi nilai kembalian dari seluruh example-routeshandler. Sebagai kenyamanan tambahan, defroutes-defined handler dibungkus wrap-paramsdan wrap-cookiessecara implisit.

Berikut contoh rute yang lebih kompleks:

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

Perhatikan bentuk penghancuran sebagai ganti vektor kosong yang sebelumnya digunakan. Ide dasarnya di sini adalah badan rute mungkin tertarik pada beberapa informasi tentang permintaan tersebut; karena ini selalu datang dalam bentuk peta, bentuk penghancuran asosiatif dapat diberikan untuk mengekstrak informasi dari permintaan dan mengikatnya ke variabel lokal yang akan berada dalam ruang lingkup di badan rute.

Tes di atas:

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

Ide tindak lanjut yang brilian di atas adalah bahwa rute yang lebih kompleks dapat assocmenambah informasi ke permintaan pada tahap pencocokan:

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

Ini merespon dengan :bodydari "foo"untuk permintaan dari contoh sebelumnya.

Dua hal baru tentang contoh terbaru ini: "/:fst/*"vektor penjilidan yang tidak kosong dan tidak kosong [fst]. Yang pertama adalah sintaks Rails-and-Sinatra-like yang disebutkan di atas untuk pola URI. Ini sedikit lebih canggih daripada yang terlihat dari contoh di atas di mana batasan regex pada segmen URI didukung (misalnya ["/:fst/*" :fst #"[0-9]+"]dapat disediakan untuk membuat rute hanya menerima nilai semua digit :fstdi atas). Yang kedua adalah cara yang disederhanakan untuk mencocokkan :paramsentri di peta permintaan, yang juga merupakan peta; berguna untuk mengekstrak segmen URI dari permintaan, parameter string kueri, dan parameter formulir. Contoh untuk mengilustrasikan poin terakhir:

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

Ini akan menjadi saat yang tepat untuk melihat contoh dari teks pertanyaan:

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

Mari kita analisis setiap rute secara bergantian:

  1. (GET "/" [] (workbench))- saat menangani GETpermintaan dengan :uri "/", panggil fungsi workbenchdan render apa pun yang dikembalikan ke peta respons. (Ingat kembali bahwa nilai yang dikembalikan mungkin berupa peta, tetapi juga string, dll.)

  2. (POST "/save" {form-params :form-params} (str form-params))- :form-paramsadalah entri dalam peta permintaan yang disediakan oleh wrap-paramsmiddleware (ingat bahwa ini secara implisit disertakan oleh defroutes). Responsnya akan standar {:status 200 :headers {"Content-Type" "text/html"} :body ...}dengan (str form-params)diganti .... ( POSTPenangan yang sedikit tidak biasa , ini ...)

  3. (GET "/test" [& more] (str "<pre> more "</pre>"))- ini akan misalnya menggemakan kembali representasi string peta {"foo" "1"}jika agen pengguna memintanya "/test?foo=1".

  4. (GET ["/:filename" :filename #".*"] [filename] ...)- :filename #".*"bagian tidak melakukan apa pun (karena #".*"selalu cocok). Ini memanggil fungsi utilitas Ring ring.util.response/file-responseuntuk menghasilkan responsnya; yang {:root "./static"}bagian mengatakan itu di mana untuk mencari file.

  5. (ANY "*" [] ...)- rute tangkap semua. Merupakan praktik Compojure yang baik untuk selalu menyertakan rute seperti itu di akhir defroutesformulir untuk memastikan bahwa penangan yang didefinisikan selalu mengembalikan peta respons Dering yang valid (ingat bahwa kegagalan pencocokan rute menghasilkan nil).

Kenapa begini?

Salah satu tujuan middleware Ring adalah untuk menambahkan informasi ke peta permintaan; dengan demikian middleware penanganan cookie menambahkan :cookieskunci ke permintaan, wrap-paramspenambahan :query-paramsdan / atau:form-paramsjika ada data string / formulir kueri dan seterusnya. (Sebenarnya, semua informasi yang ditambahkan oleh fungsi middleware harus sudah ada di peta permintaan, karena itulah yang diteruskan; tugas mereka adalah mengubahnya agar lebih nyaman untuk digunakan dalam penangan yang mereka bungkus.) Pada akhirnya, permintaan yang "diperkaya" diteruskan ke penangan dasar, yang memeriksa peta permintaan dengan semua informasi yang telah diproses dengan baik yang ditambahkan oleh middleware dan menghasilkan respons. (Middleware dapat melakukan hal-hal yang lebih kompleks dari itu - seperti membungkus beberapa penangan "dalam" dan memilih di antara mereka, memutuskan apakah akan memanggil penangan yang dibungkus atau tidak, dll. Namun, itu di luar cakupan jawaban ini.)

Penangan dasar, pada gilirannya, biasanya (dalam kasus-kasus non-sepele) sebuah fungsi yang cenderung hanya membutuhkan beberapa item informasi tentang permintaan tersebut. (Misalnya ring.util.response/file-responsetidak peduli tentang sebagian besar permintaan; itu hanya membutuhkan nama file.) Oleh karena itu kebutuhan akan cara sederhana untuk mengekstrak hanya bagian yang relevan dari permintaan Ring. Compojure bertujuan untuk menyediakan mesin pencocokan pola tujuan khusus, yang berfungsi seperti itu.

Michał Marczyk
sumber
3
"Sebagai kenyamanan tambahan, penangan yang ditentukan defroutes dibungkus dalam wrap-params dan wrap-cookies secara implisit." - Mulai versi 0.6.0 Anda harus menambahkan ini secara eksplisit. Ref github.com/weavejester/compojure/commit/…
Dan Midwood
3
Sangat bagus. Jawaban ini seharusnya ada di beranda Compojure.
Siddhartha Reddy
2
Bacaan wajib bagi siapa pun yang baru mengenal Compojure. Saya berharap setiap postingan wiki dan blog tentang topik ini dimulai dengan tautan ke ini.
jemmons
7

Ada sebuah artikel bagus di booleanknot.com dari James Reeves (penulis Compojure), dan membacanya membuat saya "klik", jadi saya telah menerjemahkan ulang beberapa di sini (sebenarnya hanya itu yang saya lakukan).

Ada juga slidedeck di sini dari penulis yang sama , yang menjawab pertanyaan persis ini.

Komposisi didasarkan pada Ring , yang merupakan abstraksi untuk permintaan http.

A concise syntax for generating Ring handlers.

Jadi, apakah Ring handler itu ? Ekstrak dari dokumen:

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

Cukup sederhana, tetapi juga levelnya cukup rendah. Penangan di atas dapat didefinisikan secara lebih ringkas menggunakan ring/utilpustaka.

(use 'ring.util.response)

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

Sekarang kami ingin memanggil penangan yang berbeda tergantung pada permintaan. Kami dapat melakukan beberapa perutean statis seperti:

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

Dan lakukan refaktorisasi seperti ini:

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

Hal menarik yang dicatat James adalah bahwa ini memungkinkan rute bersarang, karena "hasil dari menggabungkan dua atau lebih rute bersama-sama dengan sendirinya adalah sebuah rute".

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

Sekarang, kita mulai melihat beberapa kode yang sepertinya dapat difaktorkan, menggunakan makro. Compojure menyediakan defroutesmakro:

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

;; is identical to

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

Compojure menyediakan makro lain, seperti GETmakro:

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

;; will expand to

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

Fungsi terakhir yang dihasilkan terlihat seperti penangan kita!

Harap pastikan untuk memeriksa posting James , karena ini masuk ke penjelasan yang lebih rinci.

nha
sumber
4

Bagi siapa pun yang masih berjuang untuk mengetahui apa yang terjadi dengan rutenya, mungkin, seperti saya, Anda tidak memahami gagasan merusak.

Sebenarnya membaca dokumen untuklet membantu menjelaskan keseluruhan "dari mana nilai-nilai ajaib itu berasal?" pertanyaan.

Saya menempelkan bagian yang relevan di bawah ini:

Clojure mendukung pengikatan struktural abstrak, yang sering disebut penghancuran, di daftar pengikat biarkan, daftar parameter fn, dan makro apa pun yang diperluas menjadi let atau fn. Ide dasarnya adalah bahwa bentuk-mengikat dapat menjadi struktur data literal yang berisi simbol-simbol yang terikat ke masing-masing bagian dari init-expr. Pengikatan bersifat abstrak karena literal vektor dapat mengikat apa pun yang berurutan, sedangkan literal peta dapat mengikat ke apa pun yang bersifat asosiatif.

Vector binding-exprs memungkinkan Anda untuk mengikat nama ke bagian-bagian yang berurutan (bukan hanya vektor), seperti vektor, daftar, urutan, string, array, dan apa pun yang mendukung ke-n. Bentuk sekuensial dasar adalah vektor bentuk pengikatan, yang akan terikat ke elemen berurutan dari init-expr, dicari melalui n. Sebagai tambahan, dan secara opsional, & diikuti oleh bentuk-penjilidan akan menyebabkan bentuk-penjilidan terikat ke sisa urutan, yaitu bagian yang belum terikat, dicari melalui nthnext. Terakhir, juga opsional,: diikuti dengan simbol akan menyebabkan simbol tersebut terikat ke seluruh 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 memungkinkan Anda untuk mengikat nama ke bagian-bagian yang berurutan (bukan hanya vektor), seperti vektor, daftar, urutan, string, array, dan apa pun yang mendukung ke-n. Bentuk sekuensial dasar adalah vektor bentuk pengikatan, yang akan terikat ke elemen berurutan dari init-expr, dicari melalui n. Sebagai tambahan, dan secara opsional, & diikuti oleh bentuk-penjilidan akan menyebabkan bentuk-penjilidan terikat ke sisa urutan, yaitu bagian yang belum terikat, dicari melalui nthnext. Terakhir, juga opsional,: diikuti dengan simbol akan menyebabkan simbol tersebut terikat ke seluruh 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
sumber
3

Saya belum memulai hal-hal web clojure tetapi, saya akan, ini hal-hal yang saya tandai.

nickik
sumber
Terima kasih, tautan ini sangat membantu. Saya telah mengatasi masalah ini untuk bagian yang lebih baik hari ini dan berada di tempat yang lebih baik dengannya ... Saya akan mencoba memposting tindak lanjut di beberapa titik.
Sean Woods
1

Apa masalahnya dengan destrukturisasi ({form-params: form-params})? Kata kunci apa yang tersedia untuk saya saat merusak?

Kunci yang tersedia adalah yang ada di peta masukan. Destrukturisasi tersedia di dalam formulir let dan doseq, atau di dalam parameter ke fn atau defn

Kode berikut mudah-mudahan 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)]

contoh yang lebih canggih, menampilkan destrukturisasi bersarang:

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

Jika digunakan dengan bijak, perusakan mendeklarasikan kode Anda dengan menghindari akses data boilerplate. dengan menggunakan: sebagai dan mencetak hasilnya (atau kunci hasil) Anda bisa mendapatkan gambaran yang lebih baik tentang data lain yang bisa Anda akses.

pembuat suara
sumber