文档简介: Hono 4 的应用、路由、Context 对象、中间件生态与错误处理的条目式速查,含从 Express 迁移的心智模型差异
目标读者: 使用 Hono 开发 Node.js 后端的开发者
前置知识: 第一个服务器
文档信息(用途、难度与维护记录)
| 属性 | 内容 |
|---|---|
| 模块 | 09-nodejs-backend |
| 象限 | 字典(reference) |
| 难度 | ⭐ |
| 标签 | #Hono4 #路由 #中间件 #Context |
| 更新日期 | 2026年9月 |
Hono 实例既是应用也是"路由器":主应用与子应用是同一类型,用 route() 组合,天然支持任意层级嵌套。
import { Hono } from "hono";
const app = new Hono();
const users = new Hono();
users.get("/"); // 子应用内路径不带前缀
users.get("/:id");
users.post("/");
app.route("/users", users); // 挂载:/users、/users/:id
// 或让子应用自带前缀:users.basePath("/users")
app.all("/log", (c) => c.body(null)); // 匹配所有 HTTP 方法
app.on("GET", ["/a", "/b"], handler); // 多路径精确声明app.get("/users/:id"); // ✅ 命名参数
app.get("/users/:id?"); // ✅ 可选段:同时匹配 /users 与 /users/9
app.get("/posts/:id{[0-9]+}"); // ✅ 带正则约束的参数(非数字不匹配)
app.get("/static/*"); // ✅ 通配符(Hono 原生支持,无需具名)- 可选段未命中时
param是undefined:/users/:id?匹配/users时c.req.param("id")返回undefined(类型也是string | undefined)——用前先判空或给默认值(c.req.param("id") ?? "me"),别直接喂给只认 string 的逻辑 - 通配符
*与具名参数不能出现在同一段:/files/*与/files/:id冲突时按注册顺序命中 - 子应用内写死
/users/:id再挂到/users前缀下,会得到/users/users/:id——前缀只出现在挂载处
处理器与中间件只接触一个 Context(c)。请求侧全部挂在 c.req 上,返回值均为解析后的数据。
c.req.param("id"); // 路径参数(string);无参调用返回全量对象
c.req.query("page"); // 查询参数(string | undefined);无参调用返回全量对象
c.req.header("Content-Type"); // 请求头(名字大小写不敏感)
c.req.url; // 完整 URL 字符串
c.req.path; // 路径部分
c.req.method;
await c.req.json(); // 解析 JSON 请求体
await c.req.text(); // 纯文本请求体
await c.req.arrayBuffer(); // 二进制请求体
await c.req.parseBody(); // 表单 → 普通键值对象(文件值可为 File),不是 FormData 实例
c.req.raw; // 底层原生 Request(Web 标准)
c.req.valid("json"); // 经 validator 校验后的类型安全数据param/query返回的都是string——数字比较前必须显式转换(或用 Zodz.coerce)c.req.json()在请求体不是合法 JSON 时抛错——交给app.onError统一兜住,不要裸调不接parseBody()用于 multipart/form-data 或 application/x-www-form-urlencoded;JSON 应调用 json(),不要依赖不匹配类型的结果来验证输入
c.json()/c.text()/c.html() 等工厂方法返回 Response(或作为中间件返回值)。处理器必须返回响应——没有 Express 式的 res.end() 副作用写法。
c.json(data); // 200 JSON,自动 Content-Type
c.json({ id }, 201); // 第二参即状态码
c.json({ error: "x" }, 404);
c.text("pong");
c.html("<h1>ok</h1>");
c.body(null, 204); // 自定义响应体
c.body(stream, 200, { "Content-Type": "application/octet-stream" }); // 流式响应
c.redirect("/login", 301);
c.notFound(); // 返回内置 404 响应
c.res.status; // 读取当前响应(中间件在 next() 之后可用)
c.res.headers.set("Cache-Control", "no-store");- 处理器
return了一个响应后又继续执行代码——响应不会二次生效;分支逻辑记得return - 在 next() 后可访问最终响应;若提前设置响应头,可用 c.header(),避免把尚未生成的响应误当作最终结果
app.use(path?, handler) 注册中间件,签名 (c, next),洋葱模型:await next() 前是"进入"阶段,之后是"响应"阶段。
import type { MiddlewareHandler } from "hono";
// 先声明具体中间件,再注册;它不是返回中间件的工厂函数。
const requestLogger: MiddlewareHandler = async (c, next) => {
const start = performance.now();
await next();
console.log(`${c.req.method} ${c.req.path} ${c.res.status} ${performance.now() - start}ms`);
};
app.use(requestLogger);
app.use("/api/*", cors());
app.post("/upload", requireAuth, uploadLimiter, uploadHandler);- 顺序即语义:
bodyLimit必须在parseBody之前;认证在校验之前 - 中间件既不继续也不返回响应会导致响应未完成错误;不要把它当作暂停请求的方法
- 在
next()前抛错会跳过洋葱内侧的中间件——清理逻辑用try/finally
全部来自 hono/* 子路径导出,零额外依赖——这是 Hono 相对 Express 最大的工程差异之一。
| 导入 | 用途 | 备注 |
|---|---|---|
hono/logger |
开发期请求日志 | 生产换 pino 等结构化方案 |
hono/cors |
CORS 头 | origin/allowMethods/credentials 等 |
hono/jwt |
JWT 验签 | payload 自动写入 c.get("jwtPayload") |
hono/secure-headers |
安全响应头 | 相当于 helmet 的默认集 |
hono/basic-auth、hono/bearer-auth |
基础认证 | 内部工具/快速原型 |
hono/body-limit |
请求体大小上限 | 解析前拦截 |
hono/compress |
gzip 压缩 | node-server 下可用 |
hono/etag |
ETag 协商 | 自动 304 |
hono/timing |
Server-Timing 头 | 性能观测 |
hono/cache |
基于 Cache API 的响应缓存 | 需要运行时提供相应 Cache API;Node 环境不能仅靠导入就获得存储 |
hono/cookie |
Cookie 读写 | setCookie/getCookie/deleteCookie |
hono/http-exception |
HTTPException | 快速抛带状态码的错误 |
import { cors } from "hono/cors";
import { secureHeaders } from "hono/secure-headers";
app.use(secureHeaders()); // 默认安全头;CSP 需按应用显式配置
app.use("/api/*", cors({
origin: ["https://app.example.com"],
allowMethods: ["GET", "POST", "PATCH", "DELETE"],
credentials: true,
}));- 第三方中间件(限流、session 等)在
@hono/*或社区包中,选型前确认维护状态 cors()不传origin时默认*,且通配与credentials: true不能同用
app.onError 处理请求执行链中抛出的错误和被等待的拒绝;脱离请求链的后台 Promise 不会自动交给它。app.notFound 处理未匹配路由。
import { HTTPException } from "hono/http-exception";
app.onError((err, c) => {
if (err instanceof HTTPException) {
return err.getResponse(); // 框架异常直接透出
}
if (err instanceof HttpError) {
return c.json({ error: err.message }, err.status);
}
console.error("[unhandled]", err);
return c.text("Internal Server Error", 500);
});
app.notFound((c) => c.json({ error: "路由不存在" }, 404));- 没注册
onError时 Hono 返回默认 500 文本——生产项目必须注册并接入日志/Sentry onError内部再抛错没有兜底——确保自身逻辑不会 throw
app.request() 是内置测试客户端:向 app 直接发起模拟请求,返回 Web 标准 Response,无需监听端口。生产运行用官方适配器 @hono/node-server。
// 测试:GET /health
const res = await app.request("/health");
res.status; // 200
await res.json(); // 响应体
// 测试:POST JSON
const res = await app.request("/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "x" }),
});
// 生产启动(node-server 适配器)
import { serve } from "@hono/node-server";
const server = serve({ fetch: app.fetch, port: 3000 }); // 返回 node http.Server
server.close(); // 优雅关闭沿用 Node APIserve()不传对象时也可serve(app),但需要自定义端口/hostname 时用{ fetch, port, hostname }形式app.request()拿到的Response是流式 body——json()只能消费一次
| 维度 | Express | Hono 4 |
|---|---|---|
| 处理器签名 | (req, res, next) |
(c) => Response |
| 中间件模型 | 线性管道 + 四参错误中间件 | 洋葱模型 + app.onError |
| 响应方式 | res.json() 副作用 |
return c.json() 返回值 |
| 路由器 | Router() 构造器 |
new Hono() 子应用 + route() |
| 请求体解析 | express.json() 中间件 |
内建 c.req.json() |
| 类型扩展 | namespace Express { Request } |
interface ContextVariableMap |
| 测试 | supertest(或注入 app) | app.request() 内置 |
| 运行时 | Node 专属 | Web 标准 API,跨运行时 |
Hono 的 Context 是本次请求的上下文,不是全局用户状态。把认证结果存入 c.set,下游用 c.get 读取,可以避免不同请求共享一个可变的 currentUser。TypeScript 的 Variables 声明只帮助检查类型,不能证明认证中间件确实运行过;注册顺序和路径仍需测试。
中间件执行 await next() 后才继续处理响应;提前 return c.json(..., 401) 会阻止后续业务执行。框架捕获路由抛出的错误并构建响应,因此外层中间件不能假设每个下游错误都会以被拒绝的 next() 传播,必要时查看响应状态和 c.error。
练习:构建一个 /private 路由,用计数器记录业务调用次数。无令牌请求应返回 401 且计数保持 0;合法令牌才增长。再给 JSON 接口传畸形请求体,明确返回 400,而非把所有解析错误都当服务器故障。使用 app.request 检查状态、响应头和响应体;无需为这类测试监听端口。
查阅:请求体解析、中间件执行语义。本页包含 API 片段,使用未定义 handler/schema 的段落需在具体应用中装配。