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 的代码结构:
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => {
return c.text('Hello Hono!')
})
export default app
获取参数:
app.get('/hello/:test',
(c) => {
const test = c.req.param('test')
return c.json({
test
})
})
中间件
类似 KOA 的洋葱圈中间件模式:
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 参数校验
安装依赖:
npm i -S zod @hono/zod-validator
参数校验,支持param、query、json、header等,同时校验配置多个中间件即可。
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
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 类似,支持拆分文件:
// 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
在入口文件中引用:
app.route('/authors', authors).route('/books', books)
异常捕获
可以在处理函数或中间件中抛出异常:
throw new HTTPException(401, { message: "未登录" })
使用 onError 捕获:
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 推导类型。
const app = new Hono().basePath('/api')
const routes = app.route('/authors', authors).route('/books', books)
export type AppType = typeof routes
客户端项目中导入该类型,即可使用:
import { hc } from "hono/client";
import { AppType } from "./server";
const client = hc<AppType>('http://localhost:8787/')
客户端使用:
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()
客户端获取接口的入参和响应类型:
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"];