Hono Stacks

Hono 让简单的事情变得更简单,让困难的事情也变得简单。它不仅仅适合返回 JSON,还非常适合构建包括 REST API 服务端和客户端在内的全栈应用。

RPC

Hono 的 RPC 功能让你可以在几乎不改动代码的情况下共享 API 规范。由 hc 生成的客户端会读取这些规范,并以类型安全的方式访问端点。

以下库使这成为可能:

我们可以将这些组件的组合称为 Hono Stack。现在,让我们用它来创建一个 API 服务端和客户端。

编写 API

首先,编写一个接收 GET 请求并返回 JSON 的端点。

  1. import { Hono } from 'hono'
  2. const app = new Hono()
  3. app.get('/hello', (c) => {
  4. return c.json({
  5. message: `Hello!`,
  6. })
  7. })

使用 Zod 验证

用 Zod 验证查询参数的值。

Hono Stacks - 图1

  1. import { zValidator } from '@hono/zod-validator'
  2. import { z } from 'zod'
  3. app.get(
  4. '/hello',
  5. zValidator(
  6. 'query',
  7. z.object({
  8. name: z.string(),
  9. })
  10. ),
  11. (c) => {
  12. const { name } = c.req.valid('query')
  13. return c.json({
  14. message: `Hello! ${name}`,
  15. })
  16. }
  17. )

共享类型

为了生成端点的类型规范,需要导出它的类型。

警告 要让 RPC 正确推断路由,所有包含的方法必须链式调用,并且端点或 app 的类型必须从声明的变量中推断。更多细节参见 Best Practices for RPC

  1. const route = app.get(
  2. '/hello',
  3. zValidator(
  4. 'query',
  5. z.object({
  6. name: z.string(),
  7. })
  8. ),
  9. (c) => {
  10. const { name } = c.req.valid('query')
  11. return c.json({
  12. message: `Hello! ${name}`,
  13. })
  14. }
  15. )
  16. export type AppType = typeof route

客户端

接下来是客户端实现。通过将 AppType 类型作为泛型传入 hc 来创建客户端对象。这样智能补全功能就会自动生效,端点路径和请求类型会被提示。

Hono Stacks - 图2

  1. import { AppType } from './server'
  2. import { hc } from 'hono/client'
  3. const client = hc<AppType>('/api')
  4. const res = await client.hello.$get({
  5. query: {
  6. name: 'Hono',
  7. },
  8. })

Response 与 fetch API 兼容,但 json() 获取的数据具有类型定义。

Hono Stacks - 图3

  1. const data = await res.json()
  2. console.log(`${data.message}`)

共享 API 规范意味着你可以随时感知服务端的变化。

Hono Stacks - 图4

搭配 React

你可以使用 React 在 Cloudflare Pages 上构建应用。

API 服务端:

  1. // functions/api/[[route]].ts
  2. import { Hono } from 'hono'
  3. import { handle } from 'hono/cloudflare-pages'
  4. import { z } from 'zod'
  5. import { zValidator } from '@hono/zod-validator'
  6. const app = new Hono()
  7. const schema = z.object({
  8. id: z.string(),
  9. title: z.string(),
  10. })
  11. type Todo = z.infer<typeof schema>
  12. const todos: Todo[] = []
  13. const route = app
  14. .post('/todo', zValidator('form', schema), (c) => {
  15. const todo = c.req.valid('form')
  16. todos.push(todo)
  17. return c.json({
  18. message: 'created!',
  19. })
  20. })
  21. .get((c) => {
  22. return c.json({
  23. todos,
  24. })
  25. })
  26. export type AppType = typeof route
  27. export const onRequest = handle(app, '/api')

使用 React 和 React Query 的客户端:

  1. // src/App.tsx
  2. import {
  3. useQuery,
  4. useMutation,
  5. QueryClient,
  6. QueryClientProvider,
  7. } from '@tanstack/react-query'
  8. import { AppType } from '../functions/api/[[route]]'
  9. import { hc, InferResponseType, InferRequestType } from 'hono/client'
  10. const queryClient = new QueryClient()
  11. const client = hc<AppType>('/api')
  12. export default function App() {
  13. return (
  14. <QueryClientProvider client={queryClient}>
  15. <Todos />
  16. </QueryClientProvider>
  17. )
  18. }
  19. const Todos = () => {
  20. const query = useQuery({
  21. queryKey: ['todos'],
  22. queryFn: async () => {
  23. const res = await client.todo.$get()
  24. return await res.json()
  25. },
  26. })
  27. const $post = client.todo.$post
  28. const mutation = useMutation<
  29. InferResponseType<typeof $post>,
  30. Error,
  31. InferRequestType<typeof $post>['form']
  32. >({
  33. mutationFn: async (todo) => {
  34. const res = await $post({
  35. form: todo,
  36. })
  37. return await res.json()
  38. },
  39. onSuccess: async () => {
  40. queryClient.invalidateQueries({ queryKey: ['todos'] })
  41. },
  42. onError: (error) => {
  43. console.log(error)
  44. },
  45. })
  46. return (
  47. <div>
  48. <button
  49. onClick={() => {
  50. mutation.mutate({
  51. id: Date.now().toString(),
  52. title: 'Write code',
  53. })
  54. }}
  55. >
  56. Add Todo
  57. </button>
  58. <ul>
  59. {query.data?.todos.map((todo) => (
  60. <li key={todo.id}>{todo.title}</li>
  61. ))}
  62. </ul>
  63. </div>
  64. )
  65. }