RPC
RPC 功能允許在伺服器和客戶端之間共享 API 規格。
您可以導出由驗證器指定的輸入類型和 json()
發出的輸出類型。 Hono Client 將能夠導入它。
注意
為了使 RPC 類型在單體倉庫中正常工作,請在客戶端和伺服器的 tsconfig.json 檔案中,在 compilerOptions
中設定 "strict": true
。閱讀更多。
伺服器
您在伺服器端需要做的就是編寫一個驗證器並建立一個變數 route
。以下範例使用 Zod 驗證器。
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 規格。
export type AppType = typeof route
客戶端
在客戶端,首先導入 hc
和 AppType
。
import { AppType } from '.'
import { hc } from 'hono/client'
hc
是一個用於建立客戶端的函式。將 AppType
作為泛型傳遞,並將伺服器 URL 指定為引數。
const client = hc<AppType>('https://127.0.0.1:8787/')
呼叫 client.{path}.{method}
並將您想要發送到伺服器的資料作為引數傳遞。
const res = await client.posts.$post({
form: {
title: 'Hello',
body: 'Hono is a cool project',
},
})
res
與 "fetch" Response 相容。您可以使用 res.json()
從伺服器檢索資料。
if (res.ok) {
const data = await res.json()
console.log(data.message)
}
檔案上傳
目前,客戶端不支援檔案上傳。
狀態碼
如果您在 c.json()
中明確指定狀態碼,例如 200
或 404
。它將作為傳遞給客戶端的類型新增。
// 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
您可以通過狀態碼取得資料。
// 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()
用於找不到的回應。客戶端從伺服器取得的資料無法正確推斷。
// 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()
並指定找不到回應的狀態碼。
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
}
)
路徑參數
您也可以處理包含路徑參數的路由。
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
指定要包含在路徑中的字串。
const res = await client.posts[':id'].$get({
param: {
id: '123',
},
query: {},
})
標頭
您可以將標頭附加到請求。
const res = await client.search.$get(
{
//...
},
{
headers: {
'X-Custom-Header': 'Here is Hono Client',
'X-User-Agent': 'hc',
},
}
)
若要將通用標頭新增到所有請求,請將其指定為 hc
函式的引數。
const client = hc<AppType>('/api', {
headers: {
Authorization: 'Bearer TOKEN',
},
})
init
選項
您可以將 fetch 的 RequestInit
物件作為 init
選項傳遞給請求。以下是中止請求的範例。
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 /
將會導致以下錯誤。
Uncaught TypeError: Failed to construct 'URL': Invalid URL
// ❌ 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()
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 範例指令碼中,使用 Service Bindings 的 fetch
方法而不是預設的 fetch
。
# wrangler.toml
services = [
{ binding = "AUTH", service = "auth-service" },
]
// src/client.ts
const client = hc<CreateProfileType>('/', {
fetch: c.env.AUTH.fetch.bind(c.env.AUTH),
})
推斷
使用 InferRequestType
和 InferResponseType
來了解要請求的物件類型和要傳回的物件類型。
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。
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
如果應用程式較大,例如 建立較大型應用程式 中提到的範例,您需要注意類型推斷。一種簡單的方法是鏈接處理常式,以便始終推斷類型。
// 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
// 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
然後,您可以像往常一樣匯入子路由器,並確保也鏈接其處理常式,因為在這種情況下這是應用程式的頂級,這是我們要匯出的類型。
// 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 就會變得越慢。其中一個主要原因是執行了大量的類型實例化來推斷應用程式的類型。
例如,假設您的應用程式具有如下的路由
// app.ts
export const app = new Hono().get('foo/:id', (c) =>
c.json({ ok: true }, 200)
)
Hono 將推斷類型如下
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 快得多!
編譯包含伺服器應用程式的客戶端可為您提供最佳效能。將以下程式碼放入您的專案中
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
來取得已計算類型的客戶端。
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
的專案參照是一個不錯的選擇。
您也可以使用 concurrently
或 npm-run-all
等工具手動協調您的建置流程。
手動指定類型引數
這有點麻煩,但是您可以手動指定類型引數以避免類型實例化。
const app = new Hono().get<'foo/:id'>('foo/:id', (c) =>
c.json({ ok: true }, 200)
)
僅指定單個類型引數就可以在效能上有所差異,但是如果您有很多路由,則可能需要花費大量的時間和精力。
將您的應用程式和客戶端分成多個檔案
如在較大的應用程式中使用 RPC中所述,您可以將應用程式分成多個應用程式。您也可以為每個應用程式建立一個客戶端
// 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
不需要一次實例化所有路由的類型。