(defparameter *web* (make-instance '<app>)) @route GET "/"(ดัชนี defun () (render #P"index.tmpl")) @route GET "/hello"(defun พูดสวัสดี (&คีย์ (|ชื่อ| "แขก")) (ไม่มีรูปแบบ "สวัสดี ~A" |ชื่อ|))
ทุกอย่าง. Caveman2 ถูกเขียนตั้งแต่เริ่มต้น
สิ่งเหล่านี้เป็นจุดที่น่าสังเกต
มีพื้นฐานมาจากหนิงเกิ้ล
มีการบูรณาการฐานข้อมูล
ใช้ระบบการกำหนดค่าใหม่ที่แยกจากกัน (Envy)
มีมาโครการกำหนดเส้นทางใหม่
หนึ่งในคำถามที่พบบ่อยคือ "ฉันควรใช้ตัวไหน: ningle หรือ Caveman? อะไรคือความแตกต่าง?" ฉันคิดว่าสิ่งเหล่านี้ถูกถามบ่อยมากเพราะ Caveman และ ningle มีความคล้ายคลึงกันมากเกินไป ทั้งคู่เรียกว่า "ไมโคร" และไม่มีการรองรับฐานข้อมูล
ด้วย Caveman2 ทำให้ Caveman ไม่ได้เป็นเฟรมเวิร์กแอปพลิเคชันเว็บแบบ "ไมโคร" อีกต่อไป รองรับ CL-DBI และมีการจัดการการเชื่อมต่อฐานข้อมูลตามค่าเริ่มต้น มนุษย์ถ้ำเริ่มโตขึ้นแล้ว
Caveman มีวัตถุประสงค์เพื่อเป็นชุดของส่วนทั่วไปของเว็บแอปพลิเคชัน ด้วย Caveman2 ฉันใช้กฎสามข้อในการตัดสินใจ:
สามารถขยายได้
ปฏิบัติได้จริง
อย่าบังคับอะไร
คุณมาที่นี่เพราะคุณสนใจที่จะใช้ชีวิตแบบมนุษย์ถ้ำใช่ไหม? นี่ไม่ใช่ดิสนีย์แลนด์ แต่เราสามารถเริ่มต้นได้ที่นี่ เข้าไปในถ้ำกันเถอะ!
Caveman2 พร้อมใช้งานบน Quicklisp แล้ว
(ql: โหลดด่วน: Caveman2)
(caveman2:make-project #P"/path/to/myapp/" :author "<ชื่อเต็มของคุณ>");-> กำลังเขียน /path/to/myapp/.gitignore; การเขียน /path/to/myapp/README.markdown; การเขียน /path/to/myapp/app.lisp; การเขียน /path/to/myapp/db/schema.sql; การเขียน /path/to/myapp/shlyfile.lisp; การเขียน /path/to/myapp/myapp-test.asd; การเขียน /path/to/myapp/myapp.asd; การเขียน /path/to/myapp/src/config.lisp; การเขียน /path/to/myapp/src/db.lisp; การเขียน /path/to/myapp/src/main.lisp; การเขียน /path/to/myapp/src/view.lisp; การเขียน /path/to/myapp/src/web.lisp; การเขียน /path/to/myapp/static/css/main.css; การเขียน /path/to/myapp/t/myapp.lisp; การเขียน /path/to/myapp/templates/_errors/404.html; การเขียน /path/to/myapp/templates/index.tmpl; การเขียน /path/to/myapp/templates/layout/default.tmpl
นี่คือตัวอย่างที่ถือว่าชื่อแอปพลิเคชันของคุณคือ "myapp" ก่อนที่จะเริ่มเซิร์ฟเวอร์ คุณต้องโหลดแอปของคุณก่อน
(ql: โหลดด่วน: myapp)
แอปพลิเคชันของคุณมีฟังก์ชันชื่อ start
และ stop
เพื่อเริ่ม/หยุดแอปพลิเคชันเว็บของคุณ
(myapp:start :พอร์ต 8080)
เนื่องจาก Caveman มีพื้นฐานมาจาก Clack/Lack คุณจึงสามารถเลือกเซิร์ฟเวอร์ที่จะรันได้ เช่น Hunchentoot, Woo หรือ Wookie เป็นต้น
(myapp:start :server :hunchentoot :port 8080) (myapp:start :server :fcgi :port 8080)
ฉันขอแนะนำให้คุณใช้ Hunchentoot บนเครื่องท้องถิ่น และใช้ Woo ในสภาพแวดล้อมการใช้งานจริง
คุณยังสามารถเริ่มแอปพลิเคชันของคุณได้โดยใช้คำสั่ง clackup
$ ros install clack $ which clackup /Users/nitro_idiot/.roswell/bin/clackup $ APP_ENV=development clackup --server :fcgi --port 8080 app.lisp
Caveman2 มี 2 วิธีในการกำหนดเส้นทาง - @route
และ defroute
คุณสามารถใช้อย่างใดอย่างหนึ่ง
@route
เป็นมาโครคำอธิบายประกอบ ที่กำหนดโดยใช้ cl-annot ต้องใช้เมธอด สตริง URL และฟังก์ชัน
@route GET "/"(ดัชนี defun () - เส้นทางที่ไม่มี name.@route GET "/welcome"(lambda (&key (|name| "Guest")) (ไม่มีรูปแบบ "ยินดีต้อนรับ ~A" |ชื่อ|))
สิ่งนี้คล้ายกับ @url
ของ Caveman1 ยกเว้นรายการอาร์กิวเมนต์ คุณไม่จำเป็นต้องระบุอาร์กิวเมนต์เมื่อไม่จำเป็น
defroute
เป็นเพียงมาโคร มันมีฟังก์ชันการทำงานเช่นเดียวกับ @route
(ละลายดัชนี "/" () - เส้นทางที่ไม่มีชื่อ(defroute "/welcome" (&key (|name| "Guest")) (ไม่มีรูปแบบ "ยินดีต้อนรับ ~A" |ชื่อ|))
เนื่องจาก Caveman มีฐานอยู่บน ningle Caveman จึงมีระบบการกำหนดเส้นทางที่คล้ายกับ Sinatra เช่นกัน
- คำขอ GET (ค่าเริ่มต้น)@route GET "/" (แลมบ์ดา () ...) (defroute ("/" :method :GET) () ...);; POST request@route POST "/" (แลมบ์ดา () ...) (defroute ("/" :method :POST) () ...);; PUT request@route PUT "/" (แลมบ์ดา () ...) (defroute ("/" :method :PUT) () ...);; DELETE request@route DELETE "/" (แลมบ์ดา () ...) (defroute ("/" :method :DELETE) () ...);; OPTIONS request@route OPTIONS "/" (แลมบ์ดา () ...) (defroute ("/" :method :OPTIONS) () ...);; สำหรับ method@route ทั้งหมด "/" (แลมบ์ดา () ...) (defroute ("/" :method :ANY) () ...)
รูปแบบเส้นทางอาจมี "คำหลัก" เพื่อใส่ค่าลงในอาร์กิวเมนต์
(defroute "/hello/:name" (&ชื่อคีย์) (รูปแบบไม่มีชื่อ "สวัสดี ~A"))
ตัวควบคุมด้านบนจะถูกเรียกใช้เมื่อคุณเข้าถึง "/hello/Eitaro" หรือ "/hello/Tomohiro" และ name
จะเป็น "Eitaro" หรือ "Tomohiro" ตามความเหมาะสม
(&key name)
เกือบจะเหมือนกับรายการแลมบ์ดาของ Common Lisp ยกเว้นว่าจะอนุญาตให้ใช้คีย์อื่นเสมอ
(defroute "/hello/:name" (&rest params &key name) ;; ... -
รูปแบบเส้นทางอาจมีพารามิเตอร์ "ไวด์การ์ด" สามารถเข้าถึงได้โดยใช้ splat
(defroute "/say/*/to/*" (&key splat) ; จับคู่ /say/hello/to/world (รูปแบบไม่มีเครื่องหมาย "~A"));=> (สวัสดีชาวโลก)(defroute "/download/*.*" (&key splat) ; ตรงกัน /download/path/to/file.xml (รูปแบบไม่มีเครื่องหมาย "~ A")) ;=> (เส้นทาง/ถึง/ไฟล์ xml)
หากคุณต้องการเขียนโดยใช้นิพจน์ทั่วไปในกฎ URL :regexp t
ควรใช้งานได้
(defroute ("/hello/([w]+)" :regexp t) (&คีย์แคปเจอร์) (ไม่มีรูปแบบ "Hello, ~A!" (จับภาพครั้งแรก)))
โดยปกติ เส้นทางจะถูกทดสอบสำหรับการจับคู่ตามลำดับที่กำหนดไว้ และจะเรียกใช้เฉพาะเส้นทางแรกที่ตรงกันเท่านั้น โดยไม่สนใจเส้นทางต่อไปนี้ อย่างไรก็ตาม เส้นทางสามารถทดสอบรายการที่ตรงกันในรายการต่อไปได้ โดยรวม next-route
(defroute "/guess/:who" (&คีย์ใคร) (ถ้า (string= ใคร "เออิทาโร") "เข้าใจแล้ว!" (เส้นทางถัดไป))) (defroute "/guess/*" () "คุณพลาด!")
คุณสามารถส่งคืนรูปแบบต่อไปนี้อันเป็นผลมาจาก defroute
สตริง
ชื่อพาธ
รายการตอบกลับของ Clack (ประกอบด้วยสถานะ ส่วนหัว และเนื้อหา)
เปลี่ยนเส้นทางไปยังเส้นทางอื่นด้วย (redirect "url")
อาร์กิวเมนต์ทางเลือกที่สองคือรหัสสถานะ 302 โดยค่าเริ่มต้น
เมื่อคุณกำหนดเส้นทางด้วยชื่อ คุณสามารถค้นหา URL จากชื่อด้วย (url-for route-name &rest params)
ฟังก์ชันจะส่งข้อผิดพลาดหากไม่พบเส้นทาง
ดูเพิ่มเติมที่:
add-query-parameters base-url params
คีย์พารามิเตอร์ที่มีวงเล็บเหลี่ยม ("[" & "]") จะถูกแยกวิเคราะห์เป็นพารามิเตอร์ที่มีโครงสร้าง คุณสามารถเข้าถึงพารามิเตอร์ที่แยกวิเคราะห์เป็น _parsed
ในเราเตอร์
<รูปแบบการกระทำ="/แก้ไข"> <input type="name" name="person[name]" /> <input type="name" name="person[email]" /> <input type="name" name="person[birth][year] ]" /> <input type="name" name="person[birth][month]" /> <input type="name" name="person[birth][day]" /></form>
(defroute "/edit" (&key _parsed) (รูปแบบไม่มี "~S" (cdr (assoc "person" _parsed :test #'string=))));=> "(("name" . "Eitaro") ("email" . "e.arrows@gmail .com") ("วันเกิด" . (("ปี" . 2000) ("เดือน" . 1) ("วัน" . 1))))";; ด้วย assoc-utils(ql:quickload :assoc-utils) (นำเข้า 'assoc-utils:aget) (defroute "/edit" (&key _parsed) (รูปแบบไม่มี "~S" (aget _parsed "บุคคล")))
ปุ่มว่างหมายความว่ามีหลายค่า
<รูปแบบการกระทำ="/เพิ่ม"> <input type="text" name="items[][name]" /> <input type="text" name="items[][price]" /> <input type="text" name="items[ ][ชื่อ]" /> <input type="text" name="items[][price]" /> <input type="submit" value="Add" /></form>
(defroute "/add" (&key _parsed) (รูปแบบไม่มี "~S" (assoc "items" _parsed :test #'string=)));=> "((("name" . "WiiU") ("price" . "30000")) ((" ชื่อ" . "PS4") ("ราคา" . "69000")))"
Caveman ใช้ Djula เป็นเครื่องมือสร้างเทมเพลตเริ่มต้น
{% ขยาย "layouts/default.html" %} {% ชื่อบล็อก %} ผู้ใช้ | แอพของฉัน{% ปิดกั้น %} {% บล็อกเนื้อหา %}<div id="main"> <ul> {% สำหรับผู้ใช้ในผู้ใช้ %}<li><a href="{{ user.url }}">{{ user.name }}</a></li> {% สิ้นสุดสำหรับ %} </ul></div>{% สิ้นสุดบล็อก %}
(นำเข้า 'myapp.view:render) (render #P"users.html"'(:users ((:url "/id/1" :ชื่อ "nitro_idiot") (:url "/id/2" :name "meymao")) :has-next-page T))
หากคุณต้องการรับบางสิ่งจากฐานข้อมูลหรือเรียกใช้ฟังก์ชันโดยใช้ Djula คุณต้องระบุ list
การโทรอย่างชัดเจนเมื่อส่งผ่านอาร์กิวเมนต์เพื่อแสดงผลเพื่อให้โค้ดทำงานได้
(นำเข้า 'myapp.view:render) (render #P"users.html"(list :users (get-users-from-db)))
นี่คือตัวอย่างของ JSON API
(defroute "/user.json" (&key |id|) (let ((person (find-person-from-db |id|)));; person => (:|name| "Eitaro Fukamachi" :|email| "[email protected]")(render- บุคคล json)));=> {"name": Eitaro Fukamachi, "email": "[email protected]"}
render-json
เป็นส่วนหนึ่งของโครงการโครงกระดูก คุณสามารถค้นหาโค้ดได้ใน "src/view.lisp"
รูปภาพ, CSS, JS, favicon.ico และ robot.txt ในไดเรกทอรี "static/" จะแสดงตามค่าเริ่มต้น
/images/logo.png => {PROJECT_ROOT}/static/images/logo.png /css/main.css => {PROJECT_ROOT}/static/css/main.css /js/app/index.js => {PROJECT_ROOT}/static/js/app/index.js /robot.txt => {PROJECT_ROOT}/static/robot.txt /favicon.ico => {PROJECT_ROOT}/static/favicon.ico
คุณสามารถเปลี่ยนกฎเหล่านี้ได้โดยการเขียน "PROJECT_ROOT/app.lisp" ใหม่ ดู Clack.Middleware.Static เพื่อดูรายละเอียด
Caveman ใช้ Envy เป็นตัวสลับการกำหนดค่า ซึ่งช่วยให้สามารถกำหนดการกำหนดค่าหลายรายการและสลับระหว่างการกำหนดค่าเหล่านั้นตามตัวแปรสภาพแวดล้อม
นี่เป็นตัวอย่างทั่วไป:
(defpackage :myapp.config (:use :cl:envy)) (ในแพ็คเกจ :myapp.config) (setf (config-env-var) "APP_ENV") (defconfig :common `(:application-root ,(asdf:component-pathname (asdf:find-system :myapp)))) (defconfig |การพัฒนา| `(:debug T:databases((:maindb :sqlite3 :database-name ,(merge-pathnames #P"test.db"*application-root*))))) (defconfig |การผลิต| '(:databases((:maindb :mysql :database-name "myapp" :username "whoami" :password "1234") (:workerdb :mysql :ชื่อฐานข้อมูล "งาน" :ชื่อผู้ใช้ "whoami" :รหัสผ่าน "1234")))) (defconfig |การแสดงละคร| `(:debug T,@|การผลิต|))
ทุกการกำหนดค่าคือรายการคุณสมบัติ คุณสามารถเลือกการกำหนดค่าที่จะใช้โดยการตั้งค่า APP_ENV
หากต้องการรับค่าจากการกำหนดค่าปัจจุบัน ให้เรียก myapp.config:config
ด้วยคีย์ที่คุณต้องการ
(นำเข้า 'myapp.config:config) (setf (osicat: ตัวแปรสภาพแวดล้อม "APP_ENV") "การพัฒนา") (config :debug);=> T
เมื่อคุณเพิ่ม :databases
ในการกำหนดค่า Caveman จะเปิดใช้งานการรองรับฐานข้อมูล :databases
คือรายการเชื่อมโยงของการตั้งค่าฐานข้อมูล
(defconfig |การผลิต| '(:databases((:maindb :mysql :database-name "myapp" :username "whoami" :password "1234") (:workerdb :mysql :ชื่อฐานข้อมูล "งาน" :ชื่อผู้ใช้ "whoami" :รหัสผ่าน "1234"))))
db
ในแพ็คเกจ myapp.db
เป็นฟังก์ชันสำหรับเชื่อมต่อกับแต่ละฐานข้อมูลที่กำหนดค่าไว้ข้างต้น นี่คือตัวอย่าง
(ใช้แพ็คเกจ '(:myapp.db :sxql :datafly)) (defun ผู้ค้นหาผู้ใหญ่ () (พร้อมการเชื่อมต่อ (db) (ดึงข้อมูลทั้งหมด (เลือก :*(จาก :บุคคล) (โดยที่ (:>= :อายุ 20))))))
การเชื่อมต่อยังคงมีอยู่ในระหว่างเซสชัน Lisp และจะถูกนำมาใช้ซ้ำในทุกคำขอ HTTP
retrieve-all
และภาษาคิวรีมาจาก datafly และ SxQL ดูชุดเอกสารประกอบเหล่านั้นสำหรับข้อมูลเพิ่มเติม
มีตัวแปรพิเศษหลายตัวที่พร้อมใช้งานในระหว่างการร้องขอ HTTP *request*
และ *response*
แสดงถึงคำขอและการตอบกลับ หากคุณคุ้นเคยกับ Clack นี่เป็นอินสแตนซ์ของคลาสย่อยของ Clack.Request และ Clack.Response
(ใช้แพ็คเกจ: Caveman2);; รับค่าส่วนหัวของผู้อ้างอิง (http-referer *request*);; ตั้งค่าส่วนหัวประเภทเนื้อหา (setf (getf (response-headers *response*) :content-type) "application/json");; ตั้งค่าสถานะ HTTP (setf (สถานะ *ตอบกลับ*) 304)
หากคุณต้องการตั้งค่าประเภทเนื้อหา "application/json" สำหรับคำขอ "*.json" ทั้งหมด คุณสามารถใช้ next-route
ได้
(ยกเลิก "/*.json" () (setf (getf (ส่วนหัวตอบกลับ *ตอบกลับ*) :ประเภทเนื้อหา) "application/json") (เส้นทางถัดไป)) (defroute "/user.json" () ...) (defroute "/search.json" () ...) (defroute ("/new.json" :method :POST) () ...)
ข้อมูลเซสชันมีไว้เพื่อการจดจำข้อมูลเฉพาะของผู้ใช้ *session*
เป็นตารางแฮชที่เก็บข้อมูลเซสชัน
ตัวอย่างนี้เพิ่ม :counter
ในเซสชัน และแสดงสำหรับผู้เยี่ยมชมแต่ละคน
(ละลาย "/ ตัวนับ" () (ไม่มีรูปแบบ "คุณมาที่นี่ ~A ครั้ง" (incf (gethash :counter *session* 0))))
Caveman2 เก็บข้อมูลเซสชันไว้ในหน่วยความจำตามค่าเริ่มต้น หากต้องการเปลี่ยนแปลง ให้ระบุ :store
เป็น :session
ใน "PROJECT_ROOT/app.lisp"
ตัวอย่างนี้ใช้ RDBMS เพื่อจัดเก็บข้อมูลเซสชัน
'(:ย้อนรอย :output (getf (config) :error-log)) ไม่มี)- :session+ (:session+ :store (make-dbi-store :connector (lambda ()+ (ใช้ #'dbi:connect+ (myapp.db:connection-settings))))) (ถ้า (การผลิตp) ไม่มี (แลมบ์ดา (แอป)
หมายเหตุ: อย่าลืมเพิ่ม :lack-session-store-dbi
เป็น :depends-on
ของแอปของคุณ มันไม่ได้เป็นส่วนหนึ่งของ Clack/Lack
ดูซอร์สโค้ดของ Lack.Session.Store.DBi สำหรับข้อมูลเพิ่มเติม
ขาดเซสชัน Store.Dbi
(นำเข้า 'Caveman2:throw-code) (defroute ("/auth" :method :POST) (&key |name| |password|) (เว้นแต่ (อนุญาต |ชื่อ| |รหัสผ่าน|) (โยนรหัส 403)))
หากต้องการระบุหน้าข้อผิดพลาดสำหรับ 404, 500 หรือเช่นนั้น ให้กำหนดวิธี on-exception
แอปของคุณ
(defmethod ข้อยกเว้น ((app <web>) (รหัส (eql 404))) (ประกาศ (ละเว้นรหัสแอป)) (ชื่อพาธผสาน #P"_errors/404.html" *template-directory*))
แม้ว่า Caveman จะไม่มีคุณสมบัติสำหรับการปรับใช้แบบร้อน แต่ Server::Starter ซึ่งเป็นโมดูล Perl ทำให้เป็นเรื่องง่าย
$ APP_ENV=production start_server --port 8080 -- clackup --server :fcgi app.lisp
หมายเหตุ: Server::Starter ต้องการให้เซิร์ฟเวอร์รองรับการผูก fd เฉพาะ ซึ่งหมายความว่ามีเพียง :fcgi
และ :woo
เท่านั้นที่ทำงานกับคำสั่ง start_server
หากต้องการรีสตาร์ทเซิร์ฟเวอร์ ให้ส่งสัญญาณ HUP ( kill -HUP <pid>
) ไปยังกระบวนการ start_server
Caveman ส่งออก error backtraces ไปยังไฟล์ที่ระบุไว้ที่ :error-log
ในการกำหนดค่าของคุณ
(defconfig |default| `(:error-log #P"/var/log/apps/myapp_error.log":databases((:maindb :sqlite3 :database-name ,(merge-pathnames #P"myapp.db"*) แอปพลิเคชันรูท *)))))
(นำเข้า 'cl-who:with-html-output-to-string) (ละลาย "/" () (with-html-output-to-string (เอาต์พุตไม่มี :prologue t) (:html (:head (:title "ยินดีต้อนรับสู่มนุษย์ถ้ำ!")) (:body "Blah blah blah."))));=> "<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/ xhtml1/DTD/xhtml1-strict.dtd">; <html><head><title>ยินดีต้อนรับสู่มนุษย์ถ้ำ!</title></head><body>บลา บลา บลา</body></html>"
เว็บไซต์ CL-WHO
(นำเข้า 'cl-markup:xhtml) (ละลาย "/" () (xhtml (:head (:title "ยินดีต้อนรับสู่มนุษย์ถ้ำ!")) (:body "Blah blah blah.")));=> "<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional/ /EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html><head><title>ยินดีต้อนรับสู่ มนุษย์ถ้ำ!</title></head><body>บลา บลา บลา</body></html>"
พื้นที่เก็บข้อมูล CL-Markup
{เนมสเปซ myapp.view} {เทมเพลต renderIndex}<!DOCTYPE html><html><head> <title>"ยินดีต้อนรับสู่มนุษย์ถ้ำ!</title></head><body> บลา บลา บลา</body></html>{/template}
(นำเข้า 'myapp.config:*template-directory*) (ปิดเทมเพลต:compile-cl-templates (ผสานชื่อพาธ #P"index.tmpl"*template-directory*)) (ละลาย "/" () (myapp.view:render-index))
cl-ปิดแม่แบบ
เอกสารประกอบเทมเพลตการปิด
Clack - สภาพแวดล้อมเว็บแอปพลิเคชัน
ขาด - แกนกลางของ Clack
ningle - เฟรมเวิร์กแอปพลิเคชันเว็บระดับไมโครที่ Caveman ใช้
Djula - เอ็นจิ้นการสร้างเทมเพลต HTML
CL-DBI - ไลบรารีอินเทอร์เฟซที่ไม่ขึ้นกับฐานข้อมูล
SxQL - ไลบรารีตัวสร้าง SQL
Envy - ตัวสลับการกำหนดค่า
รอสเวลล์ - ผู้จัดการการใช้งาน Lisp ทั่วไป
เออิทาโร่ ฟุคามาชิ ([email protected])
ได้รับอนุญาตภายใต้ใบอนุญาต LLGPL