“Ý tưởng lớn” đằng sau các tuyến đường compojure là gì?


109

Tôi mới sử dụng Clojure và đang sử dụng Compojure để viết một ứng dụng web cơ bản. Tuy nhiên, tôi đang đụng phải một bức tường với defroutescú pháp của Compojure , và tôi nghĩ rằng tôi cần phải hiểu cả "cách" và "tại sao" đằng sau tất cả.

Có vẻ như một ứng dụng kiểu Ring bắt đầu với một bản đồ yêu cầu HTTP, sau đó chỉ cần chuyển yêu cầu qua một loạt các chức năng phần mềm trung gian cho đến khi nó được chuyển đổi thành một bản đồ phản hồi, được gửi trở lại trình duyệt. Phong cách này dường như quá "thấp" đối với các nhà phát triển, do đó cần một công cụ như Compojure. Tôi có thể thấy điều này cũng cần nhiều trừu tượng hơn trong các hệ sinh thái phần mềm khác, đáng chú ý nhất là với WSGI của Python.

Vấn đề là tôi không hiểu cách tiếp cận của Compojure. Hãy lấy defroutesbiểu thức S sau :

(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ôi biết rằng chìa khóa để hiểu tất cả những điều này nằm trong một số thói quen vĩ mô, nhưng tôi chưa hoàn toàn hiểu về macro. Tôi đã nhìn chằm chằm vào defroutesnguồn trong một thời gian dài, nhưng không hiểu! Những gì đang xảy ra ở đây? Hiểu được "ý tưởng lớn" có thể sẽ giúp tôi trả lời những câu hỏi cụ thể sau:

  1. Làm cách nào để truy cập môi trường Ring từ bên trong một hàm được định tuyến (ví dụ: workbenchhàm)? Ví dụ: giả sử tôi muốn truy cập tiêu đề HTTP_ACCEPT hoặc một số phần khác của yêu cầu / phần mềm trung gian?
  2. Đối phó với hàm hủy ( {form-params :form-params}) là gì? Những từ khóa nào có sẵn cho tôi khi cơ cấu lại?

Tôi thực sự thích Clojure nhưng tôi rất bối rối!

Câu trả lời:


212

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:

  1. 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.

  2. 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).

  3. 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:headthay 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-routechỉ 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: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/rendermultimethod để biết chi tiết (mã hoàn toàn tự ghi ở đây).

Hãy thử sử dụng defroutesngay 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 :headgiống như mong đợi.

