Compojure giải thích (ở một mức độ nào đó)
NB. Tôi đang làm việc với Compojure 0.4.1 ( đây là cam kết phát hành 0.4.1 trên GitHub).
Tại sao?
Ở phần đầu compojure/core.clj
, có bản tóm tắt hữu ích về mục đích của Compojure:
Cú pháp ngắn gọn để tạo trình xử lý Ring.
Ở mức độ bề ngoài, đó là tất cả những gì cần có cho câu hỏi "tại sao". Để đi sâu hơn một chút, chúng ta hãy xem cách ứng dụng kiểu Ring hoạt động:
Một yêu cầu đến và được chuyển thành một bản đồ Clojure phù hợp với thông số của Ring.
Bản đồ này được tạo thành một cái gọi là "hàm xử lý", được mong đợi để tạo ra một phản hồi (cũng là một bản đồ Clojure).
Bản đồ phản hồi được chuyển đổi thành phản hồi HTTP thực tế và được gửi lại cho máy khách.
Bước 2. ở trên là thú vị nhất, vì người xử lý có trách nhiệm kiểm tra URI được sử dụng trong yêu cầu, kiểm tra bất kỳ cookie nào, v.v. và cuối cùng đi đến phản hồi thích hợp. Rõ ràng, tất cả công việc này cần được tính toán thành một tập hợp các phần được xác định rõ ràng; đây thường là một hàm xử lý "cơ sở" và một tập hợp các hàm phần mềm trung gian bao bọc nó. Mục đích của Compojure là đơn giản hóa việc tạo ra hàm xử lý cơ sở.
Làm sao?
Compojure được xây dựng xung quanh khái niệm "các tuyến đường". Những điều này thực sự được thực hiện ở cấp độ sâu hơn bởi thư viện Clout (một phần phụ của dự án Compojure - nhiều thứ đã được chuyển đến các thư viện riêng biệt ở quá trình chuyển đổi 0.3.x -> 0.4.x). Một tuyến đường được xác định bởi (1) một phương thức HTTP (GET, PUT, HEAD ...), (2) một mẫu URI (được chỉ định bằng cú pháp mà dường như sẽ quen thuộc với Webby Rubyists), (3) một biểu mẫu hủy cấu trúc được sử dụng trong liên kết các phần của bản đồ yêu cầu với các tên có sẵn trong phần thân, (4) phần thân của các biểu thức cần tạo ra phản hồi Ring hợp lệ (trong những trường hợp không tầm thường, đây thường chỉ là một lệnh gọi đến một hàm riêng biệt).
Đây có thể là một điểm tốt để xem một ví dụ đơn giản:
(def example-route (GET "/" [] "<html>...</html>"))
Hãy kiểm tra điều này tại REPL (bản đồ yêu cầu bên dưới là bản đồ yêu cầu Ring hợp lệ tối thiểu):
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>"}
Nếu :request-method
là :head
thay vào đó, phản ứng sẽ nil
. Chúng ta sẽ quay lại câu hỏi nil
ở đây có nghĩa là gì trong một phút nữa (nhưng lưu ý rằng đó không phải là Ring hồi phục hợp lệ!).
Như rõ ràng từ ví dụ này, example-route
chỉ là một hàm, và một hàm rất đơn giản ở đó; nó xem xét yêu cầu, xác định xem nó có quan tâm đến việc xử lý nó hay không (bằng cách kiểm tra :request-method
và :uri
) và, nếu có, trả về một bản đồ phản hồi cơ bản.
Điều cũng rõ ràng là phần thân của tuyến đường không thực sự cần đánh giá để có một bản đồ phản ứng thích hợp; Compojure cung cấp khả năng xử lý mặc định lành mạnh cho các chuỗi (như đã thấy ở trên) và một số kiểu đối tượng khác; xem compojure.response/render
multimethod để biết chi tiết (mã hoàn toàn tự ghi ở đây).
Hãy thử sử dụng defroutes
ngay bây giờ:
(defroutes example-routes
(GET "/" [] "get")
(HEAD "/" [] "head"))
Các phản hồi cho yêu cầu mẫu được hiển thị ở trên và biến thể của nó :request-method :head
giống như mong đợi.
Các hoạt động bên trong của example-routes
mỗi tuyến đường được thử lần lượt; ngay sau khi một trong số chúng trả về một không nil
phản hồi, phản hồi đó sẽ trở thành giá trị trả về của toàn bộ example-routes
trình xử lý. Như một sự tiện lợi bổ sung, các defroutes
trình xử lý được xác định được bao bọc wrap-params
và wrap-cookies
ngầm định.
Dưới đây là một ví dụ về một tuyến đường phức tạp hơn:
(def echo-typed-url-route
(GET "*" {:keys [scheme server-name server-port uri]}
(str (name scheme) "://" server-name ":" server-port uri)))
Lưu ý biểu mẫu cấu trúc thay cho vectơ trống đã sử dụng trước đó. Ý tưởng cơ bản ở đây là nội dung của tuyến đường có thể quan tâm đến một số thông tin về yêu cầu; vì điều này luôn đến dưới dạng bản đồ, một biểu mẫu cấu trúc kết hợp có thể được cung cấp để trích xuất thông tin từ yêu cầu và liên kết nó với các biến cục bộ sẽ nằm trong phạm vi trong phần thân của tuyến.
Một bài kiểm tra ở trên:
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"}
Ý tưởng tiếp theo tuyệt vời ở trên là các tuyến đường phức tạp hơn có thể assoc
bổ sung thông tin vào yêu cầu ở giai đoạn đối sánh:
(def echo-first-path-component-route
(GET "/:fst/*" [fst] fst))
Này trả lời bằng một :body
số "foo"
cho yêu cầu từ ví dụ trước.
Hai điểm mới về ví dụ mới nhất này: "/:fst/*"
vectơ ràng buộc không rỗng và không rỗng [fst]
. Đầu tiên là cú pháp giống Rails và Sinatra đã nói ở trên cho các mẫu URI. Nó phức tạp hơn một chút so với những gì rõ ràng từ ví dụ trên trong đó các ràng buộc regex trên các phân đoạn URI được hỗ trợ (ví dụ: ["/:fst/*" :fst #"[0-9]+"]
có thể được cung cấp để làm cho tuyến đường chỉ chấp nhận các giá trị toàn chữ số :fst
ở trên). Thứ hai là một cách đơn giản hóa đối với :params
mục nhập trong bản đồ yêu cầu, bản thân nó là một bản đồ; nó hữu ích để trích xuất các phân đoạn URI từ yêu cầu, tham số chuỗi truy vấn và tham số biểu mẫu. Một ví dụ để minh họa điểm sau:
(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\"}"}
Đây sẽ là thời điểm tốt để xem ví dụ từ văn bản câu hỏi:
(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>"))
Hãy lần lượt phân tích từng tuyến đường:
(GET "/" [] (workbench))
- khi xử lý một GET
yêu cầu :uri "/"
, hãy gọi hàm workbench
và hiển thị bất cứ thứ gì nó trả về vào một bản đồ phản hồi. (Nhớ lại rằng giá trị trả về có thể là một bản đồ, nhưng cũng có thể là một chuỗi, v.v.)
(POST "/save" {form-params :form-params} (str form-params))
- :form-params
là một mục trong bản đồ yêu cầu được cung cấp bởi wrap-params
phần mềm trung gian (nhớ lại rằng nó được bao gồm một cách ngầm định bởi defroutes
). Câu trả lời sẽ là tiêu chuẩn {:status 200 :headers {"Content-Type" "text/html"} :body ...}
với (str form-params)
thay thế cho ...
. (Một người POST
xử lý hơi khác thường , cái này ...)
(GET "/test" [& more] (str "<pre> more "</pre>"))
- ví dụ như điều này sẽ lặp lại biểu diễn chuỗi của bản đồ {"foo" "1"}
nếu tác nhân người dùng yêu cầu "/test?foo=1"
.
(GET ["/:filename" :filename #".*"] [filename] ...)
- :filename #".*"
phần không làm gì cả (vì #".*"
luôn khớp). Nó gọi chức năng tiện ích Ring ring.util.response/file-response
để tạo ra phản hồi của nó; các {:root "./static"}
phần nói nó ở đâu để tìm kiếm các tập tin.
(ANY "*" [] ...)
- một lộ trình bắt tất cả. Thông lệ Compojure tốt là luôn bao gồm một tuyến đường như vậy ở cuối defroutes
biểu mẫu để đảm bảo rằng trình xử lý đang được xác định luôn trả về một bản đồ phản hồi Ring hợp lệ (nhớ lại rằng kết quả là một lỗi đối sánh tuyến dẫn đến nil
).
Tại sao lại theo cách này?
Một mục đích của phần mềm trung gian Ring là thêm thông tin vào bản đồ yêu cầu; do đó phần mềm trung gian xử lý cookie thêm một :cookies
khóa vào yêu cầu, wrap-params
thêm :query-params
và / hoặc:form-params
nếu có một chuỗi / dữ liệu biểu mẫu truy vấn, v.v. (Nói một cách chính xác, tất cả thông tin mà các chức năng phần mềm trung gian đang thêm phải đã có trong bản đồ yêu cầu, vì đó là những gì chúng được thông qua; công việc của họ là biến đổi nó để thuận tiện hơn khi làm việc với các trình xử lý mà chúng bao bọc.) Cuối cùng, yêu cầu "được bổ sung" được chuyển đến trình xử lý cơ sở, trình xử lý này sẽ kiểm tra bản đồ yêu cầu với tất cả thông tin được xử lý trước độc đáo được phần mềm trung gian thêm vào và tạo ra phản hồi. (Phần mềm trung gian có thể làm những việc phức tạp hơn thế - như gói một số trình xử lý "bên trong" và lựa chọn giữa chúng, quyết định xem có gọi (các) trình xử lý được bọc hay không, v.v. Tuy nhiên, điều đó nằm ngoài phạm vi của câu trả lời này.)
Đến lượt nó, trình xử lý cơ sở thường (trong những trường hợp không tầm thường) là một hàm có xu hướng chỉ cần một số ít các mục thông tin về yêu cầu. (Ví dụ: ring.util.response/file-response
không quan tâm đến hầu hết các yêu cầu; nó chỉ cần một tên tệp.) Do đó, cần một cách đơn giản để giải nén các phần liên quan của một yêu cầu Ring. Compojure nhằm mục đích cung cấp một công cụ đối sánh mẫu có mục đích đặc biệt, như nó vốn có, thực hiện điều đó.