echo Impossible|sed 's/Im/To be /'

December 25, 2017

Lisp Web - Caveman2 Clack Hunchentoot Woo - Framework

使用 Framework 開發,可以降低工作、減少出錯、方便移植等優點。Caveman2 架構在 Clack 上,可以抽換底層使用的 Web Server,無論是 Hunchentoot FastCGI Wookie Toot 或是 Woo。並且它也協助作好串接資料庫的介面。

以下文件大量參考 https://github.com/fukamachi/caveman/blob/master/README.markdown

Install and hello world

由於使用 quicklisp 作為套件管理,在建立專案時請把路徑放在 quicklisp/local-projects/ 底下,方便之後用 quickload 載入。

(ql:quickload :caveman2)
(ql:quickload :hunchentoot)

(caveman2:make-project #P"~/quicklisp/local-projects/caveman2-test/"
                 :author "<Your full name>")

;; writing ~/quicklisp/local-projects/caveman2-test/caveman2-test.asd
;; writing ~/quicklisp/local-projects/caveman2-test/caveman2-test-test.asd
;; skip .....

(ql:quickload :caveman2-test)
;; To load "caveman2-test":

(caveman2-test:start)
;; Hunchentoot server is started. <--- Hunchentoot
;; Listening on localhost:5000.   <--- port 5000
(caveman2-test:stop)

(ql:quickload :woo)
(caveman2-test:start :server :woo :port 8080)
;; Woo server is started.      <--- Woo
;; Listening on localhost:8080 <--- Port 8080
(caveman2-test:stop)

(caveman2-test:start :server :woo :address "192.168.1.1")
;; woo 需要指定對外的 ip address
;; hunchentoot 預設對外 ip 及 localhost 同時監聽
(caveman2-test:stop)

(caveman2-test:start)
;; 切回預設值 hunchentoot port 5000

打開 browser 訪問 http://localhost:5000 您將會看到 Caveman2 Welcome Page,這即表示建好 project 了。

Routing

routeing 的表示方式有二種 -- @route 與 defroute。以下範例請在 lisp repl 交談模式下鍵入。

(ql:quickload :caveman2-test)
(caveman2-test:start)
(in-package :caveman2-test.web)

;; A route with no name.
@route GET "/welcome0"
(lambda (&key (|name| "Guest"))
  (format nil "Welcome0, ~A" |name|))

;; A route with welcome1 function name.
@route GET "/welcome1"
(defun welcome1 (&key (|name| "Guest"))
  (format nil "Welcome1, ~A" |name|))

;; A route with no name.
(defroute "/welcome2" (&key (|name| "Guest"))
  (format nil "Welcome2, ~A" |name|))

;; 多個參數傳遞
(defroute "/welcome3" (&key (|n| "Guest") (|l| "who"))
  (format nil "Welcome3, ~A ~A" |n| |l|))

以 curl 的方式測試如下:

$> curl "http://localhost:5000/welcome0"
Welcome0, Guest

$> curl "http://localhost:5000/welcome1?"
Welcome1, Guest

$> curl "http://localhost:5000/welcome2?name=Joe"
Welcome2, Joe

$> curl "http://localhost:5000/welcome3?n=Joe&l=Jo"
Welcome3, Joe Jo

Route pattern *keyword*

URL path 可以用 *keyword* 的方式傳入程式中。

(defroute "/hello/:name/:last-name/" (&key name last-name)
  (if (string= name "nobody")
   (next-route)
   (if (string= name "yoda")
    (format nil "You are master ~A ~A" name last-name)
    (format nil "Hello, ~A ~A" name last-name))))

(defroute "/hello/*/*" ()
  "You missed!")

(defroute "/hello/*" ()
  "You missed!")

以 curl 的方式測試如下:

$> curl "http://localhost:5000/hello/Joe/Jo/"
Hello, Joe Jo

$> curl "http://localhost:5000/hello/Joe/"
You missed!

$> curl "http://localhost:5000/hello/"
You missed!

$> curl "http://localhost:5000/hello/yoda/lin/"
You are master yoda lin

$> curl "http://localhost:5000/hello/nobody/who/"
You missed!

Route pattern *wildcard* (星字元/萬用字元)

URL path 也可以用 * 萬用字元來表示,而把參數傳入程式中。

(defroute "/say/*/to/*" (&key splat)
  (format nil "~S" splat))

(defroute "/download/*.*" (&key splat)
  (format nil "~S" splat))

以 curl 的方式測試如下:

$> curl "http://localhost:5000/say/hello/to/world"
("hello" "world")

$> curl "http://localhost:5000/download/path/to/file.xml"
("path/to/file" "xml")
$> curl "http://localhost:5000/download/path/to/file.xml.txt"
("path/to/file" "xml.txt")

Route pattern with regular expression (正規表示式)

URL path 也可以用 regular expression (正規表示式),把需要的參數傳入程式中。

(defroute ("/regexp_hello/([\\w]+)" :regexp t) (&key captures)
  (format nil "Hello, ~S" captures))

(defroute ("/regexp-hello/([\\w]+)" :regexp t) (&key captures)
  (format nil "Hello, ~S" captures))
$> curl "http://localhost:50000/regexp_hello/yoda is master/YO!"
Hello, ("yoda")

# I got the problem!! why ("y") not ("yoda"). Need time for check it.
$> curl "http://localhost:50000/regexp-hello/yoda is master/YO!"
Hello, ("y")

Structured query/post parameters

以下是 form-data 分別透過 get 與 post 的方式傳遞表單,最後以 plist 的資料方式呈現。

