首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
用 Common Lisp 的 Caveman 框架构建极简 Web 应用:从路由到 REPL 热加载
📅 2026/10/29 12:53:28
✍️ 爱科研究院
👁 阅读 3,247
1. 项目代号 caveman它为什么能靠极简留住我去年年底我接到一个很没存在感的需求给团队做一个内部构建看板。页面不超过五个数据库表不超过三张核心是展示流水线状态、点开看日志、偶尔触发一个 webhook。这种活儿我最早会打开 Express后来换 Flask再后来看到模板里堆满了框架自动生成的目录我总有种错觉——我是在写业务还是在给框架打工。这时候有人把 Common Lisp 的 Caveman 甩到我面前。第一眼看到这个名字我笑了这框架的态度太直白连装都不装一下。Caveman 就是原始人的意思设计的初衷也简单到令人舒适——它只帮你把 Web 应用最容易被重复的部分搭好HTTP 服务的启动、路由的分发、模板目录和静态文件的挂载。至于业务逻辑该怎么组织、数据库选哪个、参数校验用不用库它统统不表态。这种不表态恰恰是我需要的。Caveman 的底层是 Clack默认的 HTTP 后端是 Hunchentoot路由用的是一层叫 easy-routes 的薄封装模板交给 Djula。听起来组件不少但它们每一个都负责一件极小的事没有任何一个层要你理解容器生命周期或依赖注入上下文。我用 Spring 写过一个内部系统整天被你 Bean 注入对了吗这种问题折磨。换成 Caveman 之后打开源码的感觉就像走进一间只有一张桌子、一把石斧的房间——你不会迷路也不需要学一堆仪式才能开始干活。有人可能会问为什么不用 Flask我试过Flask 本身也轻但轻到一定程度之后反而剩下你自己决定的余地太多。Caveman 的特色是它给你一个固定的项目骨架却不给你固定的业务答案。它定义好了路径、端口、模板渲染这些 Web 框架的基础设施剩下的每一个决策都留给你用 Lisp 本身的能力去解决。实际体验下来这种组合特别适合内部工具你能快速跑起来又不会被框架的全家桶式哲学推着走。1.1 这个框架到底解决了什么问题直接说它解决的问题绕不开三件事。第一件事是启动一个 Web 服务的成本。Caveman 有一个命令行工具跑一下就能生成一个完整的项目骨架里面连.asd系统定义都写好了你直接ql:quickload就能在 REPL 里把它跑起来。第二件事是路由定义。在 Caveman 里一个路由就是一个defroute宏参数映射、HTTP method、返回内容全部挤在一个函数式声明里和控制反转容器那套东西完全是两个世界。第三件事是模板。Caveman 默认接上 Djula它的语法和 Django 模板几乎是一模一样的血脉如果你之前写过 Jinja2基本不需要学习成本。1.2 为什么没有官方数据层反而救了我Caveman 不捆绑 ORM 这件事我一开始以为是它生态不成熟用完之后才意识到这是克制。我需要的数据层用cl-dbi直接连 SQLite 跑几条 SQL 就够了后来要建两张有关联的表我再引入了 Mito。如果你在 Spring 里想换数据层哪怕只是改一个连接池配置都有几十个 XML 和注解等着你。但在 Caveman 里数据层只是你在.asd文件里加一条依赖、在自己的代码里调用的问题。这种选择权不会被框架的代码风格污染因为你写的本来就是 Lisp 表达式不是被框架抽象过的半语言。我后来复盘Caveman 这个名字真正的意思是它不是石器时代而是够用就好。你不需要为还没发生的高并发准备一堆抽象不需要为一个内部工具引入服务发现。你只需要一把斧头把眼前这块木头劈开。2. 从头搭建caveman 命令、目录结构和 REPL 启动如果你现在就想试下面这段完全来自我本机的操作记录。我用的环境是 macOS RoswellCommon Lisp 实现是 SBCLQuicklisp 是最新版。不同版本之间可能有细微差别但主流程到目前为止很稳定。2.1 环境准备先装 Roswell 和 QuicklispCommon Lisp 的环境管理比很多人想象中简单但也确实有一点门槛。我建议直接用 Roswell它同时负责 SBCL 的安装、Quicklisp 的管理和命令行脚本的运行。装上之后基本命令是brew install roswell # macOS # 或者 Debian/Ubuntu 上可以用 apt install roswell ros setup然后在 REPL 里把 Quicklisp 拉起来再ql:quickload一下 Caveman 本体(ql:quickload :caveman)如果你用的编辑器是 Emacs 或者 VSCode建议顺手把 Slime 或 Sly 配上。Caveman 这种框架最大的爽点全在 REPL 里等会你可以边改代码边热加载不需要一遍遍重启进程。没配 REPL 也不是不能写只是你体会不到它一半的快乐。2.2 生成的骨架里每一块都在干什么环境就绪后终端敲caveman my-dashboard我用过几次生成的目录结构大致是下面这个样子my-dashboard/ ├── my-dashboard.asd ├── src/ │ └── my-dashboard.lisp ├── templates/ │ ├── index.html │ └── layout.html ├── static/ │ ├── css/ │ └── js/ └── test/第一次看到这个结构说实话我有点不习惯因为目录少得不像一个工程。但仔细看每一块都有明确职责.asd文件是 ASDF 的软件包定义相当于写「这个项目叫什么、依赖哪些库、包含哪些源文件」的清单src/放你的业务代码templates/放 Djula 模板static/放 CSS、JS、图片这些浏览器直接拉的文件。.asd文件通常长这样简化后逻辑很清楚(defsystem my-dashboard :description 内部构建看板 :version 0.1 :depends-on (caveman djula) :components ((:module src :serial t :components ((:file my-dashboard)))))第一次看到这个文件如果你习惯了 Maven 的pom.xml或者package.json可能会觉得字段太少。但少不是缺点因为这个系统定义本来就不需要描述构建插件、资源过滤、环境 profiles 这种事儿。你在这个文件里加依赖在src/my-dashboard.lisp里写路由和业务运行时要加数据库就再往:depends-on里塞一个cl-dbi。2.3 REPL 里改代码不用重启服务启动服务的体验也和传统方式完全不同。在 REPL 里(ql:quickload :my-dashboard) (my-dashboard:start-app :port 5000)终端出现 Hunchentoot 的启动日志然后浏览器打开http://localhost:5000就能看到页面。之后你觉得某个接口返回结构不对直接在 REPL 里重新定义那个defroute的函数体再刷新浏览器。是的连服务都不用重启。那种敲完代码立刻生效的节奏比我之前在 Node 里折腾nodemon要顺滑得多因为这不是靠外部工具监听文件变更而是 Lisp 环境本身就具备重新定义函数的能力。3. 把业务塞进骨架路由、模板、数据库三根骨头骨架有了接下来就是往里面填肉。我习惯把 Caveman 的日常开发拆成三根骨头路由负责接收请求模板负责渲染页面数据库负责存状态。这三块都各自独立甚至在写的时候你可以先写一块剩下两块拿假数据顶住。3.1 路由defroute 用起来像在写遗嘱短但明确Caveman 的路由写法是(defroute (/ping :method :get) () pong)翻译成人话就是当有人用 GET 请求访问/ping返回字符串pong。就这么直接。带路径参数的路由也不绕弯(defroute (/users/:id :method :get) (id) (format nil 用户ID: ~a id)):id会被 easy-routes 自动从 URL 里解析出来然后作为同名参数传进函数。我第一次用的时候最直观的感受是这里没有拦截器、路由守卫、注解扫描这一层层的抽象路由就是一个函数函数返回什么页面就显示什么。如果你要返回 JSON也很简单把响应头设置一下就行(defroute /api/builds () (setf (hunchentoot:content-type*) application/json; charsetutf-8) (jonathan:to-json (my-dashboard.core:latest-builds)))这里我用的是jonathan这个 JSON 库。你看Caveman 自己不提供 JSON 序列化器但你可以在任何路由里想用哪个就用哪个框架完全不干涉。这种自由度在最开始会让人有点不知所措但用顺手了你会上瘾因为没有任何一个中间层替你做决定你也不需要去猜框架帮你做了什么。3.2 模板Djula 让我把 HTML 和逻辑干净分开Caveman 默认把模板渲染的活交给 Djula。Djula 的语法对写过 Django/Jinja2 的人来说几乎不需要心理建设!-- templates/layout.html -- html head title{% block title %}默认标题{% endblock %}/title /head body {% block content %}{% endblock %} /body /html!-- templates/index.html -- {% extends layout.html %} {% block title %}构建看板{% endblock %} {% block content %} ul {% for build in builds %} li{{ build.name }} —— {{ build.status }}/li {% endfor %} /ul {% endblock %}在 Caveman 的路由里渲染模板大致是(defroute / () (render index.html (list :builds (my-dashboard.core:all-builds))))render把模板文件和一个 plist 组合起来模板里的builds就能遍历了。这个设计足够朴素以至于我几乎没有想过模板语法能不能更强大——它越是克制越不容易在模板里写出一坨又一坨的业务逻辑。后来我每次遇到复杂的表格和筛选条件都会在路由函数里先算好数据再把已经整理好的结果塞给模板。这个习惯很大程度上是 Djula 的简单性帮我养成的。3.3 数据层Mito 与 cl-dbi 的选择建议数据库这块我前后试过两种方案。项目刚起步时我用cl-dbi直接写 SQL因为表只有几张(dbi:connect :sqlite3 :database-name #Pdashboard.db)然后自己拼 SQL、取结果、组装成列表。对两个表来说这是最快最短的路径而且你能清楚看到数据库查询发生了什么不会有 ORM 帮你偷偷懒再偷偷埋雷。后来表多起来、关联变复杂我引入了同一个作者写的 Mito。Mito 的思路是让你用 Common Lisp 的defclass定义表(mito:deftable build () ((name :col-type (:varchar 128)) (status :col-type (:varchar 16)) (started-at :col-type (:timestamp))))定义完类之后mito:select-dao这些函数会帮你把行转成对象省去手写映射的重复劳动。我的建议是如果你只是存几个配置项、展示几条记录直接cl-dbi手写 SQL一旦出现外键关联、一对多关系、多个查询条件组合就切到 Mito。两个库的切换成本在 Caveman 里低得可怕因为它们都不是 Caveman 强制绑定的组件你只是在.asd里加一行依赖、在自己的代码里换一个包名。3.4 组装一个小接口用真实的前后端跑一遍我们把上面三块拼起来一个真实的接口大概长这样(defroute (/api/builds/:name :method :get) (name) (let ((build (my-dashboard.core:find-build name))) (if build (progn (setf (hunchentoot:content-type*) application/json; charsetutf-8) (jonathan:to-json build)) (progn (setf (hunchentoot:return-code*) hunchentoot:http-not-found) \not found\))))这段代码本身的逻辑很简单查数据、有就返回 JSON、没有就 404。难能可贵的是从 HTTP 请求进来到响应返回所有步骤都是我能在十秒钟内讲清楚的。没有框架自动帮你做的黑魔法也没有从控制器返回的字符串到用户看到的页面之间需要理解的隐晦转换。这就是 Caveman 的核心价值它把 Web 开发还原成了一门你可以完整掌握的技能。4. 上线三天我踩到的三个 Caveman 相关坑再轻量的框架也不会让你完全避开坑。我那个看板上线三天前前后后碰到三个问题每一个都不是特别难解决但如果你不了解底下的机制排查起来可能要绕弯路。4.1 多线程共享状态的失控Caveman 默认跑在 Hunchentoot 上而 Hunchentoot 默认是多线程的。每个请求到达时可能由不同的线程处理。这本身没什么问题问题出在我一开始图省事在一个全局变量里存了最后一次构建的状态(defvar *last-build* nil)测试的时候一切正常因为我是单线程一个个手工发请求一接入团队的真实流水线多个 webhook 同时打进来的瞬间*last-build*的内容就乱了。一会儿显示 A 项目的状态一会儿显示 B 项目的状态看似随机其实是并发写同一块全局内存导致的。解决方式也符合 Lisp 的直白风格要么给修改*last-build*的代码加上锁要么干脆别用全局变量把状态放到数据库里每次请求都从数据库读。我用的是第二种方式因为看板场景本身就要落库。这个坑的教训是别因为框架轻量就忽略并发问题——轻量的是框架不是你服务的并发压力。4.2 静态文件 404先怀疑反向代理再怀疑框架看板界面需要一点 CSS我把样式丢进static/css/app.css本地跑得好好的部署到服务器上之后所有样式全部 404。一开始我怀疑是 Caveman 静态文件目录配置错了后来发现根本不是。我的部署方式是 Nginx 反代到 Hunchentoot。Nginx 配置里我没有把/static/这个路径单独指到项目目录结果请求/static/css/app.css时Nginx 把它原样代理给了 Hunchentoot而 Caveman 察觉到static/是静态目录也会试着从自己的静态目录里找。两边看起来都没错但实际效果就是要么 Nginx 返回 404要么 Caveman 返回 404。最后的配置很简单在 Nginx 里显式托底location /static/ { alias /home/app/my-dashboard/static/; expires 7d; } location / { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }如果你也遇到静态资源全挂我建议先看这一层别一上来就怀疑框架本身。4.3 后台运行与开机自启的常见部署误区Common Lisp 服务最常见的部署方式是用 Roswell 启动一个脚本把服务跑起来。如果直接用sbcl加载源码你得到的通常是一个前台进程SSH 一断开服务就没了。有人会用nohup把进程放到后台但服务器重启之后还得手动拉起。我最后用 systemd 写了个最小单元文件[Unit] Descriptionmy-dashboard Afternetwork.target [Service] Typesimple Userapp WorkingDirectory/home/app/my-dashboard ExecStart/usr/local/bin/ros -s my-dashboard -e (my-dashboard:start-app :port 5000) Restartalways [Install] WantedBymulti-user.target这样服务崩溃了能自动拉起开机也会自动启动日志交给journalctl -u my-dashboard查看。说白了这一节跟 Caveman 本身的关系不大但它是我从本地跑通到线上能用之间最绕的一段路。写在这儿的理由是轻量框架往往让你忽略了服务到底怎么活着这件事而部署运维恰恰是没有任何框架能替你做好的部分。5. 什么时候该选 caveman什么时候该果断放弃我不打算把 Caveman 吹成万能灵药。事实上我在这个项目结束之后认真想过下一次遇到什么场景我还会用它什么场景我绝对不会用它想清楚了之后你才不会因为一个框架看着很酷就把它用在错误的地方。5.1 它的能力边界在哪里Caveman 最适合的是内部工具、原型验证、低并发的后端服务以及那些你希望完全掌控每一行代码的场景。它不适合的也很明确如果你的团队没人写过 Common Lisp不要为了用 Caveman 而用 Caveman如果你的业务需要大量现成组件比如完整的管理后台、聊天系统、支付流程也不要指望 Caveman 提供现成的。还有一点要老实说Common Lisp 在 Web 生态上的周边配套远不如 Node 和 Python 丰富。你会有时候惊讶于原来这个库还活着也会因为某个老库在 Quicklisp 里不可用而不得不自己造轮子。框架本身极简意味着你要有自己动手的底气这种底气不是每个人一开始都有的。5.2 和 Flask、Sinatra、Express 的横向对比我手头这几个框架都写过不少把它们放在一起对比更直观维度CavemanFlaskSinatraExpress框架定位极简骨架微框架微框架轻量框架语言生态Common LispPythonRubyJavaScript路由定义defroute 宏装饰器DSL函数中间件默认模板DjulaJinja2ERB无官方默认数据层不绑定不绑定不绑定不绑定开发模式REPL 热加载flake8debug mode自动重载nodemon上手门槛需要 Lisp 基础极低低低我个人的感受Flask 和 Express 的上手门槛确实最低社区资料也最多适合绝大多数团队。Caveman 的优势不在于更容易而在于更透明。当你和 Lisp 已经很熟的时候你在 Caveman 里写的每一行代码都能在脑子里完整映射到 HTTP 协议和服务器的处理流程上。这种透明感带来的掌控力是追求框架完备性的人很难体会的。5.3 如果我要继续用下去下一步玩法看板上线稳定运行之后我又在 Caveman 里加了两个小东西一个是从数据库导出报表的定时任务另一个是接收外部 webhook 的入口。这两个功能本身很简单但让我看到 Caveman 的一个特点——它可以非常自然地嵌入到一个更大的 Lisp 程序里而不需要把一个 Web 框架当作整个应用的边界。下一次我可能会把 Caveman 和一个后台 worker 放在同一个 Lisp 镜像里让 Web 请求和异步任务共享同一套业务函数。这种框架只是应用的一个侧面的体验跟我以前整个应用长在框架里的体验是完全相反的。它让我重新理解了框架各自的位置Caveman 只管接收请求、返回响应而你的程序逻辑仍然属于你自己。如果这篇东西能给你留下一个画面我希望是一个原始人蹲在篝火旁用石头敲出一把顺手的工具然后继续做真正想做的事。工具就应该是这个样子——放在手边、没有废话、坏了能修而不是一个需要你持续供养的庞然大物。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/29 12:53:28
SAP基金管理:承诺预算与支付预算的机制解析与配置避坑
2026/10/29 12:53:28
MySQL语句执行全流程解析与性能调优实战
2026/10/29 12:48:27
深色模式适配工程化:色彩令牌、三端落地与回归测试
2026/10/29 13:43:37
PythonDOA入门:六个脚本打通窄带信号与CNN训练
2026/10/29 13:43:37
LoRA微调ViT模型在医疗影像诊断中的高效应用
2026/10/29 13:43:37
STC15硬件PWM实战:寄存器配置、频率与占空比计算避坑
2026/10/29 13:43:37
普通刷新与强制刷新:HTTP缓存、Service Worker与CDN
2026/10/29 13:43:37
企业级CMDB落地:CI建模、自动采集与数据治理
2026/10/29 13:38:37
中望3D2026核心功能与工程应用深度解析
2026/10/29 0:00:09
服务器巡检报告模板怎么写:从检查项到PowerShell数据回填
2026/10/29 0:00:09
Zed编辑器Split Diff功能深度评测与优化指南
2026/10/29 0:00:09
OpenAI新模型定价解析:基础档便宜八成,快档贵六倍值不值?
2026/10/28 12:27:10
全国计算机二级C语言真题PDF高效刷题指南
2026/10/28 15:37:16
requests+BeautifulSoup:爬虫抓取h1标题的入门实战指南
2026/10/28 12:37:46
递推最小二乘参数辨识MATLAB编程:二阶系统与渐消记忆实现
2026/10/28 15:37:16
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/28 15:37:15
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/28 15:37:15
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)