Clack — 给 Common Lisp 加一层标准化的 web 服务器接口
是什么
Clack 是 Eitaro Fukamachi 2011 年开源的 Common Lisp web 应用环境。日常类比:像电源插座的国标——不管你买的是苹果充电器还是华为充电器,墙上插孔都长一样。Clack 就是 Common Lisp 世界给 web 服务器和 web 应用之间约定的那个”国标插孔”。
它的灵感直接来自 Ruby 的 Rack 和 Python 的 WSGI:
(clack:clackup (lambda (env) (declare (ignore env)) '(200 (:content-type "text/plain") ("Hello, Clack!"))))一个 Clack 应用就是一个普通 lambda:吃 env(请求信息),吐 (状态码 头部 正文) 三元组。Hunchentoot / Woo / Wookie 各家服务器只要支持 Clack 协议,这个 lambda 就能不改一个字直接跑。
为什么重要
不理解 Clack,下面这些问题没法解释:
- 为什么 Common Lisp 写 web 应用,换服务器后端不用重写代码
- 为什么 Caveman2 / Ningle / Lucerne 这些”框架”看起来都长得很像——它们底下都是 Clack
- 为什么 Lisp 圈推荐”先选 Clack,再挑后端”,而不是直接选 Hunchentoot
- 为什么 sinatra / express 这些跨语言的微框架都隐含一个 server-app 解耦层
核心要点
Clack 的设计可以拆成 三块:
-
应用是函数:一个应用 =
(lambda (env) ...)。env是 plist(属性列表),里面装:request-method:path-info:query-string:headers等字段。返回值固定是(status headers body)三元组。 -
中间件是函数的函数:中间件 = 接收一个 app 返回新 app 的高阶函数。日常类比:俄罗斯套娃,每一层在请求进来时做一件事(记日志、加鉴权、压缩),传给下一层;响应出去时反向走一遍。
-
clackup 是粘合剂:
clack:clackup把 app 和后端服务器粘到一起。:server :woo切成 Woo,:server :hunchentoot切成 Hunchentoot,应用代码完全不动。
三块加起来叫 Clack 协议,2015 年前后被拆成更小的 Lack 核心 + Clack 启动器两层。
实践案例
案例 1:最小 Hello World
(ql:quickload :clack)
(defvar *handler* (clack:clackup (lambda (env) (declare (ignore env)) '(200 (:content-type "text/plain") ("Hello, Clack!")))))打开 http://localhost:5000 就能看到。停服务用 (clack:stop *handler*)。这个例子里没有路由、没有模板、什么都没有——这正是 Clack 的卖点:最小可运行 web 应用 5 行。
案例 2:用中间件搭日志 + 静态文件
(clack:clackup (lack:builder :accesslog (:static :path "/public/" :root #P"./static/") *app*))lack:builder 把多个中间件和最终 app 串起来。请求先过 :accesslog(记访问日志)→ :static(命中静态文件就直接返回)→ *app*(业务逻辑)。响应反向走一遍。
中间件本质就是函数包装:
(defun wrap-log (app) (lambda (env) (format t "~A ~A~%" (getf env :request-method) (getf env :path-info)) (funcall app env)))接收 app 返回新 app——这种”中间件栈”模式和 Express / Koa / Django 一模一样。
案例 3:换后端不改代码
;; 开发时用 Hunchentoot,调试方便(clack:clackup *app* :server :hunchentoot :port 8080)
;; 上线时换 Woo(基于 libev,吞吐高 5-10 倍)(clack:clackup *app* :server :woo :port 80 :address "0.0.0.0")应用代码 *app* 零改动。这就是『标准化插孔』的实际收益——开发用方便的,上线用快的。
后端选择速查:
- Hunchentoot:CL 圈最老牌,文档全,单线程模型简单,适合学习和中等流量
- Woo:Fukamachi 自己写的,基于 libev 异步,吞吐高 5-10 倍,生产首选
- Wookie / Toot:相对小众,特殊场景才考虑
踩过的坑
-
body 必须是 list / pathname / stream:返回
"Hello"(字符串)会被当成字符 list 输出乱码。要写("Hello")或(list "Hello")。 -
environment 是 plist 不是 hash-table:取值用
(getf env :request-method),写成gethash直接挂掉。这点对从 Python WSGI 转过来的人最反直觉。 -
中间件顺序栈式生效:
:builder里写在最外面的中间件最先处理请求、最后处理响应。鉴权要写在静态文件前面(外面),否则静态文件会绕过鉴权。 -
clackup 默认阻塞 REPL:直接调用会卡住交互式开发。要么传
:use-thread t,要么用bordeaux-threads:make-thread丢到独立线程,否则你以为服务起来了实际上 REPL 死锁。
适用 vs 不适用场景
适用:
- Common Lisp 写 web 后端、API 服务、内网工具
- 想要”先开发再优化”——开发用 Hunchentoot,上线切 Woo
- 学 Common Lisp 生态的入口——Caveman2 / Ningle / Mito 都建在它上面
不适用:
- 不写 Common Lisp 的人——这是 CL 专属基建,跨语言学价值有限
- 需要完整 MVC 框架的项目——Clack 只是协议层,要 ORM / 模板 / 路由得叠 Caveman2
- 实时双向通信为主的应用——WebSocket 支持靠后端实现差异大,不如直接选 Woo + websocket-driver
历史小故事(可跳过)
- 2011 年:Eitaro Fukamachi 看到 Ruby Rack 把 web server 抽象掉的好处,决定给 Common Lisp 做一份,命名 Clack(“Common Lisp Rack”的缩写)。
- 2013-2014 年:Clack v1 时代——把所有功能(中间件、构建器、应用基类)塞在一个包里,包很大。
- 2015 年前后:重构出 Lack 作为更小的核心协议层,Clack 退化为”启动器 + 兼容层”。一个项目通常依赖两个名字。
- 2017 年至今:Caveman2、Ningle、Lucerne 这些 CL web 框架默认依赖 Clack/Lack;Woo(Fukamachi 自己写的高性能后端)成为生产首选。
学到什么
- 协议优先于实现:先定义 server↔app 接口,再让 N 家服务器都来实现。这是 Rack / WSGI / Clack / koa 的共同模式。
- 应用即函数 + 中间件即高阶函数:函数式语言天生适合做这套抽象,CL 用 lambda 比 Ruby 用 Proc 更直接。
- 小核心 + 周边生态:Clack/Lack 自己很小(几千行),所有”框架感”由 Caveman2 等上层提供。这是 Unix 风格的 web 抽象设计。
- 类比迁移:理解了 Rack/WSGI 之后再看 Clack,五分钟就能上手——好的设计模式跨语言通用。
延伸阅读
- 文档站:Clack official docs —— 含中间件清单和后端对比表
- 作者博客:Fukamachi 的 Lisp 系列 —— Clack/Woo/Caveman2 设计动机
- 同款项目:sinatra —— Ruby 的微 web 框架,理念与 Clack 上层 Ningle 一致
- 同款抽象:express —— Node.js 的 Connect/Express 也是「app + 中间件栈」
- Python 对照:flask —— WSGI app + 装饰器路由的最小组合
关联
- sinatra —— Ruby 的 Sinatra 是 Ningle/Lucerne 的灵感,Clack 是它们的底层
- express —— Express 的
app.use(mw)等价于lack:builder - koa —— Koa 的洋葱模型中间件,和 Clack 中间件栈是同一个概念
- flask —— Flask 跑在 WSGI 上,Clack 跑在自己的协议上,地位一致
- django —— Django 也是 WSGI app,Clack 等价物在 CL 圈是 Caveman2
- fastapi —— FastAPI 走 ASGI(异步版 WSGI),Clack 暂没等价异步协议
- hanami —— Hanami 是 Ruby 框架,底下还是 Rack——和 Caveman2/Clack 同构
反向链接
- boxen —— boxen — 给终端文本套个边框的事
- django —— Django — 全功能 batteries-included 的 Python web 框架
- enquirer —— enquirer — 让 CLI 工具会问问题的轻量库
- express —— Express — Node.js 最经典的 Web 框架
- fastapi —— FastAPI — 用 Python 类型注解写 API
- flask —— Flask — 用装饰器把 URL 接到函数上的 Python 微框架
- hanami —— Hanami — Ruby 里既不是 Rails 也不是 Sinatra 的第三选择
- koa —— Koa — async/await + ctx 对象 + 洋葱模型 的极简 Node.js web 框架
- ora —— ora — 终端 spinner 用 ANSI 反复擦写同一行
- sinatra —— Sinatra — 用 Ruby 三行代码起一个 web 服务
- textual —— Textual — 用 CSS 写终端界面的 Python 框架
- yargs —— yargs — Node.js 命令行参数解析的事实标准