(ql:quickload :cl-who)

(defroute "/edit-form" ()
  (cl-who:with-html-output-to-string (s nil :prologue t :indent t)
    (:form :action "/edit"
     (:input :type "text" :name "person[name]")
     (:input :type "text" :name "person[email]")
     (:input :type "text" :name "person[birth][year]")
     (:input :type "text" :name "person[birth][month]")
     (:input :type "text" :name "person[birth][day]")
     (:input :type "submit" :value "Add"))))

(defroute "/edit" (&key _parsed)
  (format nil "~S" (cdr (assoc "person" _parsed :test #'string=))))

(defroute "/edit-form-post" ()
  (cl-who:with-html-output-to-string (s nil :prologue t :indent t)
    (:form :action "/edit-post" :method "post"              ;; add method = post
     (:input :type "text" :name "person[name]")
     (:input :type "text" :name "person[email]")
     (:input :type "text" :name "person[birth][year]")
     (:input :type "text" :name "person[birth][month]")
     (:input :type "text" :name "person[birth][day]")
     (:input :type "submit" :value "Add"))))

(defroute ("/edit-post" :method :POST) (&key _parsed) ;; add method = post
  (format nil "~S" (cdr (assoc "person" _parsed :test #'string=))))

(defroute "/add-form" ()
  (cl-who:with-html-output-to-string (s nil :prologue t :indent t)
    (:form :action "/add"
     (:input :type "text" :name "items[][name]")
     (:input :type "text" :name "items[][price]")
     (:input :type "text" :name "items[][name]")
     (:input :type "text" :name "items[][price]")
     (:input :type "submit" :value "Add"))))

(defroute "/add-form-post" ()
  (cl-who:with-html-output-to-string (s nil :prologue t :indent t)
    (:form :action "/add" :method "post"        ;; add method = post
     (:input :type "text" :name "items[][name]")
     (:input :type "text" :name "items[][price]")
     (:input :type "text" :name "items[][name]")
     (:input :type "text" :name "items[][price]")
     (:input :type "submit" :value "Add"))))

(defroute ("/add"  :method :ANY) (&key _parsed) ;; add method = any 同時收 get/post
  (format nil "~S" (assoc "items" _parsed :test #'string=)))

輸出範例:

http://localhost:5000/edit-form output
http://localhost:5000/edit-form-post output
(("name" . "Eitaro Fukamachi") ("email" . "e.arrows@gmail.com") ("birth" ("year" . "2018") ("month" . "01") ("day" . "01")))

http://localhost:5000/add-from output
http://localhost:5000/add-from-post output
("items" (("name" . "pencil") ("price" . "5")) (("name" . "stapler") ("price" . "70")))

Templates

考慮用前端工具 vue.js ,brower 畫面由 client side browser render 完成,故這段暫時略過。

JSON API

json api 很常用,修改範例如下:

(defroute "/user.json" (&key |id|)
  ;; (let ((person (find-person-from-db |id|)))
  (let ((person '(:|name| "Eitaro Fukamachi" :|email| "e.arrows@gmail.com")))
    (render-json person)))
$> curl "http://localhost:5000/user.json?id=0001"
{"name":"Eitaro Fukamachi","email":"e.arrows@gmail.com"}

Static file

靜態檔案擺放的位置,可透過修改 app.lisp 更改位置,但不建議。

/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

寫入檔案 reload project 執行測試

新增套件相依

修改 caveman2-test.asd 把 cl-who 加入套件相依如下:

--- caveman2-test.asd.org  2017-12-18 14:33:18.509177123 +0800
+++ caveman2-test.asd      2017-12-18 17:37:03.675672573 +0800
@@ -8,7 +8,8 @@
            "envy"
            "cl-ppcre"
            "uiop"
-
+              "cl-who"
+
            ;; for @route annotation
            "cl-syntax-annot

修改 src/web.lisp

新增二項

  • import **cl-whowith-html-output-to-string* from *
  • add defroute "/welcome"
--- web.lisp.org        2017-12-19 09:03:17.750820058 +0800
+++ web.lisp            2017-12-19 16:49:56.151354622 +0800
@@ -7,6 +7,8 @@
     :caveman2-test.db
     :datafly
     :sxql)
+  (:import-from :cl-who
+               :with-html-output-to-string)
   (:export :*web*))
 (in-package :caveman2-test.web)

@@ -26,6 +28,12 @@
 (defroute "/" ()
   (render #P"index.html"))

+(defroute "/welcome" ()
+  (with-html-output-to-string (s nil :prologue t :indent t)
+    (:html
+     (:head (:title "Welcome to Caveman2!"))
+     (:body (:h1 "Blah blah blah.")))))
+
 ;;
 ;; Error pages

reload 執行

以下在 lisp repl 介面下執行,停掉、reload、重啟服務。

(caveman2-test:stop)
(ql:quickload :caveman2-test)
(caveman2-test:start)

測試

$> curl "http://localhost:5000/welcome"
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html>
  <head>
    <title>Welcome to Caveman!
    </title>
  </head>
  <body>
    <h1>Blah blah blah.
    </h1>
  </body>
</html>

小結

CL (common lisp) 在 backend 的開發上已相當的成熟了,而且 Woo 的 效能測試 非常讓人驚豔,雖然仍在 beta 狀態中,但未來作為 microservice 相當令人期待。

餘下的部份如 ** Throw an HTTP status code** Using session** Set HTTP headers or HTTP statusDatabase* *之後在找時間補上。

Posted by Lloyd Huang in on December 25, 2017