将 Express 应用迁移到 Hono 的实用建议
如果你正在维护一个 Express API,并考虑迁移到 Hono,首先需要明白的是:这并不是一次简单的查找替换式迁移。Express 构建于 Node 的 http 模块及其自有的 req/res 对象之上,而 Hono 则建立在 Fetch API 和 Web 标准之上。这一根本差异影响着一切:路由、中间件、请求处理以及响应。
在开始之前,你需要了解以下内容。
核心要点
- Express 与 Hono 的底层基础截然不同:前者基于 Node 的
http模块,后者基于 Fetch API 与 Web 标准。 - Hono 将 Express 中独立的
req与res对象统一为一个上下文对象c,且处理函数必须返回一个Response。 - 中间件签名也不同——Express 使用
(req, res, next),而 Hono 使用(c, next),并需要await next()。 - 请求体解析直接在 Hono 处理函数内部完成,而非通过全局中间件。
- 从无状态的 JSON 路由开始的增量式迁移,远比一次性重写要安全得多。
先理解架构层面的差异
Express 封装了 Node 的 IncomingMessage 与 ServerResponse,因此每一个 req 和 res 对象都是 Node 专有的。相比之下,Hono 使用标准的 Request 和 Response 对象——与你在浏览器或 Cloudflare Worker 中使用的对象完全一致。
当在 Node.js 上运行 Hono 时,需要使用 @hono/node-server 适配器来弥合差异。但处理函数本身保持运行时无关性。
// Express
app.get('/users/:id', async (req, res) => {
const user = await db.findById(req.params.id)
res.json(user)
})
// Hono
app.get('/users/:id', async (c) => {
const user = await db.findById(c.req.param('id'))
return c.json(user)
})
形态相似,但 c(上下文对象)取代了 req 和 res。你返回的是一个 Response,而不是调用 res 上的方法。
将 Node.js API 迁移到 Hono:从无状态路由入手
将 Express 应用迁移到 Hono 时,最稳妥的起点是无状态的、仅返回 JSON 的路由——不涉及文件系统访问、不依赖 Node 专有的流,也不使用会话中间件。这类路由可以干净利落地完成转换。
而依赖 Node 专有 API(如 req.socket、res.locals、res.sendFile)的路由则需要更多考量。在着手全面迁移之前,先将这些路由隔离出来,搞清楚自己面对的是什么。
Discover how at OpenReplay.com.
Hono 中间件迁移:不要想当然地认为兼容
Express 中间件遵循 (req, res, next) 的模式。Hono 中间件则使用 (c, next),通常调用 await next()。两者并不可以互换。
// Express 中间件
app.use((req, res, next) => {
req.startTime = Date.now()
next()
})
// Hono 中的等效写法
app.use(async (c, next) => {
c.set('startTime', Date.now())
await next()
})
对于常用的包,Hono 提供了官方对应的实现:
| Express | Hono 对应实现 |
|---|---|
cors() | hono/cors |
helmet() | hono/secure-headers |
express.json() | 通过 c.req.json() 内置支持 |
morgan | hono/logger 或自定义中间件 |
建议主动重写中间件,而不是试图去包装 Express 中间件——这种抽象层很少能完美适配。
请求体与响应处理
在 Express 5 中,请求体解析仍然基于中间件。而在 Hono 中,你直接在处理函数中解析请求体:
// Hono 请求体解析
app.post('/items', async (c) => {
const body = await c.req.json()
return c.json({ received: body }, 201)
})
响应始终是被返回的,而不是被修改的。这里没有 res.status(201).json(...)——你需要将状态码作为第二个参数传给 c.json()。
错误处理
Express 使用四参数的错误处理函数 (err, req, res, next)。Hono 则使用 app.onError:
app.onError((err, c) => {
console.error(err)
return c.json({ error: 'Internal Server Error' }, 500)
})
渐进式迁移,而非一次性全部重写
完全重写极少能顺利推进。更好的方式是:
- 一次只迁移一组隔离的路由。
- 在过渡期内让 Hono 与 Express 并行运行。
- 显式重写中间件——不要包装它。
- 将 Node 专有的依赖(如文件处理、遗留的认证逻辑)留到最后处理。
结语
一旦你接受了 Express 与 Hono 建立在不同基础之上的事实,将 Express 应用迁移到 Hono 其实并不复杂。其路由语法足够熟悉,大多数路由几分钟内就能完成转换。真正的工作量在于中间件,以及任何直接接触 Node 专有 API 的部分。审慎地处理这些环节,迁移就会变得可控。
常见问题
可以。一种常见的做法是在两者前面架设一个反向代理或一个小型 Node 入口,将特定路径路由到 Hono 应用,其余仍交给 Express。这样你就能一次迁移一组路由,并在出现问题时快速回滚,而不必押注在一次风险较高的整体切换上。
Hono 可以运行在 Node.js、Bun、Deno、Cloudflare Workers、AWS Lambda 等多种运行时上。在 Node 上,你需要使用 @hono/node-server 适配器来衔接 Fetch API 模型与 Node 的 http 模块。同一份处理函数代码可以在不同运行时之间保持可移植性,这是 Hono 相对 Express 的主要优势之一。
通常来说,没有。Express 中间件依赖 Node 的 req、res 对象以及 next 回调,而 Hono 中间件运行在 Fetch 风格的上下文之上。社区中确实有一些适配器,但它们往往比较脆弱。推荐的做法是基于 Hono 的 API 重写中间件,因为大多数常见需求已经有官方或内置的等效实现。
得益于轻量级路由器和基于 Fetch 的设计,Hono 通常比 Express 更快,尤其在路由处理和 JSON 响应方面。实际收益取决于你的具体负载——数据库查询和外部调用通常才是主要瓶颈。除非你的基准测试显示路由开销确实成为瓶颈,否则建议将性能提升视为迁移的副产品,而不是迁移的主要动因。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.