Skip to content

函数即服务(FaaS)友好:单个 fetch handler,天然契合 Serverless

Deno.serve(app.fetch),无需进程管理

在传统 Web 开发中,一个应用通常被理解为一个“持续运行的进程”:Node.js 启动一个 HTTP 服务器,监听某个端口,接收 TCP 连接,循环处理请求。这种模型隐含了对操作系统进程的直接控制,开发者需关注进程生命周期、端口绑定、信号处理(如 SIGTERM)、集群管理等问题。

而函数即服务(FaaS)彻底改变了这一范式。在 Serverless 架构下,应用不再是长期运行的进程,而是由事件触发的、短暂执行的函数实例。每次请求到来时,平台动态实例化函数,执行逻辑,返回响应,随后可能立即销毁实例。这种模型解耦了代码与基础设施,开发者不再关心服务器运维,但同时也要求代码必须符合特定的执行契约。

Hono 的设计完全围绕这一契约构建:它不试图模拟一个服务器进程,而是直接输出一个符合 FaaS 规范的请求处理器(handler)


一、Hono 的核心输出:app.fetch 是一个标准 fetch 处理器

Hono 应用的核心是一个 fetch 方法:

ts
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => c.text('Hello'))

export default app.fetch

app.fetch 的类型签名是:

ts
(request: Request, env: Env, ctx: ExecutionContext) => Response | Promise<Response>

这正是 Cloudflare Workers、Deno Deploy、Vercel Edge Functions 等平台所期望的 fetch 事件处理器。它不启动服务器,不监听端口,不维护连接池——它只是一个纯函数,接收请求,返回响应。

这种设计使得 Hono 与 FaaS 平台之间没有适配层。无需 @netlify/functionsvercel/node 这类运行时包装器来桥接 Express 与 Serverless 环境,因为 Hono 本身就是为这种环境原生编写的。


二、以 Deno 为例:Deno.serve(app.fetch) 的意义

在 Deno 环境中,我们常看到如下代码:

ts
Deno.serve(app.fetch)

这行代码的含义需要拆解:

  • app.fetch 是一个函数,它能处理单个 HTTP 请求。
  • Deno.serve 是 Deno 运行时提供的一个顶层 API,它接收一个 fetch 处理器,并将其绑定到 HTTP 服务器上。

关键在于:Deno.serve 不是 Hono 的一部分,而是 Deno 运行时对 FaaS 模型的实现。它负责:

  • 接收网络请求,将其封装为 Request 对象;
  • 调用 app.fetch(request, env, ctx)
  • 将返回的 Response 对象发送回客户端。

Hono 完全不参与服务器的启动、端口绑定、TLS 终止等底层操作。它只负责“请求进来后该做什么”。

这种职责分离带来了显著优势:

  1. 可移植性:同一份 app.fetch 可以在 Deno、Cloudflare Workers、Vercel、Bun 等不同运行时中使用,只需改变外部的 serve 调用方式。
  2. 测试友好:你可以直接调用 app.fetch(new Request('http://localhost/')) 来测试路由逻辑,无需启动真实服务器。
  3. 无状态设计:由于 Hono 不维护任何全局状态(如连接池、定时器),它天然适合在无状态的 FaaS 环境中运行。

三、Serverless 的核心约束与 Hono 的应对

FaaS 环境对函数执行有严格限制,Hono 的设计直接回应了这些约束:

约束Hono 的应对
执行时间有限(如 50ms ~ 10s)避免阻塞操作,所有 I/O 为异步,中间件链基于 Promise 组合,确保非阻塞执行。
无持久进程不依赖进程内缓存、单例对象或长连接。所有状态通过外部存储(KV、数据库)管理。
冷启动敏感核心极小(~1KB),无复杂初始化逻辑,app.fetch 是轻量闭包,快速实例化。
资源隔离不访问文件系统、不执行子进程、不绑定端口,完全依赖输入(Request)和环境(env)运行。
事件驱动fetch 事件是主要入口,Hono 专注于处理该事件,不引入其他事件循环管理。

例如,在 Cloudflare Workers 中,函数执行受 CPU 时间限制。Hono 的 Radix Tree 路由在 O(log n) 时间内完成匹配,避免了线性遍历或正则回溯,确保路由查找不会成为性能瓶颈。


四、与传统框架的对比:Express 的“适配”困境

Express 的典型用法是:

ts
const app = express()
app.get('/', (req, res) => res.send('Hello'))
app.listen(3000)

要在 Serverless 环境中运行这段代码,必须通过适配器(如 @vendia/serverless-express)将其包装成一个 handler 函数:

ts
exports.handler = serverless(app)

这个适配过程本质上是:

  1. 启动一个隐藏的 Express 服务器;
  2. 将 Serverless 请求转发给该服务器;
  3. 捕获响应并转换为平台兼容格式。

这引入了额外的抽象层、内存开销和潜在的性能损耗。更重要的是,Express 的中间件可能依赖 req.connectionres.writeHead 等 Node.js 特有属性,在适配层中需模拟或降级处理,增加了不确定性和调试难度。

而 Hono 从一开始就运行在“适配后”的状态,无需转换。


五、app.fetch 的组合性与中间件链

尽管 app.fetch 是一个单一函数,Hono 通过中间件机制实现了逻辑的模块化组合:

ts
app.use('*', logger())
app.use('/api/*', cors())
app.get('/user/:id', validateUser, getUser)

这些中间件在 app.fetch 内部形成一个执行链,但最终仍输出一个单一的 fetch 处理器。这种“组合后聚合”的模型完美契合 FaaS 的单入口要求:平台只关心“如何处理请求”,而 Hono 负责将复杂的逻辑封装在这个单一接口之下。


六、结论:Hono 的“Serverless 原生性”

Deno.serve(app.fetch) 这一行代码,象征着 Hono 与 Serverless 架构的深度契合:

  • 它不模拟服务器,而是接受请求
  • 它不管理进程,而是响应事件
  • 它不依赖环境,而是利用环境(通过 c.env 访问平台绑定资源)。

这种设计使得 Hono 成为真正的“函数即服务友好”框架:它的最小执行单元就是一次请求处理,没有多余的抽象,没有隐藏的进程,没有复杂的生命周期管理。

在后续章节中,我们将看到,这种单一 fetch handler 如何与类型系统、中间件链、边缘存储等能力结合,在极简的基础上构建出复杂而高效的应用架构。