Hono 前后端分离系列 08:用 Hono RPC 做类型安全客户端
前后端分离最大的内耗之一,是接口类型不同步。
后端改了:
1 | { |
前端还在用:
1 | user.name |
结果页面空了,或者线上报错。
传统解决方案是写接口文档、手写 TypeScript 类型、生成 OpenAPI client。
这些都可以。
但如果前后端都用 TypeScript,Hono 还有一个很舒服的方案:Hono RPC。
它不是传统意义上那种复杂 RPC 框架,而是通过 Hono route 类型和 hono/client,让前端获得接口路径、入参和响应类型提示。
一、Hono RPC 解决什么问题
Hono 官方文档说,RPC feature 可以在 server 和 client 之间共享 API specifications。
简单说,它解决这几件事:
1 | 前端知道有哪些接口 |
它不要求你放弃 REST。
接口仍然是:
1 | GET /api/posts |
只是前端调用方式变成了类型安全的客户端。
二、后端导出 AppType
先写一个 route。
1 | import { Hono } from 'hono' |
主应用:
1 | import { Hono } from 'hono' |
关键是:
1 | export type AppType = typeof app |
前端会使用这个类型。
三、前端创建 hc client
前端安装 Hono:
1 | pnpm add hono |
创建 client:
1 | import { hc } from 'hono/client' |
调用列表接口:
1 | const res = await client.api.posts.$get() |
调用创建接口:
1 | const res = await client.api.posts.$post({ |
如果你少传字段,TypeScript 会提示。
如果字段类型错了,TypeScript 也会提示。
这就是它的价值。
四、为什么要配合 validator
Hono RPC 的类型推断,和 route 写法、validator、返回响应都有关系。
如果你只写:
1 | const body = await c.req.json() |
前端很难知道 body 应该是什么。
配合 zValidator 后,Hono Client 可以推断 input 类型。
这就是为什么我建议:
1 | 要用 Hono RPC,就认真做 schema |
schema 不是额外负担。
它同时服务三件事:
1 | 后端运行时校验 |
这笔投入很划算。
五、monorepo 里最好用
Hono RPC 最适合 monorepo。
结构类似:
1 | apps/ |
前端可以直接 import 后端导出的类型:
1 | import type { AppType } from '../../api/src' |
实际项目里更推荐通过 workspace 包导出:
1 | { |
然后前端:
1 | import type { AppType } from '@my-app/api' |
注意,只 import type。
不要把后端运行时代码打进前端 bundle。
六、处理 cookie 和 header
如果要带 cookie,创建 client 时可以配置 credentials。
Hono RPC 文档里也提到,可以通过 init 传 fetch 的 RequestInit。
1 | export const client = hc<AppType>(API_BASE_URL, { |
如果使用 Bearer Token:
1 | export function createClient(token?: string) { |
也可以在单个请求里传 header:
1 | await client.api.posts.$get( |
七、Hono RPC 不是银弹
Hono RPC 很好用,但不是所有项目都适合。
适合:
1 | 前后端都用 TypeScript |
不适合:
1 | 开放 API 给第三方 |
如果你要开放接口给外部客户,OpenAPI 仍然更通用。
如果只是自己的 Web 前端调用自己的 Hono API,Hono RPC 非常香。
八、最后的建议
Hono RPC 真正解决的是前后端协作成本。
我的建议是:
1 | 小项目可以先 fetch 封装 |
前后端分离不是前端和后端离得越远越好。
好的分离,是职责分开,但契约紧密。
Hono RPC 正好提供了这种紧密契约。