跳转到正文

最佳实践 ​

Hono 非常灵活,你可以按自己的喜好编写应用。 不过,遵循一些最佳实践会更好。

尽可能不要创建“控制器” ​

尽可能避免创建“类似 Ruby on Rails 的控制器”。

ts
// 🙁
// A RoR-like Controller
const booksList = (c: Context) => {
  return c.json('list books')
}

app.get('/books', booksList)

问题与类型有关。例如,如果不编写复杂的泛型,就无法在控制器中推断路径参数。

ts
// 🙁
// A RoR-like Controller
const bookPermalink = (c: Context) => {
  const id = c.req.param('id') // Can't infer the path param
  return c.json(`get ${id}`)
}

因此,无需创建类似 RoR 的控制器,应该在路径定义之后直接编写处理程序。

ts
// 😃
app.get('/books/:id', (c) => {
  const id = c.req.param('id') // Can infer the path param
  return c.json(`get ${id}`)
})

hono/factory 中的 factory.createHandlers() ​

如果仍希望创建类似 RoR 的控制器,请使用 hono/factory 中的 factory.createHandlers()。这样可以让类型推断正确工作。

ts
import { createFactory } from 'hono/factory'
import { logger } from 'hono/logger'

// ...

// 😃
const factory = createFactory()

const middleware = factory.createMiddleware(async (c, next) => {
  c.set('foo', 'bar')
  await next()
})

const handlers = factory.createHandlers(logger(), middleware, (c) => {
  return c.json(c.var.foo)
})

app.get('/api', ...handlers)

构建大型应用 ​

使用 app.route() 构建大型应用,无需创建“类似 Ruby on Rails 的控制器”。

如果应用有 /authors 和 /books 端点,希望将它们从 index.ts 拆分到独立文件,可以创建 authors.ts 和 books.ts。

ts
// authors.ts
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => c.json('list authors'))
app.post('/', (c) => c.json('create an author', 201))
app.get('/:id', (c) => c.json(`get ${c.req.param('id')}`))

export default app
ts
// books.ts
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => c.json('list books'))
app.post('/', (c) => c.json('create a book', 201))
app.get('/:id', (c) => c.json(`get ${c.req.param('id')}`))

export default app

然后导入它们,使用 app.route() 挂载到 /authors 和 /books 路径。

ts
// index.ts
import { Hono } from 'hono'
import authors from './authors'
import books from './books'

const app = new Hono()

// 😃
app.route('/authors', authors)
app.route('/books', books)

export default app

如果希望使用 RPC 功能 ​

上述代码适用于一般用例。 不过,如果希望使用 RPC 功能,可以像下面这样链式调用,以获取正确的类型。

ts
// authors.ts
import { Hono } from 'hono'

const app = new Hono()
  .get('/', (c) => c.json('list authors'))
  .post('/', (c) => c.json('create an author', 201))
  .get('/:id', (c) => c.json(`get ${c.req.param('id')}`))

export default app
export type AppType = typeof app

将 app 的类型传给 hc,即可获得正确的类型。

ts
import type { AppType } from './authors'
import { hc } from 'hono/client'

// 😃
const client = hc<AppType>('http://localhost') // Typed correctly

有关更多详情,请参阅 RPC 页面。

HEAD 请求最佳实践 ​

理解 Hono 的 HEAD 处理方式 ​

Hono 会自动处理 HEAD 请求:将它们转换为 GET 请求,并移除响应体。此行为内置于框架的请求分发层,在路由匹配之前发生。

✅ 推荐:使用 GET 路由处理 HEAD 请求 ​

typescript
// GOOD: This GET route automatically handles HEAD requests
app.get('/api/users', async (c) => {
  const users = await getUsers()
  c.header('X-Total-Count', users.length.toString())
  return c.json(users)
})

// HEAD /api/users will return:
// - Same headers as GET (including X-Total-Count)
// - Status 200
// - No body (null)

✅ 推荐:使用中间件处理 HEAD 专属逻辑 ​

typescript
// GOOD: Use middleware when HEAD needs different behavior
app.use('/api/resource', async (c, next) => {
  await next()

  // Add HEAD-specific headers after the handler
  if (c.req.method === 'HEAD') {
    c.header('X-HEAD-Processed', 'true')
    // Don't compute expensive body content for HEAD
    c.res = new Response(null, c.res)
  }
})

❌ 不推荐:尝试创建专用的 HEAD 处理程序 ​

typescript
// BAD: This won't work as expected
app.head('/api/users', (c) => {
  // This handler will NEVER be called
  c.header('X-Custom', 'value')
  return c.text('ignored')
})

// BAD: Using on() also won't work
app.on('HEAD', '/api/users', (c) => {
  // Still converted to GET before route matching
})

性能注意事项 ​

  • 如果预期会有大量 HEAD 请求,请避免在 GET 处理程序中执行昂贵操作:使用中间件检测 HEAD,并跳过响应体生成
  • 缓存头的行为相同:HEAD 响应遵循与 GET 相同的缓存规则
  • 中间件兼容性:大多数中间件支持 HEAD,但处理响应体的中间件(如压缩中间件)会自动跳过 HEAD 请求

测试 HEAD 请求 ​

typescript
// Always test both GET and HEAD responses
it('handles HEAD requests correctly', async () => {
  const getRes = await app.request('/api/users')
  const headRes = await app.request('/api/users', { method: 'HEAD' })

  expect(headRes.status).toBe(getRes.status)
  expect(headRes.headers.get('X-Total-Count')).toBe(
    getRes.headers.get('X-Total-Count')
  )
  expect(headRes.body).toBe(null)
})

注意事项 ​

  • 自动转换 HEAD 可确保 GET 和 HEAD 响应的响应头一致
  • 此行为在所有 Hono 运行时(Cloudflare Workers、Deno、Bun、Node.js)中一致
  • 如果 HEAD 与 GET 需要完全不同的逻辑,请考虑使用不同端点,而不是尝试覆盖框架的 HEAD 处理行为

基于 MIT 许可证发布。