请求参数获取
要点
- 路径参数用
c.req.param(),动态路由段:id定义的占位符 - 查询参数用
c.req.query()取单值,c.req.queries()取多值 - JSON 请求体用
await c.req.json(),注意需要await - 表单和文件上传用
await c.req.parseBody() - 请求头用
c.req.header()读取,Cookie 需要用hono/cookieHelper - 所有参数获取方式最终都基于 Web 标准的 Request 对象
1. 路径参数(Path Params)
路径参数从动态路由中提取。路由定义时用 :name 声明占位符,请求匹配后就能拿到对应值。
// src/index.ts
import { Hono } from 'hono'
const app = new Hono()
// 单个路径参数
app.get('/api/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ id })
})
// 多个路径参数
app.get('/api/users/:userId/posts/:postId', (c) => {
const userId = c.req.param('userId')
const postId = c.req.param('postId')
return c.json({ userId, postId })
})
// 一次获取所有路径参数
app.get('/api/:version/users/:id', (c) => {
const allParams = c.req.param()
return c.json(allParams)
})
export default appc.req.param('id') 返回 string | undefined,不传参数调用 c.req.param() 返回包含所有路径参数的 Record 对象。
用 curl 测试:
// terminal
# 单个参数
curl http://localhost:8787/api/users/42
# => {"id":"42"}
# 多个参数
curl http://localhost:8787/api/users/42/posts/7
# => {"userId":"42","postId":"7"}
# 获取所有参数
curl http://localhost:8787/api/v1/users/42
# => {"version":"v1","id":"42"}路径参数始终是字符串类型,需要数字的话自己转换:Number(id) 或 parseInt(id, 10)。
2. 查询参数(Query Params)
查询参数来自 URL 中 ? 后面的部分,比如 /api/users?page=1&sort=name。
// src/index.ts
app.get('/api/users', (c) => {
// 获取单个查询参数
const page = c.req.query('page')
const sort = c.req.query('sort')
return c.json({
page: page ?? '1',
sort: sort ?? 'created_at',
})
})
// 获取同名多值参数:?tag=ts&tag=node
app.get('/api/posts', (c) => {
const tags = c.req.queries('tag')
return c.json({ tags })
})c.req.query('key') 返回 string | undefined,参数不存在时返回 undefined。c.req.queries('key') 返回 string[] | undefined,专门处理同名多值的情况。
// terminal
# 基本查询参数
curl "http://localhost:8787/api/users?page=2&sort=name"
# => {"page":"2","sort":"name"}
# 缺省参数
curl "http://localhost:8787/api/users"
# => {"page":"1","sort":"created_at"}
# 同名多值
curl "http://localhost:8787/api/posts?tag=ts&tag=node"
# => {"tags":["ts","node"]}注意 curl 命令里 URL 带 ? 和 & 时需要用引号包起来,避免 shell 把 ? 当成通配符。
3. 请求体:JSON
接收 JSON 请求体用 await c.req.json()。它会自动解析 Content-Type: application/json 的请求体并返回 JavaScript 对象。
// src/index.ts
app.post('/api/users', async (c) => {
const body = await c.req.json()
return c.json({
message: '创建成功',
data: body,
}, 201)
})这个回调函数标记了 async,因为 c.req.json() 返回 Promise。这是新手容易遗漏的地方——忘记 await 拿到的是一个 Promise 对象而不是解析后的数据。
加上错误处理:
// src/index.ts
app.post('/api/users', async (c) => {
let body: unknown
try {
body = await c.req.json()
} catch {
return c.json({ error: '无效的 JSON' }, 400)
}
const { name, email } = body as { name?: string; email?: string }
if (!name || !email) {
return c.json({ error: 'name 和 email 是必填项' }, 422)
}
return c.json({ message: '创建成功', user: { name, email } }, 201)
})// terminal
curl -X POST http://localhost:8787/api/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "[email protected]"}'
# => {"message":"创建成功","user":{"name":"Alice","email":"[email protected]"}}
# 测试无效 JSON
curl -X POST http://localhost:8787/api/users \
-H "Content-Type: application/json" \
-d 'not-json'
# => {"error":"无效的 JSON"}如果不想写 try/catch,Hono 还提供了一个更简洁的写法:
// src/index.ts
app.post('/api/users', async (c) => {
const body = await c.req.json().catch(() => null)
if (!body) {
return c.json({ error: '无效的 JSON' }, 400)
}
return c.json({ data: body })
}).catch(() => null) 把解析失败的情况兜住,返回 null,后续统一判断。
4. 请求体:Form Data
await c.req.parseBody() 可以同时处理 application/x-www-form-urlencoded 和 multipart/form-data 两种格式。普通表单提交和文件上传都走这个方法。
// src/index.ts
app.post('/api/login', async (c) => {
const body = await c.req.parseBody()
const username = body['username'] as string
const password = body['password'] as string
return c.json({ message: `欢迎, ${username}` })
})文件上传场景:
// src/index.ts
app.post('/api/upload', async (c) => {
const body = await c.req.parseBody()
const file = body['file'] as File
return c.json({
name: file.name,
size: file.size,
type: file.type,
})
})parseBody() 会把文件字段解析成 File 对象,普通字段解析成 string。需要用 as 做类型断言,因为返回类型是 string | File。
// terminal
# 普通表单
curl -X POST http://localhost:8787/api/login \
-d "username=alice&password=123456"
# => {"message":"欢迎, alice"}
# 文件上传
curl -X POST http://localhost:8787/api/upload \
-F "[email protected]"
# => {"name":"photo.jpg","size":102400,"type":"image/jpeg"}5. 请求体:纯文本
接收纯文本请求体用 await c.req.text():
// src/index.ts
app.post('/api/echo', async (c) => {
const text = await c.req.text()
return c.text(`你说的是: ${text}`)
})// terminal
curl -X POST http://localhost:8787/api/echo \
-H "Content-Type: text/plain" \
-d "Hello World"
# => 你说的是: Hello Worldc.req.text() 把请求体作为完整字符串返回,不做任何解析。
6. 请求头(Headers)
读取请求头用 c.req.header():
// src/index.ts
app.get('/api/auth', (c) => {
// 获取单个 Header
const contentType = c.req.header('Content-Type')
const authorization = c.req.header('Authorization')
const userAgent = c.req.header('User-Agent')
return c.json({
contentType,
hasAuth: !!authorization,
userAgent,
})
})c.req.header('name') 返回 string | undefined,Header 名称不区分大小写。
如果需要遍历所有 Header:
// src/index.ts
app.get('/api/headers', (c) => {
const headers: Record<string, string> = {}
c.req.raw.headers.forEach((value, key) => {
headers[key] = value
})
return c.json(headers)
})c.req.raw.headers 是标准的 Headers 对象,可以用 forEach 遍历,也可以用 get()、has() 等方法操作。
// terminal
curl http://localhost:8787/api/auth \
-H "Authorization: Bearer token123" \
-H "X-Custom-Header: hello"
# => {"contentType":null,"hasAuth":true,"userAgent":"curl/8.x"}实际项目中 Authorization Header 的提取通常在中间件里完成,而不是在每个路由回调里重复写。后面中间件那篇会详细讲。
7. Cookie
Hono 提供了 Cookie Helper,需要单独导入:
// src/index.ts
import { getCookie, setCookie } from 'hono/cookie'
const app = new Hono()
app.get('/api/profile', (c) => {
const sessionId = getCookie(c, 'session_id')
if (!sessionId) {
return c.json({ error: '未登录' }, 401)
}
return c.json({ sessionId })
})
app.post('/api/login', (c) => {
setCookie(c, 'session_id', 'abc123', {
httpOnly: true,
secure: true,
maxAge: 3600,
path: '/',
})
return c.json({ message: '登录成功' })
})getCookie(c, 'name') 返回 string | undefined,第一个参数是 Hono 的 Context 对象。setCookie 第三个参数是可选的配置项,支持 httpOnly、secure、maxAge、path、domain、sameSite 等标准 Cookie 属性。
// terminal
# 登录获取 Cookie
curl -X POST http://localhost:8787/api/login -v
# 响应头里会包含 Set-Cookie: session_id=abc123; ...
# 带 Cookie 访问
curl http://localhost:8787/api/profile \
-H "Cookie: session_id=abc123"
# => {"sessionId":"abc123"}注意 getCookie 和 c.req.header('Cookie') 的区别:后者返回整个 Cookie 字符串(session_id=abc123; other=value),需要自己解析;前者直接按名字取值,省事。
8. 请求对象(Request)
Hono 的 c.req 是对标准 Request 的封装。如果需要直接访问原始 Request 对象,用 c.req.raw:
// src/index.ts
app.get('/api/raw', (c) => {
const rawRequest = c.req.raw
return c.json({
method: rawRequest.method,
url: rawRequest.url,
// c.req.raw.headers 就是标准 Headers 对象
contentType: rawRequest.headers.get('Content-Type'),
})
})大多数场景不需要直接操作 c.req.raw,Hono 的封装已经覆盖了常见需求。但有几个情况会用到:
- 获取完整 URL:
c.req.url是 Hono 提供的便捷属性,返回当前请求的完整 URL 字符串 - 传递给其他库:有些第三方库接收标准
Request对象 - 判断请求特征:
c.req.raw.bodyUsed判断请求体是否已被读取
// src/index.ts
app.get('/api/info', (c) => {
// Hono 封装的便捷属性
const url = c.req.url // 完整 URL 字符串
const method = c.req.method // 请求方法
const path = c.req.path // 路径部分
return c.json({ url, method, path })
})c.req.url 和 c.req.raw.url 结果一样,Hono 把常用属性直接暴露在了 c.req 上。
9. 对比总结
把所有参数获取方式放在一起对比:
| 方法 | 数据来源 | 返回类型 | 需要 await |
|---|---|---|---|
c.req.param('id') | URL 路径 /users/:id | string | undefined | 否 |
c.req.param() | URL 路径全部 | Record<string, string> | 否 |
c.req.query('page') | URL 查询 ?page=1 | string | undefined | 否 |
c.req.queries('tag') | URL 查询多值 | string[] | undefined | 否 |
await c.req.json() | 请求体 JSON | any | 是 |
await c.req.parseBody() | 请求体 Form/FormData | Record<string, string | File> | 是 |
await c.req.text() | 请求体纯文本 | string | 是 |
c.req.header('name') | 请求头 | string | undefined | 否 |
getCookie(c, 'name') | Cookie | string | undefined | 否 |
区分同步和异步的关键在于数据来源:路径、查询参数、Header、Cookie 都在请求头部分,解析成本低,同步返回;请求体需要从网络流中读取数据,必须异步。
延伸阅读
总结
这篇覆盖了 Hono 中获取请求参数的全部方式。路径参数、查询参数、请求头都是同步获取,请求体需要 await。Cookie 通过 hono/cookie Helper 处理。
核心记住三点:
- 同步的:
param()、query()、header()、getCookie()——数据在请求头里,直接取 - 异步的:
json()、parseBody()、text()——数据在请求体里,需要await - 底层的:
c.req.raw可以拿到标准 Request 对象,封装不够用时兜底
下一篇讲 Hono 的中间件机制,看看怎么在路由处理之前和之后插入通用逻辑。