输入搜索…

· server · 9 min 阅读

深入浅出 Hono RPC:构建 100% 类型安全的现代 Web API

深入探讨 Hono 框架独特的端到端(E2E)类型安全与 RPC 机制,包含拦截器设计、全局错误处理及客户端原理解析

深入浅出 Hono RPC:构建 100% 类型安全的现代 Web API

@zetong

前言

在传统的前后端分离开发中,前端通过 Axios 或 Fetch 请求后端 API 时,往往面临着“类型断层”的痛点:后端接口字段一旦变更,前端若未及时同步 TS 定义,极易在运行时悄悄崩溃。

Hono 作为新一代跨运行时的 Web 框架,其看家本领之一便是其独特的 RPC(Remote Procedure Call,远程过程调用) 机制。它无需任何代码生成(Codegen)步骤,仅凭原生 TypeScript 类型推导,即可让前端调用后端接口时像调用本地函数一样流畅。

本文将详细剖析 Hono RPC 的核心工作流、具体代码实现、底层 Proxy 黑魔法,以及如何在 RPC 模式下实现优雅的全局错误拦截。


技术对比:主流“端到端安全”方案

在当前 TypeScript 生态中,实现端到端类型安全主要有以下几种流派:

方案核心机制优点缺点
Hono RPC标准 REST 路由 + JS Proxy标准 REST 接口(兼容性极佳),对前端无侵入,跨平台需要手动导出类型并用 hc 包装
tRPC自定义 Procedure 协议纯 TS 开发体验极佳,生态成熟彻底放弃了标准 REST,非 TS 客户端极难调用
Elysia (Eden)标准 REST 路由 + JS Proxy性能极其恐怖,类型推导极快深度绑定 Bun 运行时
Server Actions编译器代码自动分割(如 Next.js)零 API 概念,全栈无缝开发强耦合,无法作为独立 API 服务供 App 等客户端调用

为什么选择 Hono? Hono 在不发明新协议、不依赖复杂编译魔法的前提下,用最纯粹的 TS 机制,在标准的 RESTful API 之上悄悄铺了一层类型安全的“高速公路”。


核心实战:构建 Hono RPC 服务

我们将结合 Zod 类型校验库,构建一个包含入参校验、状态码返回及类型导出的完整 RPC 服务。

1. 后端:定义带强校验的 API (app.ts)

     import { Hono } from 'hono'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'

const app = new Hono()

// 定义入参的 Zod Schema
const createUserSchema = z.object({
  name: z.string().min(2, '名字至少2个字符'),
  email: z.string().email('邮箱格式不正确'),
})

// 编写 API 路由并注入校验中间件
const route = app.post(
  '/users',
  zValidator('json', createUserSchema), // 运行时拦截非法请求
  async (c) => {
    // 这里的 data 自动获得 { name: string; email: string } 类型
    const { name, email } = c.req.valid('json') 
    
    const newUser = { 
      id: 'user_999', 
      name, 
      email, 
      createdAt: new Date().toISOString() 
    }
    
    return c.json(newUser, 201) 
  }
)

// 【核心】:仅导出路由的 TypeScript 类型(不包含任何实际运行代码,零增重打包)
export type AppType = typeof route

export default app

2. 前端:链式调用与类型同步 (client.ts)

在前端,使用 Hono 内置的 hc (Hono Client) 配合导出的 AppType 进行无缝调用:

     import { hc } from 'hono/client'
import type { AppType } from '../../backend/src/app' // 仅导入 TS 类型

// 初始化客户端并注入泛型
const client = hc<AppType>('http://localhost:3000')

async function registerUser() {
  // 🚀 IDE 将自动提供路由补全,若 json 内缺省或写错字段,编译期直接红线报错
  const res = await client.users.$post({
    json: {
      name: '张三',
      email: 'zhangsan@example.com'
    }
  })

  if (res.ok) {
    // 🚀 data 自动推导为后端 newUser 的完整对象类型
    const data = await res.json()
    console.log(`注册成功,用户 ID:${data.id}`)
  }
}

