23.06-Hono RPC客户端
要点
- Hono RPC 客户端(hc)是前后端类型安全的核心
- 从后端 AppType 自动推导出前端调用类型,后端改动前端立即感知
- 支持请求参数、响应体、错误处理的完整类型推导
- 可以配合任何前端框架使用(React、Vue、Svelte、纯 TS)
内容
1. RPC 客户端基础
1.1 后端导出 AppType
// apps/api/src/index.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
// 必须链式调用,类型推导才能正常工作
app
.get('/api/users/:id', async (c) => {
const id = c.req.param('id')
return c.json({
id,
name: 'Alice',
email: '[email protected]',
})
})
.post(
'/api/chat',
zValidator('json', z.object({
message: z.string(),
})),
async (c) => {
const { message } = c.req.valid('json')
return c.json({ reply: `你说了:${message}` })
}
)
export type AppType = typeof app
export default app1.2 前端创建 RPC 客户端
// apps/web/lib/api.ts
import { hc } from 'hono/client'
import type { AppType } from '@api/src/index'
export const api = hc<AppType>(process.env.NEXT_PUBLIC_API_URL!)现在 api 对象有完整的类型提示:
api.api.users[':id'].$get()- GET 请求api.api.chat.$post()- POST 请求
2. 类型推导详解
2.1 请求参数类型
// GET /api/users/:id
const res = await api.api.users[':id'].$get({
param: { id: '123' }, // param 类型自动推导为 { id: string }
})
// GET /api/users?page=1&limit=10
const res = await api.api.users.$get({
query: { // query 类型自动推导
page: '1',
limit: '10',
},
})
// POST /api/chat
const res = await api.api.chat.$post({
json: { // json 类型从 zValidator 推导
message: '你好',
},
})2.2 响应体类型
const res = await api.api.users[':id'].$get({
param: { id: '123' },
})
// res.json() 的返回类型自动推导
const user = await res.json()
// user 的类型是 { id: string, name: string, email: string }
console.log(user.name) // ✅ 有类型提示
console.log(user.age) // ❌ TypeScript 报错2.3 使用 InferResponseType
import type { InferResponseType } from 'hono/client'
import { api } from './api'
// 从 RPC 方法提取响应类型
type UserResponse = InferResponseType<typeof api.api.users[':id'].$get>
// { id: string, name: string, email: string }
type ChatResponse = InferResponseType<typeof api.api.chat.$post>
// { reply: string }
// 使用提取的类型
function processUser(user: UserResponse) {
console.log(user.name)
}2.4 使用 InferRequestType
import type { InferRequestType } from 'hono/client'
import { api } from './api'
// 提取请求参数类型
type ChatRequestArgs = InferRequestType<typeof api.api.chat.$post>
// { json: { message: string } }
type UserRequestArgs = InferRequestType<typeof api.api.users[':id'].$get>
// { param: { id: string } }
// 使用提取的类型
function sendChat(args: ChatRequestArgs) {
api.api.chat.$post(args)
}3. 高级用法
3.1 自定义 Header
const res = await api.api.users[':id'].$get({
param: { id: '123' },
headers: {
'Authorization': 'Bearer token123',
'X-Custom-Header': 'value',
},
})3.2 全局配置
export const api = hc<AppType>(process.env.NEXT_PUBLIC_API_URL!, {
headers: {
'Content-Type': 'application/json',
},
fetch: async (input, init) => {
// 自定义 fetch 逻辑
const token = localStorage.getItem('token')
const headers = new Headers(init?.headers)
if (token) {
headers.set('Authorization', `Bearer ${token}`)
}
return fetch(input, { ...init, headers })
},
})3.3 请求拦截器
export const api = hc<AppType>(process.env.NEXT_PUBLIC_API_URL!, {
fetch: async (input, init) => {
console.log('Request:', input, init)
const res = await fetch(input, init)
console.log('Response:', res.status)
return res
},
})4. 错误处理
4.1 检查响应状态
const res = await api.api.users[':id'].$get({
param: { id: '123' },
})
if (!res.ok) {
// res.status 是 HTTP 状态码
// res.statusText 是状态文本
throw new Error(`Request failed: ${res.status} ${res.statusText}`)
}
const user = await res.json()4.2 解析错误响应
const res = await api.api.chat.$post({
json: { message: '你好' },
})
if (!res.ok) {
// 尝试解析错误响应
const error = await res.json().catch(() => null)
if (error?.message) {
throw new Error(error.message)
}
throw new Error(`Request failed: ${res.status}`)
}4.3 统一错误处理
// src/lib/api-client.ts
export class ApiError extends Error {
constructor(
public status: number,
public message: string,
public data?: any
) {
super(message)
}
}
export async function handleResponse<T>(res: Response): Promise<T> {
if (!res.ok) {
const error = await res.json().catch(() => null)
throw new ApiError(
res.status,
error?.message || `Request failed with status ${res.status}`,
error
)
}
return res.json()
}
// 使用
const res = await api.api.users[':id'].$get({ param: { id: '123' } })
const user = await handleResponse(res)5. 配合 React Query
// src/hooks/useUser.ts
import { useQuery } from '@tanstack/react-query'
import { api } from '@/lib/api'
export function useUser(userId: string) {
return useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const res = await api.api.users[':id'].$get({
param: { id: userId },
})
if (!res.ok) {
throw new Error('Failed to fetch')
}
return res.json()
},
})
}
// 使用
function UserProfile({ userId }: { userId: string }) {
const { data: user, isLoading, error } = useUser(userId)
if (isLoading) return <div>加载中...</div>
if (error) return <div>加载失败</div>
return <h1>{user.name}</h1>
}6. 配合 Vue Query
// src/composables/useUser.ts
import { useQuery } from '@tanstack/vue-query'
import { api } from '@/lib/api'
export function useUser(userId: string) {
return useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const res = await api.api.users[':id'].$get({
param: { id: userId },
})
if (!res.ok) {
throw new Error('Failed to fetch')
}
return res.json()
},
})
}7. 实战:完整的 API 客户端封装
// src/lib/api.ts
import { hc } from 'hono/client'
import type { AppType } from '@api/src/index'
export class ApiError extends Error {
constructor(
public status: number,
public message: string,
public data?: any
) {
super(message)
}
}
async function handleResponse<T>(res: Response): Promise<T> {
if (!res.ok) {
const error = await res.json().catch(() => null)
throw new ApiError(
res.status,
error?.message || `Request failed with status ${res.status}`,
error
)
}
return res.json()
}
export const api = hc<AppType>(process.env.NEXT_PUBLIC_API_URL!, {
headers: {
'Content-Type': 'application/json',
},
fetch: async (input, init) => {
const token = localStorage.getItem('token')
const headers = new Headers(init?.headers)
if (token) {
headers.set('Authorization', `Bearer ${token}`)
}
const res = await fetch(input, { ...init, headers })
if (res.status === 401) {
localStorage.removeItem('token')
window.location.href = '/login'
}
return res
},
})
// 封装常用方法
export const apiClient = {
async getUser(userId: string) {
const res = await api.api.users[':id'].$get({
param: { id: userId },
})
return handleResponse(res)
},
async sendChat(message: string) {
const res = await api.api.chat.$post({
json: { message },
})
return handleResponse(res)
},
}// 使用
import { apiClient } from '@/lib/api'
const user = await apiClient.getUser('123')
const chat = await apiClient.sendChat('你好')8. 小结
Hono RPC 客户端的关键点:
- 类型安全:从 AppType 自动推导请求参数和响应体类型
- 类型提取:使用
InferResponseType和InferRequestType提取类型 - 错误处理:检查
res.ok,解析错误响应 - 全局配置:在创建客户端时配置 headers、fetch 拦截器
- 框架无关:可以配合 React Query、Vue Query 或任何前端框架使用
一句话带走:
Hono RPC 客户端让前后端类型完全同步,后端改动,前端 TypeScript 立即报错,彻底消除接口联调成本。