请求参数获取

要点

  • 路径参数用 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/cookie Helper
  • 所有参数获取方式最终都基于 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 app

c.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,参数不存在时返回 undefinedc.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-urlencodedmultipart/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 World

c.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 的提取通常在中间件里完成,而不是在每个路由回调里重复写。后面中间件那篇会详细讲。

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 第三个参数是可选的配置项,支持 httpOnlysecuremaxAgepathdomainsameSite 等标准 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"}

注意 getCookiec.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 的封装已经覆盖了常见需求。但有几个情况会用到:

  1. 获取完整 URLc.req.url 是 Hono 提供的便捷属性,返回当前请求的完整 URL 字符串
  2. 传递给其他库:有些第三方库接收标准 Request 对象
  3. 判断请求特征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.urlc.req.raw.url 结果一样,Hono 把常用属性直接暴露在了 c.req 上。

9. 对比总结

把所有参数获取方式放在一起对比:

方法数据来源返回类型需要 await
c.req.param('id')URL 路径 /users/:idstring | undefined
c.req.param()URL 路径全部Record&lt;string, string&gt;
c.req.query('page')URL 查询 ?page=1string | undefined
c.req.queries('tag')URL 查询多值string[] | undefined
await c.req.json()请求体 JSONany
await c.req.parseBody()请求体 Form/FormDataRecord&lt;string, string | File&gt;
await c.req.text()请求体纯文本string
c.req.header('name')请求头string | undefined
getCookie(c, 'name')Cookiestring | undefined

区分同步和异步的关键在于数据来源:路径、查询参数、Header、Cookie 都在请求头部分,解析成本低,同步返回;请求体需要从网络流中读取数据,必须异步。

延伸阅读

总结

这篇覆盖了 Hono 中获取请求参数的全部方式。路径参数、查询参数、请求头都是同步获取,请求体需要 await。Cookie 通过 hono/cookie Helper 处理。

核心记住三点:

  1. 同步的param()query()header()getCookie()——数据在请求头里,直接取
  2. 异步的json()parseBody()text()——数据在请求体里,需要 await
  3. 底层的c.req.raw 可以拿到标准 Request 对象,封装不够用时兜底

下一篇讲 Hono 的中间件机制,看看怎么在路由处理之前和之后插入通用逻辑。