进阶:统一错误拦截与 Toast 提示

在 RPC 模式下,我们依然可以像以往配置 Axios 拦截器一样,实现全局 Token 注入401 登录失效跳转以及统一的错误 Toast 弹窗通知

Hono 的 hc 在初始化时支持传入一个自定义的 fetch 函数,我们正是通过重写这个 fetch 来实现拦截器逻辑:

自定义全局拦截器客户端 (api.ts)

     import { hc } from 'hono/client'
import type { AppType } from '../../../backend/src/app'
import { toast } from 'your-toast-library' // 支持你项目中的任意 Toast 库

const customFetch = async (input: RequestInfo | URL, init?: RequestInit) => {
  // 1. 【请求拦截】:确保 init 配置项存在
  const requestInit = init || {}

  // 2. 将传入的 headers 统一包装为 Web 标准的 Headers 实例,规避复杂的数组/对象判断
  const headers = new Headers(requestInit.headers)

  // 3. 自动注入 Authorization Token
  const token = localStorage.getItem('token')
  if (token) {
    headers.set('Authorization', `Bearer ${token}`)
  }
  requestInit.headers = headers

  // 发起真正的 Fetch 请求
  const response = await fetch(input, requestInit)

  // 4. 【响应拦截】:拦截非 2xx 的响应并做统一 Toast 提示
  if (!response.ok) {
    if (response.status === 401) {
      toast.error('登录状态已失效,请重新登录')
      // 可在此处编写重定向到登录页的逻辑
    } else if (response.status === 500) {
      toast.error('服务器内部错误,请稍后再试')
    } else {
      try {
        // 使用 .clone() 复制响应流,避免直接消费后影响后续的 res.json() 操作
        const errJson = await response.clone().json()
        toast.error(errJson.message || '操作失败')
      } catch {
        toast.error(`请求失败,状态码: ${response.status}`)
      }
    }
  }

  return response
}

// 导出注入了拦截器的 Hono Client
export const api = hc<AppType>('http://localhost:3000', {
  fetch: customFetch // 注入自定义拦截器
})

底层黑魔法:Hono RPC 是如何工作的?

既然前端我们写的是 client.users.$post(),为什么它能正确发送网络请求,且不影响打包体积?这得益于现代 JavaScript 引擎提供的 Proxy(代理对象) 特性。

当你调用 hc<AppType>() 时,底层并没有生成任何实际的 API 映射代码:

  1. 动态路径拦截hc 返回一个空的 Proxy 对象。当你访问 client.users 时,Proxy 捕获到属性名 users,在内部维护的路径列表中加入 ['users']
  2. 方法与请求执行:当你访问以 $ 符号开头的属性(如 $post)时,Proxy 判定你要发起网络请求。它会立刻停止路径拼接,并自动组装:
    • 将路径拼回 URL:/users
    • 读取你的 jsonquery 传参,自动将其序列化后放入 fetchbody
    • 执行浏览器原生的 window.fetch 发出请求。

这就是为什么 Hono 客户端的 SDK 体积接近为零的原因——它用一段几十行的 Proxy 代码,动态代理了无限可能的路由路径。

总结

Hono RPC 是对现代 Web API 开发流程的一次降维打击。在实际开发中:

  • 它保留了标准 REST API 的解耦性,使得移动端、第三方脚本依旧能够用常规 HTTP 请求轻松调用。
  • 借由 TypeScript 类型传递与 ES6 Proxy 的动态代理,为前端带来了毫无阻碍、零编译、零开销的高速类型同步体验。
  • 通过合理的原生 fetch 拦截器设计,我们得以保持极佳的类型提示的同时,无痛地实现全局错误捕获和用户通知。

参考资料

返回文章列表