Hono.js 是目前比较流行的后端框架,支持所有 JS 运行时,使用简便,路由和中间件语法类似 express/koa ,可很方便地结合 zod 进行参数校验,支持类似 tRPC 的前后端 RPC 同构能力。

初始化

https://hono.dev/docs/getting-started/basic

默认支持预设:

  • aws-lambda

  • bun

  • cloudflare-pages

  • cloudflare-workers

  • deno

  • fastly

  • nextjs

  • nodejs

  • vercel

代码结构

类似于 Express 的代码结构:

1
2
3
4
5
6
7
8
9
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => {
return c.text('Hello Hono!')
})

export default app

获取参数:

1
2
3
4
5
6
7
app.get('/hello/:test',
(c) => {
const test = c.req.param('test')
return c.json({
test
})
})

中间件

类似 KOA 的洋葱圈中间件模式:

1
2
3
4
5
6
app.use(async (c, next) => {
const start = Date.now()
await next()
const end = Date.now()
c.res.headers.set('X-Response-Time', `${end - start}`)
})

结合 zod 参数校验

安装依赖:

1
npm i -S zod @hono/zod-validator

参数校验,支持param、query、json、header等,同时校验配置多个中间件即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app.post('/create/:postId',
zValidator("json", z.object({
name: z.string(),
userId: z.number(),
})),
zValidator("param", z.object({
postId: z.number(),
})),
(c) => {
const { postId } = c.req.valid("param")
const { name, userId } = c.req.valid("json")
return c.json({
name, userId, postId
})
})

原始内容转换后校验,如将 url 参数的 id 转换为 number

1
2
3
4
5
6
7
app.get('/test/:id', zValidator('param', z.object({
id: z.coerce.number()
})), async c => {
const { id } = c.req.valid('param');
console.log(typeof id, 'id');
return c.json({ id })
})

路由拆分

hono.js 不推荐使用 controller 的模式去拆分路由。

与 express 类似,支持拆分文件:

1
2
3
4
5
6
7
8
9
// books.ts
import { Hono } from 'hono'

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

export default app

在入口文件中引用:

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

异常捕获

可以在处理函数或中间件中抛出异常:

1
throw new HTTPException(401, { message: "未登录" })

使用 onError 捕获:

1
2
3
4
5
6
7
app.onError((err, c) => {
if (err instanceof HTTPException) {
return c.json({ error: err.message }, err.status)
}
console.error(err)
return c.json({ error: '服务器未知错误' }, 500)
})

RPC

hono.js 的 rpc 非常实用,一方面是类似于 express 这样标准的 rest api ,又可获取类似于 tPRC 的前后端同构能力。

在后端项目中导出类型,可通过 pnpm monorepo 在前后端项目间共享类型。如果使用 Next.js 这类前后端同构的框架,可直接获得对应的类型。

需注意,定义路由时要用链式结构连接所有路由,方便 ts 推导类型。

1
2
3
4
5
const app = new Hono().basePath('/api')

const routes = app.route('/authors', authors).route('/books', books)

export type AppType = typeof routes

客户端项目中导入该类型,即可使用:

1
2
3
4
import { hc } from "hono/client";
import { AppType } from "./server";

const client = hc<AppType>('http://localhost:8787/')

客户端使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
const client = hc<AppType>('http://localhost:8787/')

const res = await client.api.books.create[':postId'].$post({
json: {
name: '11',
userId: 1
},
param: {
postId: '111'
}
})

const data = await res.json()

客户端获取接口的入参和响应类型:

1
2
3
4
5
6
7
8
9
import { InferResponseType, InferRequestType } from "hono";

// 获取响应类型
type LoginResponseType = InferResponseType<typeof client.api.users.login.$post>;

// 获取请求参数类型
type LoginRequestType = InferRequestType<typeof client.api.users.login.$post>;
// 只读取请求参数类型中的 json 部分
type LoginRequestBodyType = InferRequestType<typeof client.api.users.login.$post>["json"];