(defparameter *web* (make-instance '<app>)) @route GET“/”(defun索引() (渲染#P“index.tmpl”)) @route GET "/hello"(defun say-hello (&key (|name| "Guest")) (格式为零“你好,〜A”|名称|))
一切。 Caveman2 是从头开始编写的。
这些都是值得注意的点。
基于ningle
具有数据库集成
使用新的独立配置系统(Envy)
有新的路由宏
最常见的问题之一是“我应该使用哪一个:ningle 还是 Caveman?有什么区别?”我认为这些问题被如此频繁地问到是因为 Caveman 和 ningle 太相似了。它们都被称为“micro”,并且没有数据库支持。
有了Caveman2,Caveman 不再是一个“微型”Web 应用程序框架。支持CL-DBI,默认具有数据库连接管理。穴居人已经开始长大。
Caveman 旨在成为 Web 应用程序公共部分的集合。对于 Caveman2,我使用三个规则来做出决定:
可扩展。
务实一点。
不要强迫任何事情。
你来这里是因为你有兴趣像穴居人一样生活,对吗?这不是迪士尼乐园,但我们可以从这里开始。我们进入一个山洞吧!
Caveman2 现已在 Quicklisp 上提供。
(ql:快速加载:穴居人2)
(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
函数来启动/停止您的 Web 应用程序。
(myapp:启动:端口 8080)
由于 Caveman 基于 Clack/Lack,因此您可以选择在哪个服务器上运行 - Hunchentoot、Woo 或 Wookie 等。
(myapp:启动:服务器:hunchentoot:端口 8080) (myapp:启动:服务器:fcgi:端口 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 提供了两种定义路由的方法 - @route
和defroute
。您可以使用其中任何一个。
@route
是一个注释宏,使用cl-annot定义。它需要一个方法、一个 URL 字符串和一个函数。
@route GET“/”(defun索引() ...);;没有名称的路线。@route GET "/welcome"(lambda (&key (|name| "Guest")) (格式为零“欢迎,〜A”|名称|))
除了其参数列表之外,这与 Caveman1 的@url
类似。当不需要时,您不必指定参数。
defroute
只是一个宏。它提供与@route
相同的功能。
(defroute索引“/”() ...);;没有名称的路线。(defroute "/welcome" (&key (|name| "Guest")) (格式为零“欢迎,〜A”|名称|))
由于Caveman基于ningle,所以Caveman也有类似Sinatra的路由系统。
;; GET 请求(默认)@route GET "/" (lambda () ...) (defroute(“/”:方法:GET)()...);; POST 请求@路由 POST "/" (lambda () ...) (defroute(“/”:方法:POST)()...);; PUT 请求@路由 PUT "/" (lambda () ...) (defroute(“/”:方法:PUT)()...);;删除请求@路由删除“/”(lambda()...) (defroute(“/”:方法:删除)()...);;选项 request@route 选项 "/" (lambda () ...) (defroute(“/”:方法:选项)()...);;对于所有方法@route ANY "/" (lambda () ...) (defroute(“/”:方法:ANY)()...)
路由模式可能包含“关键字”以将值放入参数中。
(defroute "/hello/:name" (&键名称) (格式为零“你好,~A”名字))
当您访问“/hello/Eitaro”或“/hello/Tomohiro”时,将调用上述控制器, name
将根据情况为“Eitaro”或“Tomohiro”。
(&key name)
与 Common Lisp 的 lambda 列表几乎相同,只是它总是允许其他键。
(defroute "/hello/:name" (&rest params &key name) ;; ... )
路由模式还可以包含“通配符”参数。可以使用splat
访问它们。
(defroute "/say/*/to/*" (&key splat) ; 匹配 /say/hello/to/world (format nil "~A" splat));=> (hello world)(defroute "/download/*.*" (&key splat) ; 匹配 /download/path/to/file.xml (格式为零“〜A”splat)) ;=>(路径/到/文件 xml)
如果您想在 URL 规则中使用正则表达式, :regexp t
应该可以。
(defroute ("/hello/([w]+)" :regexp t) (&key 捕获) (格式为零“你好,〜A!”(第一次捕获)))
通常,路由按照定义的顺序进行匹配测试,并且仅调用第一个匹配的路由,而忽略后续路由。但是,路由可以通过包含next-route
来继续测试列表中的匹配项。
(defroute "/guess/:who" (&key who) (if (string= who "Eitaro") "你抓住我了!" (next-route))) (defroute "/guess/*" () "你错过了!")
您可以返回以下格式作为defroute
的结果。
细绳
路径名
Clack 的回复列表(包含 Status、Headers 和 Body)
使用(redirect "url")
重定向到另一个路由。第二个可选参数是状态代码,默认情况下为 302。
当您使用名称定义路由时,您可以使用(url-for route-name &rest params)
从名称中找到 URL。
如果没有找到路由,该函数将抛出错误。
参见:
add-query-parameters base-url params
包含方括号(“[”和“]”)的参数键将被解析为结构化参数。您可以在路由器中以 _parsed _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) (format nil "~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) (格式nil“~S”(aget_parsed“人”)))
空白键意味着它们有多个值。
<表单操作=“/添加”> <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" /></form>
(defroute“/add”(&key_parsed) (format nil "~S" (assoc "items" _parsed :test #'string=)));=> "((("名称" . "WiiU") ("价格" . "30000")) (("名称" . "PS4") ("价格" . "69000")))"
Caveman 使用 Djula 作为其默认模板引擎。
{% 扩展“layouts/default.html” %} {% 块标题 %}用户 | MyApp{% endblock %} {% 块内容 %}<div id="main"> <ul> {% for users in users %}<li><a href="{{ user.url }}">{{ user.name }}</a></li> {% endfor %} </ul></div>{% endblock %}
(导入'myapp.view:渲染) (渲染 #P"users.html"'(:users ((:url "/id/1" :name "硝基白痴") (:url "/id/2" :name "meymao")) :有下一页 T))
如果您想从数据库获取某些内容或使用 Djula 执行函数,则必须在将参数传递给 render 时显式调用list
以便代码执行。
(导入'myapp.view:渲染) (渲染 #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 person)));=> {"name":"Eitaro Fukamachi","email":"[email protected]"}
render-json
是骨架项目的一部分。你可以在“src/view.lisp”中找到它的代码。
默认情况下将提供“static/”目录中的图像、CSS、JS、favicon.ico 和 robots.txt。
/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 (:使用:cl:羡慕)) (包内:myapp.config) (setf(config-env-var)“APP_ENV”) (defconfig:通用 `(:应用程序根,(asdf:组件路径名(asdf:find-system:myapp)))) (defconfig |development| `(:debug T:databases((:maindb :sqlite3 :database-name ,(merge-pathnames #P"test.db"*application-root*))))) (defconfig |生产| '(:数据库((:maindb :mysql :数据库名称“myapp”:用户名“whoami”:密码“1234”) (:workerdb:mysql:数据库名称“jobs”:用户名“whoami”:密码“1234”)))) (defconfig |staging| `(:debug T,@|生产|))
每个配置都是一个属性列表。您可以通过设置APP_ENV
选择要使用的配置。
要从当前配置中获取值,请使用所需的密钥调用myapp.config:config
。
(导入'myapp.config:配置) (setf(osicat:环境变量“APP_ENV”)“开发”) (配置:调试);=> T
当您将:databases
添加到配置中时,Caveman 会启用数据库支持。 :databases
是数据库设置的关联列表。
(defconfig |生产| '(:数据库((:maindb :mysql :数据库名称“myapp”:用户名“whoami”:密码“1234”) (:workerdb:mysql:数据库名称“jobs”:用户名“whoami”:密码“1234”))))
myapp.db
包中的db
是用于连接到上面配置的每个数据库的函数。这是一个例子。
(使用包'(:myapp.db:sxql:datafly)) (defun 搜索-成人 () (有连接(db) (检索所有 (选择:*(来自:人) (其中 (:>= : 20 岁))))))
连接在 Lisp 会话期间处于活动状态,并将在每个 HTTP 请求中重用。
retrieve-all
和查询语言来自 datafly 和 SxQL。请参阅这些文档集以获取更多信息。
HTTP 请求期间有几个可用的特殊变量。 *request*
和*response*
代表请求和响应。如果您熟悉 Clack,它们是 Clack.Request 和 Clack.Response 子类的实例。
(使用包:caveman2);;获取Referer header的值。(http-referer *request*);;设置 Content-Type 标头。(setf (getf (response-headers *response*) :content-type) "application/json");;设置 HTTP 状态。(setf (status *response*) 304)
如果您想为所有“*.json”请求设置Content-Type“application/json”,则可以使用next-route
。
(defroute "/*.json" () (setf(getf(响应头*响应*):内容类型)“application/json”) (下一条路线)) (defroute "/user.json" () ...) (defroute "/search.json" () ...) (defroute(“/new.json”:方法:POST)()...)
会话数据用于存储用户特定的数据。 *session*
是存储会话数据的哈希表。
此示例递增会话中的:counter
,并为每个访问者显示它。
(defroute "/counter" () (格式nil“你来过这里~A次。”(incf (gethash :counter *session* 0))))
Caveman2 默认将会话数据存储在内存中。要更改此设置,请在“PROJECT_ROOT/app.lisp”中将:store
指定为:session
。
此示例使用 RDBMS 来存储会话数据。
'(:回溯 :输出(getf(配置):错误日志)) nil)- :session+ (:session+ :store (make-dbi-store :connector (lambda ()+ (apply #'dbi:connect+ (myapp.db:connection-settings))))) (如果(生产p) 零 (拉姆达(应用程序)
注意:不要忘记添加:lack-session-store-dbi
作为:depends-on
您的应用程序。它不是 Clack/Lack 的一部分。
有关更多信息,请参阅 Lack.Session.Store.DBi 的源代码。
缺少Session.Store.Dbi
(导入'caveman2:抛出代码) (defroute ("/auth" :方法 :POST) (&key |名称| |密码|) (除非(授权 |名称| |密码|) (抛出代码 403)))
要指定 404、500 等错误页面,请定义应用程序的on-exception
方法。
(defmethod on-exception ((app <web>) (代码 (eql 404))) (声明(忽略应用程序代码)) (合并路径名 #P"_errors/404.html" *模板目录*))
尽管 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-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 (输出 nil :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-世界卫生组织网站
(导入'cl-标记: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-标记存储库
{命名空间 myapp.view} {模板 renderIndex}<!DOCTYPE html><html><head> <title>“欢迎来到穴居人!</title></head><body> 等等等等。</body></html>{/template}
(导入'myapp.config:*模板目录*) (closure-template:compile-cl-templates (merge-pathnames #P"index.tmpl"*template-directory*)) (默认路由“/”() (myapp.view:渲染索引))
cl 闭合模板
闭包模板文档
Clack - Web 应用程序环境。
Lack - Clack 的核心。
ningle - Caveman 所基于的超微型 Web 应用程序框架。
Djula - HTML 模板引擎。
CL-DBI - 独立于数据库的接口库。
SxQL - SQL 构建器库。
Envy - 配置切换器。
Roswell - Common Lisp 实施经理。
深町荣太郎 ([email protected])
根据 LLGPL 许可证获得许可。