跳至内容

RPC

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

您可以导出由 Validator 指定的输入类型的类型和由 json() 发出的输出类型。Hono Client 将能够导入它。

注意

为了使 RPC 类型在单仓库中正常工作,在 Client 和 Server 的 tsconfig.json 文件中,在 compilerOptions 中设置 "strict": true了解更多。

服务器

在服务器端,您需要做的就是编写一个验证器并创建一个变量 route。以下示例使用 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 规范。

ts
export type AppType = typeof route

客户端

在客户端,首先导入 hcAppType

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

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

ts
const client = hc<AppType>('https://127.0.0.1:8787/')

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

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

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

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

文件上传

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

状态码

如果您在 c.json() 中显式指定状态码,例如 200404。它将作为类型添加到传递到客户端。

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

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

ts
// client.ts
const client = hc<AppType>('https://127.0.0.1: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
>

未找到

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

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() 并为未找到响应指定状态码。

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
  }
)

路径参数

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

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 指定您要在路径中包含的字符串。

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

标头

您可以将标头附加到请求。

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

要将公共标头添加到所有请求,请将其作为 hc 函数的参数指定。

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

init 选项

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

ts
import { hc } from 'hono/client'

const client = hc<AppType>('https://127.0.0.1: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)设置的内容。

$url()

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

警告

您必须传入一个绝对 URL 才能使此方法起作用。传入一个相对 URL / 将导致以下错误。

未捕获的类型错误:无法构造“URL”:无效的 URL

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

// ✅ Will work as expected
const client = hc<AppType>('https://127.0.0.1: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>('https://127.0.0.1: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 方法

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

在以下 Cloudflare Worker 的示例脚本中,使用服务绑定中的 fetch 方法代替默认的 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),
})

推断

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

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

您还可以使用 React Hook 库,例如 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

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

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

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

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 创建一个新的客户端,并像往常一样使用它。

已知问题

IDE 性能

使用 RPC 时,路由越多,IDE 速度就会越慢。造成这种情况的主要原因之一是执行了大量类型实例化来推断应用程序的类型。

例如,假设您的应用程序有这样的路由

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

Hono 将推断出以下类型

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 的速度。

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

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

编译您的客户端(包括服务器应用程序)可以获得最佳性能。将以下代码放在您的项目中

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 来获取已计算出类型的客户端。

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

如果您的项目是单仓库,此解决方案非常适合。使用像 turborepo 这样的工具,您可以轻松地将服务器项目和客户端项目分开,并获得更好的集成,从而管理它们之间的依赖关系。这里有一个 工作示例

如果您的客户端和服务器位于同一个项目中,tsc项目引用 是一个不错的选择。

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

手动指定类型参数

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

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

仅指定单个类型参数会影响性能,而如果您有很多路由,则可能需要花费大量时间和精力。

将您的应用程序和客户端拆分为多个文件

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

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 不需要一次实例化所有路由的类型。

在 MIT 许可下发布。