传统 Web 框架往往将开发者绑定在固定的架构模式上:路由、控制器、模型、视图层层堆叠,一旦需要调整,牵一发而动全身。启动时间随代码量增长而膨胀,热重载要么不可靠,要么需要复杂的配置。Biff 作为 Clojure 生态中的全栈框架,选择了一条不同的路径 —— 通过函数式系统组合(system composition),将应用拆分为可独立演化、可热替换的模块化组件。
系统组合的核心思想
Biff 的设计哲学可以用一句话概括:"强默认,弱绑定"(strong defaults, weakly held)。框架提供开箱即用的完整技术栈整合 ——XTDB 作为不可变数据库、htmx 实现服务端驱动的交互 UI、Malli 负责 schema 验证、_hyperscript 处理轻量级客户端逻辑 —— 但这些组件并非铁板一块。每个子系统都通过纯数据结构和协议接口定义,开发者可以随时替换或移除不需要的部分。
这种架构的关键在于组件生命周期管理。Biff 将数据库连接、缓存、消息队列、Web 服务器等基础设施抽象为带有 start 和 stop 方法的组件。系统启动时,按照依赖顺序初始化各个组件;关闭时,逆序优雅地释放资源。这种方式借鉴了 Erlang/OTP 的监督树思想,但用 Clojure 的不可变数据结构和函数式风格重新表达。
实践:定义可组合的组件
在 Biff 项目中,系统组件通常定义在 src/system/ 目录下。一个典型的组件实现如下:
(defrecord Database [config conn]
component/Lifecycle
(start [this]
(let [conn (xt/start-node config)]
(assoc this :conn conn)))
(stop [this]
(when conn (.close conn))
(assoc this :conn nil)))
组件之间通过依赖注入连接。系统映射(system map)是一个普通的 Clojure 映射,键为组件名称,值为组件实例。启动时,Biff 使用 component 库或类似的机制,根据声明的依赖关系自动排序初始化顺序。这种方式的好处是测试时可以用内存数据库或 mock 实现替换真实组件,无需修改业务代码。
REPL 驱动与热重载
Biff 最显著的特性是文件保存即热重载。开发模式下,框架监视源文件变化,自动重新加载修改的命名空间,而无需重启整个应用。这意味着你可以在应用运行时连接到 REPL,实时修改路由处理器、数据库查询或 UI 模板,立即看到效果。
更进一步,Biff 支持生产环境 REPL 连接。在部署后的服务器上,你可以通过 SSH 隧道连接到运行中的应用,检查状态、修复 bug、甚至添加新功能,而无需停机部署。这种模式将 Clojure "代码即数据" 的理念延伸到运维层面,大幅缩短了从发现问题到修复上线的周期。
数据驱动的路由与副作用隔离
Biff 的路由定义是纯数据结构,而非注解或宏魔法:
(def routes
[["/api/users"
{:get {:handler list-users
:auth :required}
:post {:handler create-user
:schema [:map [:email :string]]}}]])
路由表在启动时转换为 Ring 处理器,中间件(参数解析、认证、日志)通过函数组合层层包裹。业务逻辑写在纯函数中,接收请求映射,返回响应映射,所有副作用(数据库写入、邮件发送、HTTP 调用)通过协议抽象到适配器层。
这种端口 - 适配器(ports and adapters)模式带来清晰的边界:核心领域逻辑不依赖任何框架或基础设施,只操作领域模型和协议接口。适配器层负责将外部系统(数据库、邮件服务、支付网关)映射到这些协议。更换数据库或添加缓存时,只需实现新的适配器,核心业务代码保持不变。
可落地的项目结构与配置
一个典型的 Biff 项目结构如下:
src/
├── core/ ; 纯领域逻辑,无副作用
│ ├── user.clj
│ └── order.clj
├── ports/ ; 协议定义
│ ├── persistence.clj
│ └── email.clj
├── adapters/ ; 实现细节
│ ├── db/
│ └── smtp/
├── web/ ; HTTP 层
│ ├── routes.clj
│ └── middleware.clj
└── system/ ; 组件组合
├── init.clj
└── dev.clj
配置文件使用 EDN 格式,环境特定的设置(数据库 URL、API 密钥)通过环境变量或外部配置映射注入:
{:database {:xtdb/type :jdbc
:jdbc/url #env "DATABASE_URL"}
:email {:provider :sendgrid
:api-key #env "SENDGRID_KEY"}}
权衡与适用场景
函数式系统组合并非银弹。它需要团队理解不可变数据、函数式编程和依赖注入模式,学习曲线比传统 MVC 框架更陡峭。对于简单 CRUD 应用,这种架构可能显得过度设计。
但在以下场景,Biff 的系统组合模式展现出明显优势:
- 长期演化的业务系统:清晰的组件边界使代码库在增长过程中保持可维护性
- 高频迭代的原型开发:REPL 驱动和热重载将反馈循环缩短到秒级
- 需要灵活替换基础设施的项目:端口 - 适配器模式让数据库、消息队列的迁移变得可控
- 小团队全栈开发:htmx + 服务端渲染减少前后端协调成本,一人可覆盖完整功能
Biff 的部署选项也体现了实用主义:既提供 Ubuntu VPS 的一键部署脚本,也支持 Uberjar + Docker 的标准化流程。这种灵活性让团队可以根据运维成熟度选择合适的发布策略。
监控与运维要点
在生产环境使用 REPL 连接时,建议配置以下监控点:
- REPL 会话审计:记录所有通过 REPL 执行的代码,便于追溯变更
- 组件健康检查:为每个子系统暴露
/health端点,监控数据库连接、消息队列状态 - 优雅关闭超时:设置组件停止的最大等待时间(建议 30 秒),避免强制终止导致数据不一致
- 内存与 GC 监控:Clojure 的不可变数据结构可能产生临时对象,关注年轻代 GC 频率
资料来源
- Biff 官方文档:https://biffweb.com
- Hacker News 讨论:https://news.ycombinator.com(Biff.core: system composition for Clojure web apps)
内容声明:本文无广告投放、无付费植入。
如有事实性问题,欢迎发送勘误至 i@hotdrydog.com。