AI 电子伴侣
创建时间: 2026-04-08最后更新: 2026-04-18

1. Context 对象:路由处理的唯一入口

Hono 的每个路由处理函数都接收一个参数 c,这就是 Context 对象。

所有请求信息的读取、响应的构造,都通过它完成。

src/index.ts
1
app.get('/hello', (c) => {
2
// c.req — 请求信息
3
// c.json() / c.text() / c.html() — 构造响应
4
// c.header() — 设置响应头
5
// c.status() — 设置状态码
6
return c.text('Hello!')
7
})

不像 Express 把请求和响应拆成 reqres 两个对象,Hono 把所有东西收敛到一个 c 里。API 表面积更小,用起来更直觉。

2. 读取请求信息

查询参数

URL 里 ?key=value 的部分:

src/index.ts
1
// GET /search?q=hono&page=2
2
app.get('/search', (c) => {
3
const q = c.req.query('q') // 'hono'
4
const page = c.req.query('page') // '2'(注意是字符串)
5
return c.json({ q, page })
6
})

路由参数

路径中 :param 定义的动态段:

src/index.ts
1
// GET /users/42
2
app.get('/users/:id', (c) => {
3
const id = c.req.param('id') // '42'
4
return c.json({ id })
5
})

请求头

src/index.ts
1
app.get('/profile', (c) => {
2
const token = c.req.header('Authorization')
3
if (!token) {
4
return c.json({ error: 'Unauthorized' }, 401)
5
}
6
return c.json({ token })
7
})

请求体 — JSON

POST/PUT 请求发送的 JSON 数据,需要 await

src/index.ts
1
app.post('/users', async (c) => {
2
const body = await c.req.json()
3
// body: { name: 'Alice', email: 'alice@example.com' }
4
return c.json({ created: body }, 201)
5
})

这里有一个很重要的注意点:请求体通常只能读取一次。也就是说,你不能先 await c.req.text() 看一眼原始内容,再接着 await c.req.json() 重新解析。同一个 body 被消费后,后面通常就不能再读了。

所以实战里要先想清楚:这次请求你到底要按 JSON 读、按文本读,还是按表单读,然后只选一种方式。

请求体 — 纯文本

src/index.ts
1
app.post('/webhook', async (c) => {
2
const text = await c.req.text()
3
console.log('Received:', text)
4
return c.text('OK')
5
})

请求体 — 表单数据

src/index.ts
1
app.post('/upload', async (c) => {
2
const formData = await c.req.formData()
3
const name = formData.get('name')
4
const file = formData.get('file') // File 对象
5
return c.json({ name, fileSize: file?.size })
6
})

基本信息

src/index.ts
1
app.get('/debug', (c) => {
2
return c.json({
3
url: c.req.url, // 'http://localhost:8787/debug?a=1'
4
method: c.req.method, // 'GET'
5
path: c.req.path, // '/debug'
6
})
7
})

3. 构造响应

c.json() — 返回 JSON

最常用的响应方式,自动设置 Content-Type: application/json

src/index.ts
1
app.get('/api/users', (c) => {
2
return c.json({
3
users: [
4
{ id: 1, name: 'Alice' },
5
{ id: 2, name: 'Bob' },
6
]
7
})
8
})

第二个参数可以传状态码:

src/index.ts
1
app.post('/api/users', async (c) => {
2
const body = await c.req.json()
3
return c.json({ id: 3, ...body }, 201) // 201 Created
4
})

c.text() — 返回纯文本

src/index.ts
1
app.get('/health', (c) => {
2
return c.text('OK')
3
})

c.html() — 返回 HTML

src/index.ts
1
app.get('/page', (c) => {
2
return c.html('<h1>Hello</h1><p>This is HTML</p>')
3
})

c.redirect() — 重定向

src/index.ts
1
app.get('/old-path', (c) => {
2
return c.redirect('/new-path') // 默认 302 临时重定向
3
})
4
5
app.get('/moved', (c) => {
6
return c.redirect('/new-home', 301) // 301 永久重定向
7
})

c.body() — 无内容响应

某些操作完成后不需要返回数据,比如 DELETE:

src/index.ts
1
app.delete('/api/users/:id', (c) => {
2
const id = c.req.param('id')
3
// ... 执行删除逻辑
4
return c.body(null, 204) // 204 No Content
5
})

c.newResponse() — 完全自定义

当上面的快捷方法不够用时,可以用 c.newResponse() 完全控制响应:

src/index.ts
01
app.get('/custom', (c) => {
02
return c.newResponse('custom body', {
03
status: 200,
04
headers: {
05
'Content-Type': 'text/plain',
06
'X-Custom-Header': 'my-value',
07
'Cache-Control': 'max-age=3600',
08
},
09
})
10
})

4. 设置响应头和状态码

除了在 c.json() 等方法里直接传状态码,还可以单独设置:

src/index.ts
1
app.get('/api/data', (c) => {
2
c.status(200)
3
c.header('X-Request-Id', crypto.randomUUID())
4
c.header('Cache-Control', 'no-cache')
5
return 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() 来在同一个请求的生命周期内传递数据。最典型的用法:中间件解析出用户信息,路由处理函数直接取用。

src/index.ts
01
import { Hono } from 'hono'
02
03
const app = new Hono()
04
05
// 中间件:解析用户身份
06
app.use('/api/*', async (c, next) => {
07
const token = c.req.header('Authorization')
08
// 假设 verifyToken 是你的验证函数
09
const user = { id: 1, name: 'Alice' } // 实际从 token 解析
10
c.set('user', user) // 存数据
11
await next()
12
})
13
14
// 路由:直接拿中间件存的数据
15
app.get('/api/profile', (c) => {
16
const user = c.get('user') // 取数据
17
return c.json(user)
18
})

c.set() / c.get() 的作用域是单次请求,不同请求之间互不影响。这个机制会在下一篇中间件文章里详细展开。

上面这个例子为了先讲清概念,省略了类型声明。真实项目里,像 user 这种你在中间件里写入、在路由里读取的数据,最好补上类型,这样编辑器才能正确提示字段,避免把 user.name 写错。

6. Context 和 Web Standards 的关系

c.req 不是凭空造出来的 API,它是对浏览器标准 Request 对象的包装。你可以随时访问原始的 Request:

src/index.ts
1
app.get('/raw', (c) => {
2
const rawRequest = c.req.raw // 标准的 Request 对象
3
console.log(rawRequest instanceof Request) // true
4
return 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 里学到的东西不是框架私有知识。RequestResponseHeadersFormData 这些 API,在浏览器的 fetch()、Workers、Hono 里其实是一套相通的模型。

7. 总结

Context 对象 c 是 Hono 里做事情的唯一入口:c.req 读请求,c.json() / c.text() 写响应,c.set() / c.get() 传数据。API 设计很薄,基本不用查文档就能猜到方法名。

下一篇讲中间件——Hono 用洋葱模型组织请求处理流程,c.set() / c.get() 会在那里真正派上用场。