Caveman——看到这个词很多程序员的第一反应是“穴居人调试法”出问题了不打断点直接print糊脸。但在 Common Lisp 圈子里Caveman 是另一回事一个小巧、直接、够用的 Web 开发框架常被比作 Lisp 世界里的 Flask 或 Sinatra。它不是玩具我拿它写过内部 API、自动化面板和产品原型整体体验可以用三个字概括轻、快、糙。框架本身轻跑起来快资料糙得全靠自己啃。这篇不打算写成官方文档的翻译流水账我会把 Caveman2 的路由、模板、数据库、部署、常见坑完整过一遍最后落地一个能直接跑的 REST API 例子。适合谁看有 Lisp 基础、想试试用 Lisp 写 Web 服务的人被全栈框架体积劝退、想看看冷门技术怎么走通工程的人也包括单纯好奇“为什么还有人用 Lisp”的人。没有 Lisp 基础也能看懂大部分内容遇到宏和符号我会顺手解释不用怕。Caveman 的 REPL 热更新体验值得每个被“改完代码必须重启”折磨过的开发者体验一次。1. 内容整体设计与思路拆解1.1 Caveman 的定位它不是框架是“骨架”Caveman 的定位非常明确微框架或者说路由框架。它把 Web 应用最核心的“URL 分发”这件事用defroute宏做成了 DSL然后通过 Clack 接到实际 HTTP 服务器上。其余的东西模板、ORM、静态文件、Session都是“你自己选框架不绑架你”。这和 Django、Rails 那种全家桶路线完全是两个方向。Django 给你 admin、auth、ORM、migration、表单校验一套下来很爽但代价是你得按它的世界观组织代码。Caveman 走的是 Sinatra 式的“少即是多”框架只负责把 HTTP 请求变成 Lisp 函数调用剩下的工程决策你自己来。项目名里的“Caveman”多少带点自嘲——Common Lisp 被当成上古语言拿它写 Web 应用已经够“原始人”了但框架本身的设计却很现代Clack 中间件体系、模板引擎、ORM 接口都是能打的。1.2 为什么 2024 年还在用 Lisp 写 Web很多人觉得 Lisp 是学术玩具其实它做 Web 服务有个巨大的杀手锏交互式开发。普通开发流程是“改代码、重启、看效果”Lisp 的流程是“改一个函数、在 REPL 里编译这个函数、下一次请求就生效”服务根本不重启。用做饭打比方别人是炒菜关火再尝味Lisp 是锅不关火拿勺子舀一口尝尝不够咸再放盐味道一瞬间就到。对需要频繁调接口逻辑、调试数据结构的场景这种体验能节省大量时间。另外Lisp 的宏是真能用来设计语言的。defroute看起来像关键字一样的东西本质上就是一个宏把路由路径、HTTP 方法和处理函数绑定在一起。如果你想给自己的项目加一套业务 DSL比如“权限校验注解”或者“自动参数绑定”用宏写会比 Java 的注解处理器、Python 的装饰器更自由、更顺手。性能层面SBCL 编译出来的代码可以接近 C跑 API 网关、工具类服务绰绰有余。1.3 与主流框架的选型对比框架定位自带能力性能体感上手难度生态Caveman2微框架路由 Clack接入高中高Lisp门槛小众但齐全Flask微框架路由 模板中低大Express微框架路由中低大Django全栈admin/ORM/Auth中中大Sinatra微框架路由中低中选型核心是看团队和场景。如果你只是为了个人项目或者内部工具Caveman 的成本完全可以接受如果是大型团队协作、招聘主流开发者那 Lisp 的生态劣势就会被放大别硬上。技术选型不只要看语言爽不爽还要看未来接手的人哭不哭。2. 核心细节解析与实操要点2.1 环境准备SBCL Quicklisp 编辑器Caveman2 跑在 Common Lisp 上最常用的实现是 SBCL。安装很简单macOS 用brew install sbclUbuntu 用apt install sbclWindows 从官网下载二进制包。装好之后装 Quicklisp这是 Lisp 世界的包管理器类似 Python 的 pip、Node 的 npmcurl -O https://beta.quicklisp.org/quicklisp.lisp sbcl --load quicklisp.lisp在 SBCL 的 REPL 里执行(quicklisp-quickstart:install) (ql:add-to-init-file)最后一句会往你的 Lisp 启动配置里写入加载 Quicklisp 的代码以后每次打开 SBCL 都能直接用ql:quickload。接着加载 Caveman2(ql:quickload :caveman2)首次加载会拉取一堆依赖耐心等。如果网络差Quicklisp 的 dist 源可以切换镜像网上有现成方案这里不展开。编辑器我强烈推荐 Emacs SLIME这是 Lisp 开发的“原装体验”C-c C-c编译当前函数、C-c C-z切到 REPL丝滑得不行。如果实在不想碰 EmacsVS Code 装 Common Lisp 扩展也能凑合用但没有 SLIME 那种“和运行中的程序直接对话”的爽感。2.2 生成项目骨架别从零开始Caveman2 自带项目生成器别傻乎乎手写目录结构。在 REPL 里执行(caveman2:create-project #p/Users/you/bookmark-api :template-engine :djula)这会生成一个完整骨架bookmark-api/ bookmark-api.asd src/ main.lisp config.lisp app.lisp view.lisp db.lisp templates/ static/ README.md.asd文件是 Lisp 的系统定义文件相当于package.json里面声明了依赖、组件、加载顺序。新手最容易晕的地方在这为什么改了代码不生效多半是.asd里没有加上你新写的源文件或者没有在 REPL 里重新编译系统。目录里main.lisp管启动停止app.lisp定义路由config.lisp管配置分工很清晰。2.3 路由定义与参数获取Caveman2 最核心的 API 就一个defroute。最简单的路由(defroute (/ :method :get) () Hello, Caveman!)路径参数直接用冒号声明(defroute (/users/:id :method :get) (id) (format nil user id: ~a id))这里的id是字符串不是数字。做数据库查询前记得parse-integer这是新手最容易翻车的地方。Query string 的获取方式和路径参数不同需要用lack.request:request-parameters(defroute (/search :method :get) () (let ((q (cdr (assoc q (lack.request:request-parameters *request*))))) (format nil search: ~a q)))POST 表单的解析方式类似从*request*里提取参数。*request*是 Caveman 在请求周期内动态绑定的变量由中间件填充。这里要记住一个观念Caveman 不隐藏 HTTP 细节参数解析、请求头、响应码都暴露给你控制少了魔法多了透明。2.4 模板与静态资源如果你不是做纯前后端分离模板引擎是刚需。Caveman2 默认支持 Djula它的语法和 Django 模板很像。在config.lisp里把模板目录加进去(djula:add-template-directory #ptemplates/)渲染模板(djula:render-template* index.html nil :title 首页 :items (1 2 3))静态文件需要手动挂中间件。Caveman2 的app.lisp里通常有一段(lack:builder (:static :path /static :root *static-directory*) :accesslog *app*)这行的意思是静态文件请求走:static中间件其他请求走应用主体。*static-directory*是项目里的static/目录放到线上时建议改成绝对路径否则在奇怪的工作目录下启动会 404。3. 实操过程与核心环节实现3.1 目标写一个最小的书签 API选书签管理做例子因为它领域简单、场景完整刚好能串起 MVC 三层Model 用 MitoView 返回 JSONController 就是defroute。先创建项目(caveman2:create-project #p/Users/you/bookmark-api :template-engine :djula)进入目录在src/db.lisp里定义数据库模型(mito:deftable bookmark () ((title :col-type (:varchar 200)) (url :col-type (:varchar 500)) (created-at :col-type (:timestamp) :initform (local-time:now))))然后初始化数据库(mito:connect-toplevel :sqlite3 :database-name #Pbookmark.db) (mito:ensure-tables-exist bookmark)ensure-tables-exist会自动根据deftable建表相当于简单版 migration。这里有人会问如果表结构变了怎么办麻烦点Mito 没有自动改表的能力需要手动ALTER TABLE或者删了重建。写个人项目无所谓上生产就要谨慎设计表结构。3.2 核心路由实现在src/app.lisp里写路由。先写查询全部书签(defroute (/api/bookmarks :method :get) () (jonathan:to-json (loop for b in (mito:select-dao bookmark) collect (list :id (mito:object-id b) :title (slot-value b title) :url (slot-value b url)))))mito:select-dao返回所有记录mito:object-id取主键。这里要注意不能直接对bookmark对象调jonathan:to-jsonMito 的 DAO 对象里包含一堆内部元数据序列化出来会是一坨不可读的#S(BOOKMARK ...)。正确做法是手动提取字段组装成 plist 或 alist 再转 JSON。这是整个例子最容易踩的坑。新增书签(defroute (/api/bookmarks :method :post) () (let* ((params (lack.request:request-parameters *request*)) (title (cdr (assoc title params))) (url (cdr (assoc url params)))) (if (and title url) (let ((b (mito:create-dao bookmark :title title :url url))) (jonathan:to-json (:id ,(mito:object-id b) :title ,title :url ,url))) (error missing params))))这个例子没做优雅错误返回正式项目建议用setf (hunchentoot:return-code*) 400之类的写法。重点是看流程从*request*取参数、调create-dao建对象、手动序列化 JSON。3.3 启动、测试与打包启动服务(bookmark-api:start :port 5000)然后 curl 测curl http://127.0.0.1:5000/api/bookmarks curl -X POST -d titleexampleurlhttps://example.com http://127.0.0.1:5000/api/bookmarks如果能收到 JSON说明 Caveman 的路由、Clack 服务器、Mito 数据库整个链路通了。部署阶段SBCL 可以把程序直接编译成可执行文件sbcl --load bookmark-api.asd \ --eval (ql:quickload :bookmark-api) \ --eval (sb-ext:save-lisp-and-die \bookmark-api-bin\ :executable t :toplevel #bookmark-api:start)生成的二进制文件可以直接扔到服务器上跑再用 systemd 守护。整套流程走下来你会感觉 Common Lisp 做小服务是真能落地的。4. 常见问题与排查技巧实录4.1 问题速查表现象大概率原因处理办法ql:quickload卡住或报错网络问题/源太慢换 Quicklisp 镜像或重试模板渲染中文乱码文件保存编码不是 UTF-8统一 utf-8加meta charsetutf-8静态文件 404:static中间件没配/路径不对检查lack:builder里的:root数据库表不存在没执行ensure-tables-exist启动流程里显式调用改代码不生效REPL 缓存旧编译结果重新编译系统或重启 REPL端口被占用旧进程没杀死lsof -i :5000找到进程杀掉JSON 里有#S(OBJECT ...)直接序列化 DAO 对象手动提取 slot 再转 plist/alist4.2 开发体验与避坑心得第一条独家经验写一个reset函数把“停止、重新加载、启动”这三件事绑在一起。在 REPL 里开发时频繁重新编译系统有时状态会乱尤其数据库连接和端口监听这种全局资源。写个(reset)一键恢复干净状态能省掉大量“找不到符号”“服务已占用”的尴尬(defun reset () (bookmark-api:stop) (ql:quickload :bookmark-api :verbose nil) (bookmark-api:start :port 5000))第二条经验路径尽量用绝对路径。项目里模板目录、静态目录如果写相对路径取决于你在哪个工作目录启动 SBCL可能正常也可能 404。我最开始就是被这个坑了一个多小时明明代码没问题换了个终端就找不到模板。第三条经验调试中间件链从lack:builder入手。比如 Session 不生效、静态资源不路由问题八成在 builder 的中间件顺序上。想看清当前应用到底挂了多少层中间件可以打印*app*的结构别凭感觉猜。5. 从 Caveman 看 Lisp Web 生态Caveman 只是入口它背后是一整套能衔接起来的 Lisp Web 生态。服务器层有 Hunchentoot、Woo、QootClack 提供服务器抽象类似 Ruby 的 Rack、Python 的 WSGI模板有 Djula数据库有 Mito、Datafly、SxqlJSON 有 Jonathan 和 cl-json。我要说的是这套生态不是“缺东少西的实验品”而是真的能把一个 Web 服务从开发到部署全流程跑通的方案。但如果你的目标是搭一个大型商业项目我得泼盆冷水Caveman 生态最大瓶颈不是框架能力而是“会的人太少”。当你遇到一个冷门 bug查 Stack Overflow 大概率没有答案只能读源码。这是我的亲身体会某次 Session 中间件在反向代理下不工作最后翻 Lack 的源码才发现是 Cookie 路径问题。好在 Lisp 源码注释还算友好读起来不致命。所以我的选型建议很朴素个人项目、内部工具、产品原型、学习项目放心用 Caveman2团队项目、长期维护、多人协作先确认大家愿不愿意学 Lisp 再说。最后再分享一个能提升幸福感的小技巧把你的项目目录软链到~/quicklisp/local-projects/下之后只要(ql:quickload :bookmark-api)就能加载不用反复写 load 路径。Quicklisp 默认会扫描local-projects下的所有项目这个机制比手动维护路径省事太多。我实际用下来的体会是Caveman 真正吸引人的不是某个黑科技而是整个 Lisp 交互式开发带来的“改完即生效”的节奏。写路由、跑请求、看结果像聊天一样自然。如果你已经被改代码重启服务循环折磨了很久建议找个周末试试 Caveman 写个 100 行的 API说不定会打开新世界。