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:
Permintaan tiba dan diubah menjadi peta Clojure sesuai dengan spesifikasi Ring.
Peta ini disalurkan ke dalam apa yang disebut "fungsi penangan", yang diharapkan menghasilkan respons (yang juga merupakan peta Clojure).
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-method
yang :head
sebaliknya, respon akan nil
. Kami akan kembali ke pertanyaan tentang apa nil
artinya di sini sebentar lagi (tetapi perhatikan bahwa ini bukan Ring respose yang valid!).
Seperti yang terlihat dari contoh ini, example-route
ini hanyalah sebuah fungsi, dan yang sangat sederhana; itu melihat permintaan, menentukan apakah tertarik untuk menanganinya (dengan memeriksa :request-method
dan :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/render
multimetode untuk mengetahui detailnya (kode sepenuhnya mendokumentasikan sendiri di sini).
Ayo coba gunakan defroutes
sekarang:
(defroutes example-routes
(GET "/" [] "get")
(HEAD "/" [] "head"))
Tanggapan untuk permintaan contoh yang ditampilkan di atas dan variannya dengan :request-method :head
seperti yang diharapkan.
Cara kerja bagian dalam example-routes
sedemikian rupa sehingga setiap rute dicoba secara bergantian; segera setelah salah satu dari mereka mengembalikan non- nil
respons, respons itu menjadi nilai kembalian dari seluruh example-routes
handler. Sebagai kenyamanan tambahan, defroutes
-defined handler dibungkus wrap-params
dan wrap-cookies
secara 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 assoc
menambah informasi ke permintaan pada tahap pencocokan:
(def echo-first-path-component-route
(GET "/:fst/*" [fst] fst))
Ini merespon dengan :body
dari "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 :fst
di atas). Yang kedua adalah cara yang disederhanakan untuk mencocokkan :params
entri 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:
(GET "/" [] (workbench))
- saat menangani GET
permintaan dengan :uri "/"
, panggil fungsi workbench
dan render apa pun yang dikembalikan ke peta respons. (Ingat kembali bahwa nilai yang dikembalikan mungkin berupa peta, tetapi juga string, dll.)
(POST "/save" {form-params :form-params} (str form-params))
- :form-params
adalah entri dalam peta permintaan yang disediakan oleh wrap-params
middleware (ingat bahwa ini secara implisit disertakan oleh defroutes
). Responsnya akan standar {:status 200 :headers {"Content-Type" "text/html"} :body ...}
dengan (str form-params)
diganti ...
. ( POST
Penangan yang sedikit tidak biasa , ini ...)
(GET "/test" [& more] (str "<pre> more "</pre>"))
- ini akan misalnya menggemakan kembali representasi string peta {"foo" "1"}
jika agen pengguna memintanya "/test?foo=1"
.
(GET ["/:filename" :filename #".*"] [filename] ...)
- :filename #".*"
bagian tidak melakukan apa pun (karena #".*"
selalu cocok). Ini memanggil fungsi utilitas Ring ring.util.response/file-response
untuk menghasilkan responsnya; yang {:root "./static"}
bagian mengatakan itu di mana untuk mencari file.
(ANY "*" [] ...)
- rute tangkap semua. Merupakan praktik Compojure yang baik untuk selalu menyertakan rute seperti itu di akhir defroutes
formulir 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 :cookies
kunci ke permintaan, wrap-params
penambahan :query-params
dan / atau:form-params
jika 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-response
tidak 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.