อธิบาย Compojure (ในระดับหนึ่ง)
NB. ฉันกำลังทำงานกับ Compojure 0.4.1 ( นี่คือรุ่น 0.4.1 ที่กระทำบน GitHub)
ทำไม?
ที่ด้านบนสุดcompojure/core.clj
มีข้อมูลสรุปที่เป็นประโยชน์เกี่ยวกับวัตถุประสงค์ของ Compojure:
ไวยากรณ์ที่กระชับสำหรับการสร้างเครื่องจัดการแหวน
ในระดับผิวเผินนั่นคือทั้งหมดสำหรับคำถาม "ทำไม" หากต้องการเจาะลึกลงไปอีกสักหน่อยเรามาดูกันว่าแอพรูปแบบ Ring ทำงานอย่างไร:
คำขอมาถึงและเปลี่ยนเป็นแผนที่ Clojure ตามข้อกำหนดของ Ring
แผนที่นี้รวมเข้ากับสิ่งที่เรียกว่า "ฟังก์ชันตัวจัดการ" ซึ่งคาดว่าจะตอบสนอง (ซึ่งก็คือแผนที่ Clojure เช่นกัน)
แผนที่ตอบกลับจะถูกแปลงเป็นการตอบกลับ HTTP จริงและส่งกลับไปยังไคลเอนต์
ขั้นตอนที่ 2 ในข้างต้นเป็นสิ่งที่น่าสนใจที่สุดเนื่องจากเป็นความรับผิดชอบของผู้ดำเนินการในการตรวจสอบ URI ที่ใช้ในคำขอตรวจสอบคุกกี้ ฯลฯ และในที่สุดก็ได้รับคำตอบที่เหมาะสม เห็นได้ชัดว่างานทั้งหมดนี้จำเป็นต้องรวมอยู่ในกลุ่มของชิ้นงานที่กำหนดไว้อย่างดี โดยปกติจะเป็นฟังก์ชันตัวจัดการ "ฐาน" และชุดของฟังก์ชันมิดเดิลแวร์ที่ห่อหุ้มไว้ จุดประสงค์ของ Compojure คือการทำให้การสร้างฟังก์ชันตัวจัดการพื้นฐานง่ายขึ้น
อย่างไร?
Compojure สร้างขึ้นตามแนวคิดของ "เส้นทาง" สิ่งเหล่านี้ถูกนำไปใช้ในระดับที่ลึกกว่าโดยไลบรารีClout (สปินออฟของโครงการ Compojure - หลายสิ่งถูกย้ายไปยังไลบรารีแยกต่างหากที่การเปลี่ยนแปลง 0.3.x -> 0.4.x) เส้นทางถูกกำหนดโดย (1) เมธอด HTTP (GET, PUT, HEAD ... ), (2) รูปแบบ URI (ระบุด้วยไวยากรณ์ซึ่งดูเหมือนจะคุ้นเคยกับ Webby Rubyists), (3) รูปแบบการทำลายโครงสร้างที่ใช้ใน การเชื่อมโยงส่วนต่างๆของแผนผังคำขอกับชื่อที่มีอยู่ในเนื้อหา (4) เนื้อหาของนิพจน์ที่จำเป็นต้องสร้างการตอบสนองของ Ring ที่ถูกต้อง (ในกรณีที่ไม่สำคัญซึ่งโดยปกติจะเป็นเพียงการเรียกใช้ฟังก์ชันแยกต่างหาก)
นี่อาจเป็นจุดที่ดีที่จะดูตัวอย่างง่ายๆ:
(def example-route (GET "/" [] "<html>...</html>"))
ลองทดสอบสิ่งนี้ที่ REPL (แผนที่คำขอด้านล่างเป็นแผนที่คำขอ Ring ที่ถูกต้องน้อยที่สุด):
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>"}
ถ้า:request-method
เป็นแทนการตอบสนองจะเป็น:head
nil
เราจะกลับไปที่คำถามว่าความnil
หมายที่นี่ในอีกไม่กี่นาที (แต่โปรดสังเกตว่ามันไม่ใช่การตอบสนองของ Ring ที่ถูกต้อง!)
ดังที่เห็นได้ชัดจากตัวอย่างนี้example-route
เป็นเพียงฟังก์ชันและฟังก์ชันที่เรียบง่ายมาก จะดูที่คำขอพิจารณาว่าสนใจที่จะจัดการหรือไม่ (โดยการตรวจสอบ:request-method
และ:uri
) และหากเป็นเช่นนั้นจะส่งคืนแผนผังการตอบกลับพื้นฐาน
สิ่งที่เห็นได้ชัดก็คือเนื้อความของเส้นทางไม่จำเป็นต้องประเมินตามแผนที่ตอบสนองที่เหมาะสม Compojure ให้การจัดการดีฟอลต์สำหรับสตริง (ดังที่เห็นด้านบน) และอ็อบเจ็กต์ประเภทอื่น ๆ อีกจำนวนมาก ดูcompojure.response/render
รายละเอียดหลายวิธี (รหัสนี้จัดทำเอกสารด้วยตัวเองทั้งหมดที่นี่)
ลองใช้เลยdefroutes
:
(defroutes example-routes
(GET "/" [] "get")
(HEAD "/" [] "head"))
การตอบสนองต่อคำขอตัวอย่างที่แสดงด้านบนและตัวแปรด้วย:request-method :head
เป็นไปตามที่คาดไว้
การทำงานภายในexample-routes
เป็นเช่นนั้นในแต่ละเส้นทางจะถูกพยายามในทางกลับกัน ทันทีที่หนึ่งในนั้นส่งคืนการไม่nil
ตอบกลับคำตอบนั้นจะกลายเป็นค่าส่งคืนของexample-routes
ตัวจัดการทั้งหมด เพื่อความสะดวกที่เพิ่มขึ้นdefroutes
ตัวจัดการที่กำหนดไว้จะถูกห่อหุ้มwrap-params
และwrap-cookies
โดยปริยาย
นี่คือตัวอย่างของเส้นทางที่ซับซ้อนมากขึ้น:
(def echo-typed-url-route
(GET "*" {:keys [scheme server-name server-port uri]}
(str (name scheme) "://" server-name ":" server-port uri)))
สังเกตรูปแบบการทำลายแทนเวกเตอร์เปล่าที่ใช้ก่อนหน้านี้ แนวคิดพื้นฐานคือเนื้อหาของเส้นทางอาจสนใจข้อมูลบางอย่างเกี่ยวกับคำขอ เนื่องจากสิ่งนี้มาถึงในรูปแบบของแผนที่เสมอจึงสามารถจัดหาแบบฟอร์มการทำลายล้างแบบเชื่อมโยงเพื่อดึงข้อมูลจากคำขอและผูกเข้ากับตัวแปรท้องถิ่นซึ่งจะอยู่ในขอบเขตในเนื้อหาของเส้นทาง
การทดสอบข้างต้น:
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"}
แนวคิดในการติดตามผลข้างต้นที่ยอดเยี่ยมคือเส้นทางที่ซับซ้อนมากขึ้นอาจให้assoc
ข้อมูลเพิ่มเติมในคำขอในขั้นตอนการจับคู่:
(def echo-first-path-component-route
(GET "/:fst/*" [fst] fst))
ตอบสนองนี้กับ:body
ของ"foo"
การร้องขอจากตัวอย่างก่อนหน้านี้
มีสองสิ่งที่ใหม่ ๆ เกี่ยวกับตัวอย่างล่าสุดนี้คือและไม่ว่างเปล่าเวกเตอร์ที่มีผลผูกพัน"/:fst/*"
[fst]
ประการแรกคือไวยากรณ์ที่คล้ายกับ Rails-and-Sinatra ข้างต้นสำหรับรูปแบบ URI มันซับซ้อนกว่าที่เห็นได้ชัดจากตัวอย่างข้างต้นเล็กน้อยในการรองรับข้อ จำกัด regex บนเซ็กเมนต์ URI (เช่น["/:fst/*" :fst #"[0-9]+"]
สามารถจัดหาเพื่อให้เส้นทางยอมรับเฉพาะค่าทั้งหมดที่เป็นตัวเลข:fst
ในด้านบน) วิธีที่สองคือวิธีที่ง่ายกว่าในการจับคู่:params
รายการในแผนที่คำขอซึ่งก็คือแผนที่ มันมีประโยชน์สำหรับการแยกส่วน URI จากคำขอพารามิเตอร์สตริงการสืบค้นและพารามิเตอร์ฟอร์ม ตัวอย่างเพื่อแสดงประเด็นหลัง:
(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\"}"}
นี่เป็นช่วงเวลาที่ดีที่จะได้ดูตัวอย่างจากข้อความคำถาม:
(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>"))
มาวิเคราะห์แต่ละเส้นทางกัน:
(GET "/" [] (workbench))
- เมื่อจัดการกับGET
คำขอ:uri "/"
ให้เรียกใช้ฟังก์ชันworkbench
และแสดงสิ่งที่ส่งกลับไปยังแผนที่ตอบกลับ (โปรดจำไว้ว่าค่าที่ส่งคืนอาจเป็นแผนที่ แต่ยังรวมถึงสตริงเป็นต้น)
(POST "/save" {form-params :form-params} (str form-params))
- :form-params
เป็นรายการในแผนที่คำขอที่จัดทำโดยwrap-params
มิดเดิลแวร์ (โปรดจำไว้ว่ามีการรวมโดยปริยายโดยdefroutes
) การตอบสนองจะเป็นมาตรฐาน{:status 200 :headers {"Content-Type" "text/html"} :body ...}
ที่มีการเปลี่ยนแทน(str form-params)
...
( POST
ตัวจัดการที่ผิดปกติเล็กน้อยนี่ ... )
(GET "/test" [& more] (str "<pre> more "</pre>"))
- นี้จะสะท้อนกลับเช่นการเป็นตัวแทนสตริงของแผนที่ถ้าตัวแทนของผู้ใช้ถามหา{"foo" "1"}
"/test?foo=1"
(GET ["/:filename" :filename #".*"] [filename] ...)
- :filename #".*"
ส่วนนี้ไม่ทำอะไรเลย (เนื่องจาก#".*"
ตรงกันเสมอ) เรียกใช้ฟังก์ชันยูทิลิตี้ Ring ring.util.response/file-response
เพื่อสร้างการตอบสนอง {:root "./static"}
ส่วนหนึ่งบอกว่าที่จะมองหาไฟล์
(ANY "*" [] ...)
- เส้นทางรับทั้งหมด เป็นการดีที่จะรวมเส้นทางดังกล่าวไว้ที่ส่วนท้ายของdefroutes
แบบฟอร์มเพื่อให้แน่ใจว่าตัวจัดการที่กำหนดจะส่งคืนแผนที่การตอบสนองของเสียงเรียกเข้าที่ถูกต้องเสมอ (โปรดจำไว้ว่าความล้มเหลวในการจับคู่เส้นทางส่งผลให้nil
)
ทำไมต้องเป็นแบบนี้?
จุดประสงค์ประการหนึ่งของมิดเดิลแวร์ Ring คือการเพิ่มข้อมูลลงในแผนที่คำขอ ดังนั้นมิดเดิลแวร์จัดการคุกกี้จึงเพิ่ม:cookies
คีย์ให้กับคำขอwrap-params
เพิ่ม:query-params
และ / หรือ:form-params
หากมีข้อมูลสตริง / ฟอร์มแบบสอบถามและอื่น ๆ (พูดอย่างเคร่งครัดข้อมูลทั้งหมดที่ฟังก์ชันมิดเดิลแวร์กำลังเพิ่มจะต้องมีอยู่แล้วในแผนที่คำขอเนื่องจากนั่นคือสิ่งที่พวกเขาได้รับงานของพวกเขาคือการเปลี่ยนมันให้สะดวกยิ่งขึ้นในการทำงานร่วมกับตัวจัดการที่พวกเขารวมไว้) ในที่สุดคำขอ "ที่เพิ่มประสิทธิภาพ" จะถูกส่งไปยังตัวจัดการพื้นฐานซึ่งจะตรวจสอบแผนผังคำขอพร้อมข้อมูลที่ประมวลผลล่วงหน้าอย่างดีทั้งหมดที่เพิ่มโดยมิดเดิลแวร์และสร้างการตอบกลับ (มิดเดิลแวร์สามารถทำสิ่งที่ซับซ้อนกว่านั้นได้เช่นการห่อตัวจัดการ "ภายใน" หลายตัวและเลือกระหว่างตัวจัดการตัดสินใจว่าจะเรียกตัวจัดการที่ถูกห่อหรือไม่เป็นต้นอย่างไรก็ตามนั่นคืออยู่นอกขอบเขตของคำตอบนี้)
ในทางกลับกันตัวจัดการพื้นฐานมักเป็นฟังก์ชัน (ในกรณีที่ไม่สำคัญ) ซึ่งมีแนวโน้มที่จะต้องการข้อมูลเพียงไม่กี่รายการเกี่ยวกับคำขอ (เช่นring.util.response/file-response
ไม่สนใจคำขอส่วนใหญ่ต้องการเพียงแค่ชื่อไฟล์เท่านั้น) ดังนั้นความต้องการวิธีง่ายๆในการแยกเฉพาะส่วนที่เกี่ยวข้องของคำขอ Ring Compojure มีจุดมุ่งหมายเพื่อจัดหาเอ็นจิ้นการจับคู่รูปแบบที่มีวัตถุประสงค์พิเศษเหมือนเดิมซึ่งทำเช่นนั้น