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 app

1.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 客户端的关键点:

  1. 类型安全:从 AppType 自动推导请求参数和响应体类型
  2. 类型提取:使用 InferResponseTypeInferRequestType 提取类型
  3. 错误处理:检查 res.ok,解析错误响应
  4. 全局配置:在创建客户端时配置 headers、fetch 拦截器
  5. 框架无关:可以配合 React Query、Vue Query 或任何前端框架使用

一句话带走:

Hono RPC 客户端让前后端类型完全同步,后端改动,前端 TypeScript 立即报错,彻底消除接口联调成本。