Skip to content

RPC

RPC 功能允许在服务器和客户端之间共享 API 规范。

¥The RPC feature allows sharing of the API specifications between the server and the client.

首先,从你的服务器代码中导出 typeof 你的 Hono 应用(通常称为 AppType)— 或者只是你希望客户端可用的路由。

¥First, export the typeof your Hono app (commonly called AppType)—or just the routes you want available to the client—from your server code.

通过接受 AppType 作为通用参数,Hono 客户端可以推断验证器指定的输入类型和返回 c.json() 的处理程序发出的输出类型。

¥By accepting AppType as a generic parameter, the Hono Client can infer both the input type(s) specified by the Validator, and the output type(s) emitted by handlers returning c.json().

此时,从中间件返回的响应是 [客户端无法推断。](https://github.com/honojs/hono/issues/2719)

¥[!NOTE] At this time, responses returned from middleware are not inferred by the client.

为了使 RPC 类型在 monorepo 中正常工作,在客户端和服务器的 tsconfig.json 文件中,在 `compilerOptions` 中设置 `"strict": true`。[阅读更多。](https://github.com/honojs/hono/issues/2270#issuecomment-2143745118)

¥[!NOTE]\ For the RPC types to work properly in a monorepo, in both the Client's and Server's tsconfig.json files, set "strict": true in compilerOptions. Read more.

服务器

¥Server

你需要在服务器端做的就是编写一个验证器并创建一个变量 route。以下示例使用 Zod Validator

¥All you need to do on the server side is to write a validator and create a variable route. The following example uses Zod Validator.

ts
const route = app.post(
  '/posts',
  zValidator(
    'form',
    z.object({
      title: z.string(),
      body: z.string(),
    })
  ),
  (c) => {
    // ...
    return c.json(
      {
        ok: true,
        message: 'Created!',
      },
      201
    )
  }
)

然后,导出类型以与客户端共享 API 规范。

¥Then, export the type to share the API spec with the Client.

ts
export type AppType = typeof route

客户端

¥Client

在客户端,首先导入 hcAppType

¥On the Client side, import hc and AppType first.

ts
import { AppType } from '.'
import { hc } from 'hono/client'

hc 是一个用于创建客户端的函数。将 AppType 作为泛型传递,并将服务器 URL 指定为参数。

¥hc is a function to create a client. Pass AppType as Generics and specify the server URL as an argument.

ts
const client = hc<AppType>('http://localhost:8787/')

调用 client.{path}.{method} 并将你希望发送到服务器的数据作为参数传递。

¥Call client.{path}.{method} and pass the data you wish to send to the server as an argument.

ts
const res = await client.posts.$post({
  form: {
    title: 'Hello',
    body: 'Hono is a cool project',
  },
})

res 与 "fetch" 响应兼容。你可以使用 res.json() 从服务器检索数据。

¥The res is compatible with the "fetch" Response. You can retrieve data from the server with res.json().

ts
if (res.ok) {
  const data = await res.json()
  console.log(data.message)
}

文件上传

目前,客户端不支持文件上传。

¥Currently, the client does not support file uploading.

状态代码

¥Status code

如果你在 c.json() 中明确指定了状态代码,比如 200404。它将被添加为传递给客户端的类型。

¥If you explicitly specify the status code, such as 200 or 404, in c.json(). It will be added as a type for passing to the client.

ts
// server.ts
const app = new Hono().get(
  '/posts',
  zValidator(
    'query',
    z.object({
      id: z.string(),
    })
  ),
  async (c) => {
    const { id } = c.req.valid('query')
    const post: Post | undefined = await getPost(id)

    if (post === undefined) {
      return c.json({ error: 'not found' }, 404) // Specify 404
    }

    return c.json({ post }, 200) // Specify 200
  }
)

export type AppType = typeof app

你可以通过状态代码获取数据。

¥You can get the data by the status code.

ts
// client.ts
const client = hc<AppType>('http://localhost:8787/')

const res = await client.posts.$get({
  query: {
    id: '123',
  },
})

if (res.status === 404) {
  const data: { error: string } = await res.json()
  console.log(data.error)
}

if (res.ok) {
  const data: { post: Post } = await res.json()
  console.log(data.post)
}

// { post: Post } | { error: string }
type ResponseType = InferResponseType<typeof client.posts.$get>

// { post: Post }
type ResponseType200 = InferResponseType<
  typeof client.posts.$get,
  200
>

未找到

¥Not Found

如果要使用客户端,则不应将 c.notFound() 用于未找到响应。无法正确推断客户端从服务器获取的数据。

¥If you want to use a client, you should not use c.notFound() for the Not Found response. The data that the client gets from the server cannot be inferred correctly.

ts
// server.ts
export const routes = new Hono().get(
  '/posts',
  zValidator(
    'query',
    z.object({
      id: z.string(),
    })
  ),
  async (c) => {
    const { id } = c.req.valid('query')
    const post: Post | undefined = await getPost(id)

    if (post === undefined) {
      return c.notFound() // ❌️
    }

    return c.json({ post })
  }
)

// client.ts
import { hc } from 'hono/client'

const client = hc<typeof routes>('/')

const res = await client.posts[':id'].$get({
  param: {
    id: '123',
  },
})

const data = await res.json() // 🙁 data is unknown

请使用 c.json() 并指定未找到响应的状态代码。

¥Please use c.json() and specify the status code for the Not Found Response.

ts
export const routes = new Hono().get(
  '/posts',
  zValidator(
    'query',
    z.object({
      id: z.string(),
    })
  ),
  async (c) => {
    const { id } = c.req.valid('query')
    const post: Post | undefined = await getPost(id)

    if (post === undefined) {
      return c.json({ error: 'not found' }, 404) // Specify 404
    }

    return c.json({ post }, 200) // Specify 200
  }
)

路径参数

¥Path parameters

你还可以处理包含路径参数的路由。

¥You can also handle routes that include path parameters.

ts
const route = app.get(
  '/posts/:id',
  zValidator(
    'query',
    z.object({
      page: z.string().optional(),
    })
  ),
  (c) => {
    // ...
    return c.json({
      title: 'Night',
      body: 'Time to sleep',
    })
  }
)

使用 param 指定你想要包含在路径中的字符串。

¥Specify the string you want to include in the path with param.

ts
const res = await client.posts[':id'].$get({
  param: {
    id: '123',
  },
  query: {},
})

Headers

你可以将标头附加到请求中。

¥You can append the headers to the request.

ts
const res = await client.search.$get(
  {
    //...
  },
  {
    headers: {
      'X-Custom-Header': 'Here is Hono Client',
      'X-User-Agent': 'hc',
    },
  }
)

要为所有请求添加通用标头,请将其作为参数指定给 hc 函数。

¥To add a common header to all requests, specify it as an argument to the hc function.

ts
const client = hc<AppType>('/api', {
  headers: {
    Authorization: 'Bearer TOKEN',
  },
})

init 选项

¥init option

你可以将 fetch 的 RequestInit 对象作为 init 选项传递给请求。下面是中止请求的示例。

¥You can pass the fetch's RequestInit object to the request as the init option. Below is an example of aborting a Request.

ts
import { hc } from 'hono/client'

const client = hc<AppType>('http://localhost:8787/')

const abortController = new AbortController()
const res = await client.api.posts.$post(
  {
    json: {
      // Request body
    },
  },
  {
    // RequestInit object
    init: {
      signal: abortController.signal,
    },
  }
)

// ...

abortController.abort()

信息

init 定义的 RequestInit 对象具有最高优先级。它可用于覆盖由其他选项(如 body | method | headers)设置的内容。

¥A RequestInit object defined by init takes the highest priority. It could be used to overwrite things set by other options like body | method | headers.

$url()

你可以使用 $url() 获取用于访问端点的 URL 对象。

¥You can get a URL object for accessing the endpoint by using $url().

警告

你必须传入绝对 URL 才能使其工作。传入相对 URL / 将导致以下错误。

¥You have to pass in an absolute URL for this to work. Passing in a relative URL / will result in the following error.

Uncaught TypeError: Failed to construct 'URL': Invalid URL

ts
// ❌ Will throw error
const client = hc<AppType>('/')
client.api.post.$url()

// ✅ Will work as expected
const client = hc<AppType>('http://localhost:8787/')
client.api.post.$url()
ts
const route = app
  .get('/api/posts', (c) => c.json({ posts }))
  .get('/api/posts/:id', (c) => c.json({ post }))

const client = hc<typeof route>('http://localhost:8787/')

let url = client.api.posts.$url()
console.log(url.pathname) // `/api/posts`

url = client.api.posts[':id'].$url({
  param: {
    id: '123',
  },
})
console.log(url.pathname) // `/api/posts/123`

自定义 fetch 方法

¥Custom fetch method

你可以设置自定义 fetch 方法。

¥You can set the custom fetch method.

在下面的 Cloudflare Worker 示例脚本中,使用了 Service Bindings 的 fetch 方法,而不是默认的 fetch

¥In the following example script for Cloudflare Worker, the Service Bindings' fetch method is used instead of the default fetch.

toml
# wrangler.toml
services = [
  { binding = "AUTH", service = "auth-service" },
]
ts
// src/client.ts
const client = hc<CreateProfileType>('/', {
  fetch: c.env.AUTH.fetch.bind(c.env.AUTH),
})

推断

¥Infer

使用 InferRequestTypeInferResponseType 来了解要请求的对象类型和要返回的对象类型。

¥Use InferRequestType and InferResponseType to know the type of object to be requested and the type of object to be returned.

ts
import type { InferRequestType, InferResponseType } from 'hono/client'

// InferRequestType
const $post = client.todo.$post
type ReqType = InferRequestType<typeof $post>['form']

// InferResponseType
type ResType = InferResponseType<typeof $post>

使用 SWR

¥Using SWR

你还可以使用 React Hook 库,例如 SWR

¥You can also use a React Hook library such as SWR.

tsx
import useSWR from 'swr'
import { hc } from 'hono/client'
import type { InferRequestType } from 'hono/client'
import { AppType } from '../functions/api/[[route]]'

const App = () => {
  const client = hc<AppType>('/api')
  const $get = client.hello.$get

  const fetcher =
    (arg: InferRequestType<typeof $get>) => async () => {
      const res = await $get(arg)
      return await res.json()
    }

  const { data, error, isLoading } = useSWR(
    'api-hello',
    fetcher({
      query: {
        name: 'SWR',
      },
    })
  )

  if (error) return <div>failed to load</div>
  if (isLoading) return <div>loading...</div>

  return <h1>{data?.message}</h1>
}

export default App

在更大的应用中使用 RPC

¥Using RPC with larger applications

对于较大的应用,例如 构建更大的应用 中提到的示例,需要注意推断的类型。一种简单的方法是链接处理程序,以便始终推断类型。

¥In the case of a larger application, such as the example mentioned in Building a larger application, you need to be careful about the type of inference. A simple way to do this is to chain the handlers so that the types are always inferred.

ts
// authors.ts
import { Hono } from 'hono'

const app = new Hono()
  .get('/', (c) => c.json('list authors'))
  .post('/', (c) => c.json('create an author', 201))
  .get('/:id', (c) => c.json(`get ${c.req.param('id')}`))

export default app
ts
// 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

然后,你可以像平常一样导入子路由,并确保也链接它们的处理程序,因为这是应用的顶层(在本例中),这是我们想要导出的类型。

¥You can then import the sub-routers as you usually would, and make sure you chain their handlers as well, since this is the top level of the app in this case, this is the type we'll want to export.

ts
// index.ts
import { Hono } from 'hono'
import authors from './authors'
import books from './books'

const app = new Hono()

const routes = app.route('/authors', authors).route('/books', books)

export default app
export type AppType = typeof routes

你现在可以使用已注册的 AppType 创建新客户端并像平常一样使用它。

¥You can now create a new client using the registered AppType and use it as you would normally.

已知问题

¥Known issues

IDE 性能

¥IDE performance

使用 RPC 时,路由越多,IDE 就会变得越慢。其中一个主要原因是执行了大量类型实例化来推断应用的类型。

¥When using RPC, the more routes you have, the slower your IDE will become. One of the main reasons for this is that massive amounts of type instantiations are executed to infer the type of your app.

例如,假设你的应用有这样的路由:

¥For example, suppose your app has a route like this:

ts
// app.ts
export const app = new Hono().get('foo/:id', (c) =>
  c.json({ ok: true }, 200)
)

Hono 将推断类型如下:

¥Hono will infer the type as follows:

ts
export const app = Hono<BlankEnv, BlankSchema, '/'>().get<
  'foo/:id',
  'foo/:id',
  JSONRespondReturn<{ ok: boolean }, 200>,
  BlankInput,
  BlankEnv
>('foo/:id', (c) => c.json({ ok: true }, 200))

这是单个路由的类型实例化。虽然用户不需要手动编写这些类型参数,这是一件好事,但众所周知类型实例化需要花费很多时间。每次使用应用时,IDE 中使用的 tsserver 都会执行这项耗时的任务。如果你有很多路由,这会显著降低你的 IDE 速度。

¥This is a type instantiation for a single route. While the user doesn't need to write these type arguments manually, which is a good thing, it's known that type instantiation takes much time. tsserver used in your IDE does this time consuming task every time you use the app. If you have a lot of routes, this can slow down your IDE significantly.

但是,我们有一些技巧可以缓解这个问题。

¥However, we have some tips to mitigate this issue.

Hono 版本不匹配

¥Hono version mismatch

如果你的后端与前端分开并且位于不同的目录中,则需要确保 Hono 版本匹配。如果你在后端使用一个 Hono 版本,在前端使用另一个版本,则会遇到诸如 "类型实例化过深且可能无限" 之类的问题。

¥If your backend is separate from the frontend and lives in a different directory, you need to ensure that the Hono versions match. If you use one Hono version on the backend and another on the frontend, you'll run into issues such as "Type instantiation is excessively deep and possibly infinite".

hono-version-mismatch

TypeScript 项目参考

¥TypeScript project references

Hono 版本不匹配 的情况一样,如果你的后端和前端是分开的,你会遇到问题。如果要在前端从后端(例如 AppType)访问代码,则需要使用 项目参考。TypeScript 的项目引用允许一个 TypeScript 代码库访问和使用来自另一个 TypeScript 代码库的代码。(来源:Hono RPC 和 TypeScript 项目参考)。

¥Like in the case of Hono version mismatch, you'll run into issues if your backend and frontend are separate. If you want to access code from the backend (AppType, for example) on the frontend, you need to use project references. TypeScript's project references allow one TypeScript codebase to access and use code from another TypeScript codebase. (source: Hono RPC And TypeScript Project References).

使用代码前先编译(推荐)

¥Compile your code before using it (recommended)

tsc 可以在编译时执行类型实例化等繁重任务!然后,每次使用 tsserver 时,它不需要实例化所有类型参数。它将使你的 IDE 更快!

¥tsc can do heavy tasks like type instantiation at compile time! Then, tsserver doesn't need to instantiate all the type arguments every time you use it. It will make your IDE a lot faster!

编译包括服务器应用在内的客户端可为你提供最佳性能。将以下代码放入你的项目中:

¥Compiling your client including the server app gives you the best performance. Put the following code in your project:

ts
import { app } from './app'
import { hc } from 'hono/client'

// this is a trick to calculate the type when compiling
const client = hc<typeof app>('')
export type Client = typeof client

export const hcWithType = (...args: Parameters<typeof hc>): Client =>
  hc<typeof app>(...args)

编译后,你可以使用 hcWithType 而不是 hc 来获取已计算类型的客户端。

¥After compiling, you can use hcWithType instead of hc to get the client with the type already calculated.

ts
const client = hcWithType('http://localhost:8787/')
const res = await client.posts.$post({
  form: {
    title: 'Hello',
    body: 'Hono is a cool project',
  },
})

如果你的项目是 monorepo,则此解决方案确实很合适。使用 turborepo 这样的工具,你可以轻松地分离服务器项目和客户端项目,并通过管理它们之间的依赖获得更好的集成。这是 工作示例

¥If your project is a monorepo, this solution does fit well. Using a tool like turborepo, you can easily separate the server project and the client project and get better integration managing dependencies between them. Here is a working example.

你还可以使用 concurrentlynpm-run-all 等工具手动协调构建过程。

¥You can also coordinate your build process manually with tools like concurrently or npm-run-all.

手动指定类型参数

¥Specify type arguments manually

这有点麻烦,但你可以手动指定类型参数以避免类型实例化。

¥This is a bit cumbersome, but you can specify type arguments manually to avoid type instantiation.

ts
const app = new Hono().get<'foo/:id'>('foo/:id', (c) =>
  c.json({ ok: true }, 200)
)

仅指定单一类型参数会对性能产生影响,而如果你有很多路由,则可能会花费大量时间和精力。

¥Specifying just single type argument make a difference in performance, while it may take you a lot of time and effort if you have a lot of routes.

将你的应用和客户端拆分为多个文件

¥Split your app and client into multiple files

在更大的应用中使用 RPC 中所述,你可以将应用拆分为多个应用。你还可以为每个应用创建一个客户端:

¥As described in Using RPC with larger applications, you can split your app into multiple apps. You can also create a client for each app:

ts
// authors-cli.ts
import { app as authorsApp } from './authors'
import { hc } from 'hono/client'

const authorsClient = hc<typeof authorsApp>('/authors')

// books-cli.ts
import { app as booksApp } from './books'
import { hc } from 'hono/client'

const booksClient = hc<typeof booksApp>('/books')

这样,tsserver 就不需要一次实例化所有路由的类型了。

¥This way, tsserver doesn't need to instantiate types for all routes at once.

Hono v4.7 中文网 - 粤ICP备13048890号