Các hoạt động bên trong của example-routesmỗ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 nilphản hồi, phản hồi đó sẽ trở thành giá trị trả về của toàn bộ example-routestrình xử lý. Như một sự tiện lợi bổ sung, các defroutestrình xử lý được xác định được bao bọc wrap-paramswrap-cookiesngầ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ể assocbổ 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 :bodysố "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 :paramsmụ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:

  1. (GET "/" [] (workbench))- khi xử lý một GETyêu cầu :uri "/", hãy gọi hàm workbenchvà 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.)

  2. (POST "/save" {form-params :form-params} (str form-params))- :form-paramslà một mục trong bản đồ yêu cầu được cung cấp bởi wrap-paramsphầ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 POSTxử lý hơi khác thường , cái này ...)

  3. (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".

  4. (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.

  5. (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 defroutesbiể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 :cookieskhóa vào yêu cầu, wrap-paramsthêm :query-paramsvà / hoặc:form-paramsnế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-responsekhô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 đó.


3
"Như một sự tiện lợi bổ sung, các trình xử lý do quá trình rã đông xác định được bọc trong các gói tham số và bọc cookie một cách ngầm định." - Kể từ phiên bản 0.6.0, bạn phải thêm chúng một cách rõ ràng. Tham khảo github.com/weavejester/compojure/commit/…
Dan Midwood

3
Rất tốt đặt. Câu trả lời này sẽ có trên trang chủ của Compojure.
Siddhartha Reddy

2
Bắt buộc đọc đối với bất kỳ ai mới sử dụng Compojure. Tôi ước mọi bài đăng trên wiki và blog về chủ đề này đều bắt đầu bằng một liên kết đến điều này.
jemmons

7

Có một bài báo xuất sắc trên booleanknot.com của James Reeves (tác giả của Compojure), và việc đọc nó khiến tôi "nhấp chuột", vì vậy tôi đã dịch lại một số bài viết ở đây (thực sự đó là tất cả những gì tôi đã làm).

Cũng có một slidedeck ở đây từ cùng một tác giả , trả lời chính xác câu hỏi này.

Compojure dựa trên Ring , là phần trừu tượng cho các yêu cầu http.

A concise syntax for generating Ring handlers.

Vậy, những bộ xử lý Ring đó là gì? Trích xuất từ ​​tài liệu:

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

Khá đơn giản, nhưng cũng khá thấp. Trình xử lý trên có thể được định nghĩa ngắn gọn hơn bằng cách sử dụng ring/utilthư viện.

(use 'ring.util.response)

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

Bây giờ chúng tôi muốn gọi các trình xử lý khác nhau tùy thuộc vào yêu cầu. Chúng tôi có thể thực hiện một số định tuyến tĩnh như vậy:

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

Và cấu trúc lại nó như thế này:

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

Điều thú vị mà James lưu ý sau đó là điều này cho phép các tuyến đường lồng nhau, bởi vì "kết quả của việc kết hợp hai hoặc nhiều tuyến đường với nhau chính là một tuyến đường".

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

Bây giờ, chúng ta đang bắt đầu thấy một số mã có vẻ như nó có thể được tính toán bằng cách sử dụng macro. Compojure cung cấp một defroutesmacro:

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

;; is identical to

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

Compojure cung cấp các macro khác, như GETmacro:

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

;; will expand to

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

Hàm cuối cùng được tạo ra trông giống như trình xử lý của chúng tôi!

Hãy nhớ xem bài đăng của James , vì nó đi sâu vào các giải thích chi tiết hơn.


4

Đối với bất kỳ ai vẫn đang vật lộn để tìm ra những gì đang diễn ra với các tuyến đường, có thể giống như tôi, bạn không hiểu ý tưởng của việc tái cấu trúc.

Trên thực tế, việc đọc các tài liệulet đã giúp làm sáng tỏ toàn bộ "các giá trị kỳ diệu đến từ đâu?" câu hỏi.

Tôi đang dán các phần có liên quan bên dưới:

Clojure hỗ trợ ràng buộc cấu trúc trừu tượng, thường được gọi là hủy cấu trúc, trong danh sách ràng buộc let, danh sách tham số fn và bất kỳ macro nào mở rộng thành let hoặc fn. Ý tưởng cơ bản là một biểu mẫu liên kết có thể là một cấu trúc dữ liệu theo nghĩa đen chứa các ký hiệu được liên kết với các phần tương ứng của init-expr. Liên kết là trừu tượng trong đó một ký tự vectơ có thể liên kết với bất kỳ thứ gì liên quan đến tuần tự, trong khi một ký tự bản đồ có thể liên kết với bất kỳ thứ gì có tính liên kết.

Vector ràng buộc-exprs cho phép bạn liên kết tên với các phần của những thứ tuần tự (không chỉ là vectơ), như vectơ, danh sách, seqs, chuỗi, mảng và bất kỳ thứ gì hỗ trợ nth. Dạng tuần tự cơ bản là một vectơ của các dạng liên kết, sẽ được liên kết với các phần tử liên tiếp từ init-expr, được tra cứu qua thứ n. Ngoài ra, và theo tùy chọn, & theo sau là biểu mẫu liên kết sẽ khiến biểu mẫu liên kết đó bị ràng buộc với phần còn lại của chuỗi, tức là phần chưa được liên kết, được tra cứu qua nthnext. Cuối cùng, cũng là tùy chọn,: như sau một biểu tượng sẽ khiến biểu tượng đó bị ràng buộc với toàn bộ 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 ràng buộc-exprs cho phép bạn liên kết tên với các phần của những thứ tuần tự (không chỉ là vectơ), như vectơ, danh sách, seqs, chuỗi, mảng và bất kỳ thứ gì hỗ trợ nth. Dạng tuần tự cơ bản là một vectơ của các dạng liên kết, sẽ được liên kết với các phần tử liên tiếp từ init-expr, được tra cứu qua thứ n. Ngoài ra, và theo tùy chọn, & theo sau là biểu mẫu liên kết sẽ khiến biểu mẫu liên kết đó bị ràng buộc với phần còn lại của chuỗi, tức là phần chưa được liên kết, được tra cứu qua nthnext. Cuối cùng, cũng là tùy chọn,: như sau một biểu tượng sẽ khiến biểu tượng đó bị ràng buộc với toàn bộ 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]]

3

Cảm ơn, những liên kết này chắc chắn hữu ích. Tôi đã giải quyết vấn đề này trong khoảng thời gian tốt hơn trong ngày và đang ở một nơi tốt hơn với nó ... Tôi sẽ cố gắng đăng bài tiếp theo vào một thời điểm nào đó.
Sean Woods

1

Vấn đề với cấu trúc hủy ({form-params: form-params}) là gì? Những từ khóa nào có sẵn cho tôi khi cơ cấu lại?

Các phím có sẵn là những phím có trong bản đồ đầu vào. Hủy cấu trúc có sẵn bên trong các biểu mẫu let và Liều lượngq, hoặc bên trong các tham số để fn hoặc defn

Đoạn mã sau hy vọng sẽ có nhiều thông tin:

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

một ví dụ nâng cao hơn, hiển thị cấu trúc hủy lồng nhau:

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

Khi được sử dụng một cách khôn ngoan, việc hủy cấu trúc sẽ giải mã mã của bạn bằng cách tránh truy cập dữ liệu soạn sẵn. bằng cách sử dụng: as và in kết quả (hoặc các khóa của kết quả), bạn có thể biết rõ hơn những dữ liệu nào khác mà bạn có thể truy cập.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.