1. Context 对象:路由处理的唯一入口
Hono 的每个路由处理函数都接收一个参数 c,这就是 Context 对象。
所有请求信息的读取、响应的构造,都通过它完成。
1app.get('/hello', (c) => {2// c.req — 请求信息3// c.json() / c.text() / c.html() — 构造响应4// c.header() — 设置响应头5// c.status() — 设置状态码6return c.text('Hello!')7})
不像 Express 把请求和响应拆成 req 和 res 两个对象,Hono 把所有东西收敛到一个 c 里。API 表面积更小,用起来更直觉。
2. 读取请求信息
查询参数
URL 里 ?key=value 的部分:
1// GET /search?q=hono&page=22app.get('/search', (c) => {3const q = c.req.query('q') // 'hono'4const page = c.req.query('page') // '2'(注意是字符串)5return c.json({ q, page })6})
路由参数
路径中 :param 定义的动态段:
1// GET /users/422app.get('/users/:id', (c) => {3const id = c.req.param('id') // '42'4return c.json({ id })5})
请求头
1app.get('/profile', (c) => {2const token = c.req.header('Authorization')3if (!token) {4return c.json({ error: 'Unauthorized' }, 401)5}6return c.json({ token })7})
请求体 — JSON
POST/PUT 请求发送的 JSON 数据,需要 await:
1app.post('/users', async (c) => {2const body = await c.req.json()3// body: { name: 'Alice', email: 'alice@example.com' }4return c.json({ created: body }, 201)5})
这里有一个很重要的注意点:请求体通常只能读取一次。也就是说,你不能先 await c.req.text() 看一眼原始内容,再接着 await c.req.json() 重新解析。同一个 body 被消费后,后面通常就不能再读了。
所以实战里要先想清楚:这次请求你到底要按 JSON 读、按文本读,还是按表单读,然后只选一种方式。
请求体 — 纯文本
1app.post('/webhook', async (c) => {2const text = await c.req.text()3console.log('Received:', text)4return c.text('OK')5})
请求体 — 表单数据
1app.post('/upload', async (c) => {2const formData = await c.req.formData()3const name = formData.get('name')4const file = formData.get('file') // File 对象5return c.json({ name, fileSize: file?.size })6})
基本信息
1app.get('/debug', (c) => {2return c.json({3url: c.req.url, // 'http://localhost:8787/debug?a=1'4method: c.req.method, // 'GET'5path: c.req.path, // '/debug'6})7})
3. 构造响应
c.json() — 返回 JSON
最常用的响应方式,自动设置 Content-Type: application/json:
1app.get('/api/users', (c) => {2return c.json({3users: [4{ id: 1, name: 'Alice' },5{ id: 2, name: 'Bob' },6]7})8})
第二个参数可以传状态码:
1app.post('/api/users', async (c) => {2const body = await c.req.json()3return c.json({ id: 3, ...body }, 201) // 201 Created4})
c.text() — 返回纯文本
1app.get('/health', (c) => {2return c.text('OK')3})
c.html() — 返回 HTML
1app.get('/page', (c) => {2return c.html('<h1>Hello</h1><p>This is HTML</p>')3})
c.redirect() — 重定向
1app.get('/old-path', (c) => {2return c.redirect('/new-path') // 默认 302 临时重定向3})45app.get('/moved', (c) => {6return c.redirect('/new-home', 301) // 301 永久重定向7})
c.body() — 无内容响应
某些操作完成后不需要返回数据,比如 DELETE:
1app.delete('/api/users/:id', (c) => {2const id = c.req.param('id')3// ... 执行删除逻辑4return c.body(null, 204) // 204 No Content5})
c.newResponse() — 完全自定义
当上面的快捷方法不够用时,可以用 c.newResponse() 完全控制响应:
01app.get('/custom', (c) => {02return c.newResponse('custom body', {03status: 200,04headers: {05'Content-Type': 'text/plain',06'X-Custom-Header': 'my-value',07'Cache-Control': 'max-age=3600',08},09})10})
4. 设置响应头和状态码
除了在 c.json() 等方法里直接传状态码,还可以单独设置:
1app.get('/api/data', (c) => {2c.status(200)3c.header('X-Request-Id', crypto.randomUUID())4c.header('Cache-Control', 'no-cache')5return c.json({ data: 'hello' })6})
c.header() 可以链式调用多次,每次设置一个响应头。注意 c.status() 和 c.header() 要在返回响应之前调用。
对新手来说,可以先记一个简单规则:
- 只是返回一个普通 JSON,并顺手带状态码:优先用
c.json(data, 201)这种写法 - 如果你还要额外设置多个响应头,或者想把"设置状态码"和"返回响应体"分开写清楚,再用
c.status()+c.header()
前者更短,后者更适合逻辑稍复杂的场景。
5. c.set / c.get — 中间件与路由之间传数据
Hono 提供了 c.set() 和 c.get() 来在同一个请求的生命周期内传递数据。最典型的用法:中间件解析出用户信息,路由处理函数直接取用。
01import { Hono } from 'hono'0203const app = new Hono()0405// 中间件:解析用户身份06app.use('/api/*', async (c, next) => {07const token = c.req.header('Authorization')08// 假设 verifyToken 是你的验证函数09const user = { id: 1, name: 'Alice' } // 实际从 token 解析10c.set('user', user) // 存数据11await next()12})1314// 路由:直接拿中间件存的数据15app.get('/api/profile', (c) => {16const user = c.get('user') // 取数据17return c.json(user)18})
c.set() / c.get() 的作用域是单次请求,不同请求之间互不影响。这个机制会在下一篇中间件文章里详细展开。
上面这个例子为了先讲清概念,省略了类型声明。真实项目里,像 user 这种你在中间件里写入、在路由里读取的数据,最好补上类型,这样编辑器才能正确提示字段,避免把 user.name 写错。
6. Context 和 Web Standards 的关系
c.req 不是凭空造出来的 API,它是对浏览器标准 Request 对象的包装。你可以随时访问原始的 Request:
1app.get('/raw', (c) => {2const rawRequest = c.req.raw // 标准的 Request 对象3console.log(rawRequest instanceof Request) // true4return c.text('OK')5})
而 c.json()、c.text() 等方法返回的都是标准的 Response 对象。所以 Hono 的处理函数本质上就是:接收一个 Request,返回一个 Response——这和 Web 标准的 Fetch Handler 完全一致。
你可以把前后端的关系想成这样:
- 浏览器里
fetch('/api/users')发出去的是一个标准Request - Hono 在服务端通过
c.req读取这个请求 - 你的路由处理函数返回一个标准
Response - 浏览器最终收到这个
Response
这意味着你在 Hono 里学到的东西不是框架私有知识。Request、Response、Headers、FormData 这些 API,在浏览器的 fetch()、Workers、Hono 里其实是一套相通的模型。
7. 总结
Context 对象 c 是 Hono 里做事情的唯一入口:c.req 读请求,c.json() / c.text() 写响应,c.set() / c.get() 传数据。API 设计很薄,基本不用查文档就能猜到方法名。
下一篇讲中间件——Hono 用洋葱模型组织请求处理流程,c.set() / c.get() 会在那里真正派上用场。