This is the full developer documentation for Hono. # Start of Hono documentation # Hono Hono - _**在日语中意为火焰🔥**_ - 是一个小型、简单且超快的 Web 框架,基于 Web Standards 构建。 它可以在任何 JavaScript 运行时上运行:Cloudflare Workers、Fastly Compute、Deno、Bun、Vercel、Netlify、AWS Lambda、Lambda@Edge 和 Node.js。 快速,但不止于快速。 ```ts twoslash import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.text('Hono!')) export default app ``` ## 快速开始 只需运行: ::: code-group ```sh [npm] npm create hono@latest ``` ```sh [yarn] yarn create hono ``` ```sh [pnpm] pnpm create hono@latest ``` ```sh [bun] bun create hono@latest ``` ```sh [deno] deno init --npm hono@latest ``` ::: ## 特性 - **超快** 🚀 - `RegExpRouter` 路由器非常快。不使用线性循环。快速。 - **轻量** 🪶 - `hono/tiny` 预设小于 14kB。Hono 零依赖,仅使用 Web Standards。 - **多运行时** 🌍 - 可在 Cloudflare Workers、Fastly Compute、Deno、Bun、AWS Lambda 或 Node.js 上运行。相同的代码在所有平台上都能运行。 - **功能齐全** 🔋 - Hono 内置了中间件、自定义中间件、第三方中间件和辅助函数。功能齐全。 - **愉悦的开发体验** 😃 - 超干净的 API。一流的 TypeScript 支持。现在,我们有了"类型"。 ## 使用场景 Hono 是一个简单的 Web 应用程序框架,类似于 Express,不包含前端。 但它可以运行在 CDN 边缘,结合中间件可以构建更大的应用程序。 以下是一些使用场景示例。 - 构建 Web API - 后端服务器代理 - CDN 前端 - 边缘应用程序 - 库的基础服务器 - 全栈应用程序 ## 谁在使用 Hono? | 项目 | 平台 | 用途 | | ---------------------------------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------ | | [cdnjs](https://cdnjs.com) | Cloudflare Workers | 免费开源的 CDN 服务。_Hono 用于 API 服务器_。 | | [Cloudflare D1](https://www.cloudflare.com/developer-platform/d1/) | Cloudflare Workers | 无服务器 SQL 数据库。_Hono 用于内部 API 服务器_。 | | [Cloudflare Workers KV](https://www.cloudflare.com/developer-platform/workers-kv/) | Cloudflare Workers | 无服务器键值数据库。_Hono 用于内部 API 服务器_。 | | [BaseAI](https://baseai.dev) | 本地 AI 服务器 | 带记忆的无服务器 AI 代理管道。用于 Web 的开源代理 AI 框架。_使用 Hono 的 API 服务器_。 | | [Unkey](https://unkey.dev) | Cloudflare Workers | 开源 API 认证和授权。_Hono 用于 API 服务器_。 | | [OpenStatus](https://openstatus.dev) | Bun | 开源网站和 API 监控平台。_Hono 用于 API 服务器_。 | | [Deno Benchmarks](https://deno.com/benchmarks) | Deno | 基于 V8 构建的安全 TypeScript 运行时。_Hono 用于基准测试_。 | | [Clerk](https://clerk.com) | Cloudflare Workers | 开源用户管理平台。_Hono 用于 API 服务器_。 | 以及以下项目: - [Drivly](https://driv.ly/) - Cloudflare Workers - [repeat.dev](https://repeat.dev/) - Cloudflare Workers 想看更多?查看 [谁在生产环境中使用 Hono?](https://github.com/orgs/honojs/discussions/1510)。 ## 1 分钟了解 Hono 使用 Hono 创建 Cloudflare Workers 应用程序的演示。 ![一个 gif,展示使用 Hono 快速创建应用程序并快速迭代。](/images/sc.gif) ## 超快 **Hono 是最快的**,与其他 Cloudflare Workers 路由器相比。 ``` Hono x 402,820 ops/sec ±4.78% (80 runs sampled) itty-router x 212,598 ops/sec ±3.11% (87 runs sampled) sunder x 297,036 ops/sec ±4.76% (77 runs sampled) worktop x 197,345 ops/sec ±2.40% (88 runs sampled) Fastest is Hono ✨ Done in 28.06s. ``` 查看 [更多基准测试](/docs/concepts/benchmarks)。 ## 轻量 **Hono 非常小**。使用 `hono/tiny` 预设,压缩后大小**小于 14KB**。有许多中间件和适配器,但仅在使用时才会打包。作为参考,Express 的大小为 572KB。 ``` $ npx wrangler dev --minify ./src/index.ts ⛅️ wrangler 2.20.0 -------------------- ⬣ Listening at http://0.0.0.0:8787 - http://127.0.0.1:8787 - http://192.168.128.165:8787 Total Upload: 11.47 KiB / gzip: 4.34 KiB ``` ## 多个路由器 **Hono 有多个路由器**。 **RegExpRouter** 是 JavaScript 世界中最快的路由器。它使用调度前创建的单个大型 Regex 匹配路由。使用 **SmartRouter**,它支持所有路由模式。 **LinearRouter** 注册路由非常快,因此适合每次初始化应用程序的环境。**PatternRouter** 简单地添加和匹配模式,使其变小。 查看 [有关路由的更多信息](/docs/concepts/routers)。 ## Web Standards 由于使用 **Web Standards**,Hono 可以在许多平台上运行。 - Cloudflare Workers - Cloudflare Pages - Fastly Compute - Deno - Bun - Vercel - AWS Lambda - Lambda@Edge - 其他 通过使用 [Node.js 适配器](https://github.com/honojs/node-server),Hono 可以在 Node.js 上运行。 查看 [有关 Web Standards 的更多信息](/docs/concepts/web-standard)。 ## 中间件和辅助函数 **Hono 有许多中间件和辅助函数**。这使得"少写代码,多做事情"成为现实。 开箱即用,Hono 提供以下中间件和辅助函数: - [基本认证](/docs/middleware/builtin/basic-auth) - [Bearer 认证](/docs/middleware/builtin/bearer-auth) - [请求体限制](/docs/middleware/builtin/body-limit) - [缓存](/docs/middleware/builtin/cache) - [压缩](/docs/middleware/builtin/compress) - [上下文存储](/docs/middleware/builtin/context-storage) - [Cookie](/docs/helpers/cookie) - [CORS](/docs/middleware/builtin/cors) - [ETag](/docs/middleware/builtin/etag) - [html](/docs/helpers/html) - [JSX](/docs/guides/jsx) - [JWT 认证](/docs/middleware/builtin/jwt) - [日志记录器](/docs/middleware/builtin/logger) - [语言](/docs/middleware/builtin/language) - [Pretty JSON](/docs/middleware/builtin/pretty-json) - [安全头](/docs/middleware/builtin/secure-headers) - [SSG](/docs/helpers/ssg) - [流式传输](/docs/helpers/streaming) - [GraphQL 服务器](https://github.com/honojs/middleware/tree/main/packages/graphql-server) - [Firebase 认证](https://github.com/honojs/middleware/tree/main/packages/firebase-auth) - [Sentry](https://github.com/honojs/middleware/tree/main/packages/sentry) - 其他! 例如,使用 Hono 只需几行代码即可添加 ETag 和请求日志记录: ```ts import { Hono } from 'hono' import { etag } from 'hono/etag' import { logger } from 'hono/logger' const app = new Hono() app.use(etag(), logger()) ``` 查看 [有关中间件的更多信息](/docs/concepts/middleware)。 ## 开发体验 Hono 提供愉悦的"**开发体验**"。 得益于 `Context` 对象,可以轻松访问请求/响应。 此外,Hono 使用 TypeScript 编写。Hono 拥有"**类型**"。 例如,路径参数将是字面量类型。 ![截图显示 Hono 在使用 URL 参数时具有适当的字面量类型。URL "/entry/:date/:id" 允许请求参数为 "date" 或 "id"](/images/ss.png) 此外,Validator 和 Hono Client `hc` 启用 RPC 模式。在 RPC 模式中, 你可以使用喜欢的验证器(如 Zod),轻松与客户端共享服务器端 API 规范,并构建类型安全的应用程序。 查看 [Hono 技术栈](/docs/concepts/stacks)。 # 第三方中间件 第三方中间件是指未捆绑在 Hono 包中的中间件。 大多数此中间件利用外部库。 ### 认证 - [Auth.js(Next Auth)](https://github.com/honojs/middleware/tree/main/packages/auth-js) - [Casbin](https://github.com/honojs/middleware/tree/main/packages/casbin) - [Clerk Auth](https://github.com/honojs/middleware/tree/main/packages/clerk-auth) - [Cloudflare Access](https://github.com/honojs/middleware/tree/main/packages/cloudflare-access) - [OAuth Providers](https://github.com/honojs/middleware/tree/main/packages/oauth-providers) - [OIDC Auth](https://github.com/honojs/middleware/tree/main/packages/oidc-auth) - [Firebase Auth](https://github.com/honojs/middleware/tree/main/packages/firebase-auth) - [Verify RSA JWT (JWKS)](https://github.com/wataruoguchi/verify-rsa-jwt-cloudflare-worker) - [Stytch Auth](https://github.com/honojs/middleware/tree/main/packages/stytch-auth) ### 验证器 - [Ajv Validator](https://github.com/honojs/middleware/tree/main/packages/ajv-validator) - [ArkType Validator](https://github.com/honojs/middleware/tree/main/packages/arktype-validator) - [Class Validator](https://github.com/honojs/middleware/tree/main/packages/class-validator) - [Conform Validator](https://github.com/honojs/middleware/tree/main/packages/conform-validator) - [Effect Schema Validator](https://github.com/honojs/middleware/tree/main/packages/effect-validator) - [Standard Schema Validator](https://github.com/honojs/middleware/tree/main/packages/standard-validator) - [TypeBox Validator](https://github.com/honojs/middleware/tree/main/packages/typebox-validator) - [Typia Validator](https://github.com/honojs/middleware/tree/main/packages/typia-validator) - [unknownutil Validator](https://github.com/ryoppippi/hono-unknownutil-validator) - [Valibot Validator](https://github.com/honojs/middleware/tree/main/packages/valibot-validator) - [Zod Validator](https://github.com/honojs/middleware/tree/main/packages/zod-validator) ### OpenAPI - [Zod OpenAPI](https://github.com/honojs/middleware/tree/main/packages/zod-openapi) - [Scalar](https://github.com/scalar/scalar/tree/main/integrations/hono) - [Swagger UI](https://github.com/honojs/middleware/tree/main/packages/swagger-ui) - [Swagger Editor](https://github.com/honojs/middleware/tree/main/packages/swagger-editor) - [Hono OpenAPI](https://github.com/rhinobase/hono-openapi) - [hono-zod-openapi](https://github.com/paolostyle/hono-zod-openapi) ### 开发 - [ESLint Config](https://github.com/honojs/middleware/tree/main/packages/eslint-config) - [SSG Plugin Essential](https://github.com/honojs/middleware/tree/main/packages/ssg-plugins-essential) ### 监控/追踪 - [Apitally (API 监控和分析)](https://docs.apitally.io/frameworks/hono) - [Highlight.io](https://www.highlight.io/docs/getting-started/backend-sdk/js/hono) - [LogTape (日志记录)](https://logtape.org/manual/integrations#hono) - [OpenTelemetry](https://github.com/honojs/middleware/tree/main/packages/otel) - [Prometheus Metrics](https://github.com/honojs/middleware/tree/main/packages/prometheus) - [Sentry](https://github.com/honojs/middleware/tree/main/packages/sentry) - [Pino logger](https://github.com/maou-shonen/hono-pino) ### 服务器/适配器 - [GraphQL Server](https://github.com/honojs/middleware/tree/main/packages/graphql-server) - [Node WebSocket Helper](https://github.com/honojs/middleware/tree/main/packages/node-ws) - [tRPC Server](https://github.com/honojs/middleware/tree/main/packages/trpc-server) ### 转译器 - [Bun Transpiler](https://github.com/honojs/middleware/tree/main/packages/bun-transpiler) - [esbuild Transpiler](https://github.com/honojs/middleware/tree/main/packages/esbuild-transpiler) ### UI / 渲染器 - [Qwik City](https://github.com/honojs/middleware/tree/main/packages/qwik-city) - [React Compatibility](https://github.com/honojs/middleware/tree/main/packages/react-compat) - [React Renderer](https://github.com/honojs/middleware/tree/main/packages/react-renderer) ### 队列/作业处理 - [GlideMQ (Message Queue REST API + SSE)](https://github.com/avifenesh/glidemq-hono) ### 国际化 - [Intlayer i18n](https://intlayer.org/doc/environment/hono) ### 实用工具 - [Bun Compress](https://github.com/honojs/middleware/tree/main/packages/bun-compress) - [Cap Checkpoint](https://capjs.js.org/guide/middleware/hono.html) - [Event Emitter](https://github.com/honojs/middleware/tree/main/packages/event-emitter) - [Geo](https://github.com/ktkongtong/hono-geo-middleware/tree/main/packages/middleware) - [Hono Rate Limiter](https://github.com/rhinobase/hono-rate-limiter) - [Hono Problem Details (RFC 9457)](https://github.com/paveg/hono-problem-details) - [Hono Simple DI](https://github.com/maou-shonen/hono-simple-DI) - [Idempotency (Stripe-style idempotency keys)](https://github.com/paveg/hono-idempotency) - [idempot-js](https://js.idempot.dev) - 符合规范的中间件,支持多种存储后端(redis、postgres、mysql、sqlite) - [jsonv-ts (Validator, OpenAPI, MCP)](https://github.com/dswbx/jsonv-ts) - [MCP](https://github.com/honojs/middleware/tree/main/packages/mcp) - [RONIN (Database)](https://github.com/ronin-co/hono-client) - [Session](https://github.com/honojs/middleware/tree/main/packages/session) - [tsyringe](https://github.com/honojs/middleware/tree/main/packages/tsyringe) - [User Agent based Blocker](https://github.com/honojs/middleware/tree/main/packages/ua-blocker) # Basic Auth 中间件 此中间件可以对指定路径应用 Basic 认证。 使用 Cloudflare Workers 或其他平台实现 Basic 认证比看起来更复杂,但使用此中间件,这很容易。 有关 Basic 认证方案如何在底层工作的更多信息,请参阅 [MDN 文档](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#basic_authentication_scheme)。 ## 导入 ```ts import { Hono } from 'hono' import { basicAuth } from 'hono/basic-auth' ``` ## 用法 ```ts const app = new Hono() app.use( '/auth/*', basicAuth({ username: 'hono', password: 'acoolproject', }) ) app.get('/auth/page', (c) => { return c.text('You are authorized') }) ``` 要限制为特定路由 + 方法: ```ts const app = new Hono() app.get('/auth/page', (c) => { return c.text('Viewing page') }) app.delete( '/auth/page', basicAuth({ username: 'hono', password: 'acoolproject' }), (c) => { return c.text('Page deleted') } ) ``` 如果你想自己验证用户,请指定 `verifyUser` 选项;返回 `true` 表示接受。 ```ts const app = new Hono() app.use( basicAuth({ verifyUser: (username, password, c) => { return ( username === 'dynamic-user' && password === 'hono-password' ) }, }) ) ``` ## 选项 ### username: `string` 正在认证的用户的用户名。 ### password: `string` 用于认证的所提供用户名的密码值。 ### realm: `string` 领域的域名,作为返回的 WWW-Authenticate 挑战 header 的一部分。默认值为 `"Secure Area"`。 请参阅更多:https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate#directives ### hashFunction: `Function` 用于处理密码安全比较的散列函数。 ### verifyUser: `(username: string, password: string, c: Context) => boolean | Promise` 用于验证用户的函数。 ### invalidUserMessage: `string | object | MessageFunction` `MessageFunction` 是 `(c: Context) => string | object | Promise`。如果用户无效,则为自定义消息。 ### onAuthSuccess: `(c: Context, username: string) => void | Promise` 成功认证后调用的回调函数。这允许你设置上下文变量或执行副作用,而无需重新解析 Authorization header。 ```ts app.use( '/auth/*', basicAuth({ username: 'hono', password: 'acoolproject', onAuthSuccess: (c, username) => { c.set('username', username) }, }) ) app.get('/auth/page', (c) => { const username = c.get('username') return c.text(`Hello, ${username}!`) }) ``` ## 更多选项 ### ...users: `{ username: string, password: string }[]` ## 示例 ### 定义多个用户 此中间件还允许你传递包含定义更多 `username` 和 `password` 对的对象的任意参数。 ```ts app.use( '/auth/*', basicAuth( { username: 'hono', password: 'acoolproject', // 在第一个对象中定义其他参数 realm: 'www.example.com', }, { username: 'hono-admin', password: 'super-secure', // 不能在此处重新定义其他参数 }, { username: 'hono-user-1', password: 'a-secret', // 或在此处 } ) ) ``` 或更少硬编码: ```ts import { users } from '../config/users' app.use( '/auth/*', basicAuth( { realm: 'www.example.com', ...users[0], }, ...users.slice(1) ) ) ``` # Bearer Auth 中间件 Bearer Auth 中间件通过验证 Request header 中的 API 令牌提供认证。 访问端点的 HTTP 客户端将添加带有 `Bearer {token}` 作为 header 值的 `Authorization` header。 从终端使用 `curl`,它将如下所示: ```sh curl -H 'Authorization: Bearer honoiscool' http://localhost:8787/auth/page ``` ## 导入 ```ts import { Hono } from 'hono' import { bearerAuth } from 'hono/bearer-auth' ``` ## 用法 > [!NOTE] > 你的 `token` 必须匹配正则表达式 `/[A-Za-z0-9._~+/-]+=*/`,否则将返回 400 错误。值得注意的是,此正则表达式同时适用于 URL-safe Base64 和标准 Base64 编码的 JWT。此中间件不要求 bearer 令牌是 JWT,只需匹配上述正则表达式。 ```ts const app = new Hono() const token = 'honoiscool' app.use('/api/*', bearerAuth({ token })) app.get('/api/page', (c) => { return c.json({ message: 'You are authorized' }) }) ``` 要限制为特定路由 + 方法: ```ts const app = new Hono() const token = 'honoiscool' app.get('/api/page', (c) => { return c.json({ message: 'Read posts' }) }) app.post('/api/page', bearerAuth({ token }), (c) => { return c.json({ message: 'Created post!' }, 201) }) ``` 要实现多个令牌(例如,任何有效令牌都可以读取,但创建/更新/删除仅限于特权令牌): ```ts const app = new Hono() const readToken = 'read' const privilegedToken = 'read+write' const privilegedMethods = ['POST', 'PUT', 'PATCH', 'DELETE'] app.on('GET', '/api/page/*', async (c, next) => { // 有效令牌列表 const bearer = bearerAuth({ token: [readToken, privilegedToken] }) return bearer(c, next) }) app.on(privilegedMethods, '/api/page/*', async (c, next) => { // 单个有效特权令牌 const bearer = bearerAuth({ token: privilegedToken }) return bearer(c, next) }) // 定义 GET、POST 等的 handlers ``` 如果你想自己验证令牌的值,请指定 `verifyToken` 选项;返回 `true` 表示接受。 ```ts const app = new Hono() app.use( '/auth-verify-token/*', bearerAuth({ verifyToken: async (token, c) => { return token === 'dynamic-token' }, }) ) ``` ## 选项 ### token: `string` | `string[]` 用于验证传入 bearer 令牌的字符串。 ### realm: `string` 领域的域名,作为返回的 WWW-Authenticate 挑战 header 的一部分。默认值为 `""`。 请参阅更多:https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate#directives ### prefix: `string` Authorization header 值的前缀(或称为 `schema`)。默认值为 `"Bearer"`。 ### headerName: `string` header 名称。默认值为 `Authorization`。 ### hashFunction: `Function` 用于处理认证令牌安全比较的散列函数。 ### verifyToken: `(token: string, c: Context) => boolean | Promise` 用于验证令牌的函数。 ### noAuthenticationHeader: `object` 自定义请求没有认证 header 时的错误响应。 - `wwwAuthenticateHeader`: `string | object | MessageFunction` - 自定义 WWW-Authenticate header 值。 - `message`: `string | object | MessageFunction` - 响应体的自定义消息。 `MessageFunction` 是 `(c: Context) => string | object | Promise`。 ### invalidAuthenticationHeader: `object` 自定义认证 header 格式无效时的错误响应。 - `wwwAuthenticateHeader`: `string | object | MessageFunction` - 自定义 WWW-Authenticate header 值。 - `message`: `string | object | MessageFunction` - 响应体的自定义消息。 ### invalidToken: `object` 自定义令牌无效时的错误响应。 - `wwwAuthenticateHeader`: `string | object | MessageFunction` - 自定义 WWW-Authenticate header 值。 - `message`: `string | object | MessageFunction` - 响应体的自定义消息。 # Body Limit 中间件 Body Limit 中间件可以限制请求体的文件大小。 此中间件首先使用请求中 `Content-Length` header 的值(如果存在)。 如果未设置,它将在流中读取 body,如果大于指定的文件大小,则执行错误处理器。 ## 导入 ```ts import { Hono } from 'hono' import { bodyLimit } from 'hono/body-limit' ``` ## 用法 ```ts const app = new Hono() app.post( '/upload', bodyLimit({ maxSize: 50 * 1024, // 50kb onError: (c) => { return c.text('overflow :(', 413) }, }), async (c) => { const body = await c.req.parseBody() if (body['file'] instanceof File) { console.log(`Got file sized: ${body['file'].size}`) } return c.text('pass :)') } ) ``` ## 选项 ### maxSize: `number` 你想要限制的最大文件大小。默认值为 `100 * 1024` - `100kb`。 ### onError: `OnError` 如果超过指定的文件大小,将调用此错误处理器。 ## 与 Bun 一起使用以处理大请求 如果显式使用 Body Limit 中间件来允许大于默认值的请求体,则可能需要相应地更改 `Bun.serve` 配置。[在撰写本文时](https://github.com/oven-sh/bun/blob/f2cfa15e4ef9d730fc6842ad8b79fb7ab4c71cb9/packages/bun-types/bun.d.ts#L2191),`Bun.serve` 的默认请求体限制为 128MiB。如果你将 Hono 的 Body Limit 中间件设置为大于该值,你的请求仍将失败,此外,中间件中指定的 `onError` 处理器将不会被调用。这是因为 `Bun.serve()` 将在将请求传递给 Hono 之前将状态码设置为 `413` 并终止连接。 如果你想使用 Hono 和 Bun 接受大于 128MiB 的请求,你也需要为 Bun 设置限制: ```ts export default { port: process.env['PORT'] || 3000, fetch: app.fetch, maxRequestBodySize: 1024 * 1024 * 200, // 你的值 } ``` 或者,根据你的设置: ```ts Bun.serve({ fetch(req, server) { return app.fetch(req, { ip: server.requestIP(req) }) }, maxRequestBodySize: 1024 * 1024 * 200, // 你的值 }) ``` # Cache 中间件 Cache 中间件使用 Web 标准的 [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache)。 Cache 中间件目前支持使用自定义域的 Cloudflare Workers 项目和使用 [Deno 1.26+](https://github.com/denoland/deno/releases/tag/v1.26.0) 的 Deno 项目。也可用于 Deno Deploy。 Cloudflare Workers 尊重 `Cache-Control` header 并返回缓存的响应。有关详细信息,请参阅 [Cloudflare 文档上的 Cache](https://developers.cloudflare.com/workers/runtime-apis/cache/)。Deno 不尊重 headers,因此如果你需要更新缓存,你需要实现自己的机制。 有关每个平台的说明,请参阅下面的 [用法](#usage)。 ## 导入 ```ts import { Hono } from 'hono' import { cache } from 'hono/cache' ``` ## 用法 ::: code-group ```ts [Cloudflare Workers] app.get( '*', cache({ cacheName: 'my-app', cacheControl: 'max-age=3600', }) ) ``` ```ts [Deno] // 必须为 Deno 运行时使用 `wait: true` app.get( '*', cache({ cacheName: 'my-app', cacheControl: 'max-age=3600', wait: true, }) ) ``` ::: ## 选项 ### cacheName: `string` | `(c: Context) => string` | `Promise` 缓存的名称。可用于存储具有不同标识符的多个缓存。 ### wait: `boolean` 布尔值,指示 Hono 是否应在继续请求之前等待 `cache.put` 函数的 Promise 解析。_对于 Deno 环境必须为 true_。默认值为 `false`。 ### cacheControl: `string` `Cache-Control` header 的指令字符串。有关更多信息,请参阅 [MDN 文档](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)。当未提供此选项时,不会向请求添加 `Cache-Control` header。 ### vary: `string` | `string[]` 在响应中设置 `Vary` header。如果原始响应 header 已经包含 `Vary` header,则合并值,删除任何重复项。将其设置为 `*` 将导致错误。有关 Vary header 及其对缓存策略的影响的更多详细信息,请参阅 [MDN 文档](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary)。 ### keyGenerator: `(c: Context) => string | Promise` 为 `cacheName` 存储中的每个请求生成 keys。这可用于根据请求参数或上下文参数缓存数据。默认值为 `c.req.url`。 ### cacheableStatusCodes: `number[]` 应缓存的状态码数组。默认值为 `[200]`。使用此选项缓存具有特定状态码的响应。 ```ts app.get( '*', cache({ cacheName: 'my-app', cacheControl: 'max-age=3600', cacheableStatusCodes: [200, 404, 412], }) ) ``` # Combine 中间件 Combine 中间件将多个中间件函数组合为单个中间件。它提供三个函数: - `some` - 仅运行给定中间件中的一个。 - `every` - 运行所有给定中间件。 - `except` - 仅当不满足条件时运行所有给定中间件。 ## 导入 ```ts import { Hono } from 'hono' import { some, every, except } from 'hono/combine' ``` ## 用法 以下是使用 Combine 中间件的复杂访问控制规则示例。 ```ts import { Hono } from 'hono' import { bearerAuth } from 'hono/bearer-auth' import { getConnInfo } from 'hono/cloudflare-workers' import { every, some } from 'hono/combine' import { ipRestriction } from 'hono/ip-restriction' import { rateLimit } from '@/my-rate-limit' const app = new Hono() app.use( '*', some( every( ipRestriction(getConnInfo, { allowList: ['192.168.0.2'] }), bearerAuth({ token }) ), // 如果两个条件都满足,rateLimit 将不会执行。 rateLimit() ) ) app.get('/', (c) => c.text('Hello Hono!')) ``` ### some 运行返回 true 的第一个中间件。中间件按顺序应用,如果任何中间件成功退出,后续中间件将不会运行。 ```ts import { some } from 'hono/combine' import { bearerAuth } from 'hono/bearer-auth' import { myRateLimit } from '@/rate-limit' // 如果客户端有有效令牌,则跳过速率限制。 // 否则,应用速率限制。 app.use( '/api/*', some(bearerAuth({ token }), myRateLimit({ limit: 100 })) ) ``` ### every 运行所有中间件,如果任何中间件失败则停止。中间件按顺序应用,如果任何中间件抛出错误,后续中间件将不会运行。 ```ts import { some, every } from 'hono/combine' import { bearerAuth } from 'hono/bearer-auth' import { myCheckLocalNetwork } from '@/check-local-network' import { myRateLimit } from '@/rate-limit' // 如果客户端在本地网络中,则跳过认证和速率限制。 // 否则,应用认证和速率限制。 app.use( '/api/*', some( myCheckLocalNetwork(), every(bearerAuth({ token }), myRateLimit({ limit: 100 })) ) ) ``` ### except 运行所有中间件,除非满足条件。你可以传递字符串或函数作为条件。如果需要匹配多个目标,请将它们作为数组传递。 ```ts import { except } from 'hono/combine' import { bearerAuth } from 'hono/bearer-auth' // 如果客户端正在访问公共 API,则跳过认证。 // 否则,需要有效令牌。 app.use('/api/*', except('/api/public/*', bearerAuth({ token }))) ``` # Compress 中间件 此中间件根据 `Accept-Encoding` 请求 header 压缩响应体。 ::: info **注意**:在 Cloudflare Workers 和 Deno Deploy 上,响应体将自动压缩,因此无需使用此中间件。 ::: ## 导入 ```ts import { Hono } from 'hono' import { compress } from 'hono/compress' ``` ## 用法 ```ts const app = new Hono() app.use(compress()) ``` ## 选项 ### encoding: `'gzip'` | `'deflate'` 允许响应压缩的压缩方案。`gzip` 或 `deflate`。如果未定义,则两者都允许,并将基于 `Accept-Encoding` header 使用。如果未提供此选项且客户端在 `Accept-Encoding` header 中同时提供两者,则优先使用 `gzip`。 ### threshold: `number` 要压缩的最小大小(以字节为单位)。默认为 1024 字节。 # Context Storage 中间件 Context Storage 中间件将 Hono `Context` 存储在 `AsyncLocalStorage` 中,使其可以全局访问。 ::: info **注意** 此中间件使用 `AsyncLocalStorage`。运行时应该支持它。 **Cloudflare Workers**:要启用 `AsyncLocalStorage`,请将 [`nodejs_compat` 或 `nodejs_als` 标志](https://developers.cloudflare.com/workers/configuration/compatibility-dates/#nodejs-compatibility-flag) 添加到你的 `wrangler.toml` 文件。 ::: ## 导入 ```ts import { Hono } from 'hono' import { contextStorage, getContext, tryGetContext, } from 'hono/context-storage' ``` ## 用法 如果 `contextStorage()` 作为中间件应用,`getContext()` 将返回当前 Context 对象。 ```ts type Env = { Variables: { message: string } } const app = new Hono() app.use(contextStorage()) app.use(async (c, next) => { c.set('message', 'Hello!') await next() }) // 你可以在 handler 之外访问变量。 const getMessage = () => { return getContext().var.message } app.get('/', (c) => { return c.text(getMessage()) }) ``` 在 Cloudflare Workers 上,你可以在 handler 之外访问 bindings。 ```ts type Env = { Bindings: { KV: KVNamespace } } const app = new Hono() app.use(contextStorage()) const setKV = (value: string) => { return getContext().env.KV.put('key', value) } ``` ## tryGetContext `tryGetContext()` 的工作方式类似于 `getContext()`,但在上下文不可用时返回 `undefined` 而不是抛出错误: ```ts const context = tryGetContext() if (context) { // 上下文可用 console.log(context.var.message) } ``` # CORS 中间件 有许多将 Cloudflare Workers 用作 Web API 并从外部前端应用程序调用它们的用例。 对于它们,我们必须实现 CORS,让我们也用中间件来做这个。 ## 导入 ```ts import { Hono } from 'hono' import { cors } from 'hono/cors' ``` ## 用法 ```ts const app = new Hono() // CORS 应该在路由之前调用 app.use('/api/*', cors()) app.use( '/api2/*', cors({ origin: 'http://example.com', allowHeaders: ['X-Custom-Header', 'Upgrade-Insecure-Requests'], allowMethods: ['POST', 'GET', 'OPTIONS'], exposeHeaders: ['Content-Length', 'X-Kuma-Revision'], maxAge: 600, credentials: true, }) ) app.all('/api/abc', (c) => { return c.json({ success: true }) }) app.all('/api2/abc', (c) => { return c.json({ success: true }) }) ``` 多个来源: ```ts app.use( '/api3/*', cors({ origin: ['https://example.com', 'https://example.org'], }) ) // 或者你可以使用 "function" app.use( '/api4/*', cors({ // `c` 是 `Context` 对象 origin: (origin, c) => { return origin.endsWith('.example.com') ? origin : 'http://example.com' }, }) ) ``` 基于来源的动态允许方法: ```ts app.use( '/api5/*', cors({ origin: (origin) => origin === 'https://example.com' ? origin : '*', // `c` 是 `Context` 对象 allowMethods: (origin, c) => origin === 'https://example.com' ? ['GET', 'HEAD', 'POST', 'PATCH', 'DELETE'] : ['GET', 'HEAD'], }) ) ``` ## 选项 ### origin: `string` | `string[]` | `(origin:string, c:Context) => string` "_Access-Control-Allow-Origin_" CORS header 的值。你也可以传递回调函数,如 `origin: (origin) => (origin.endsWith('.example.com') ? origin : 'http://example.com')`。默认值为 `*`。 ### allowMethods: `string[]` | `(origin:string, c:Context) => string[]` "_Access-Control-Allow-Methods_" CORS header 的值。你也可以传递回调函数来根据来源动态确定允许的方法。默认值为 `['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH']`。 ### allowHeaders: `string[]` "_Access-Control-Allow-Headers_" CORS header 的值。默认值为 `[]`。 ### maxAge: `number` "_Access-Control-Max-Age_" CORS header 的值。 ### credentials: `boolean` "_Access-Control-Allow-Credentials_" CORS header 的值。 ### exposeHeaders: `string[]` "_Access-Control-Expose-Headers_" CORS header 的值。默认值为 `[]`。 ## 依赖于环境的 CORS 配置 如果你想根据执行环境(如开发或生产)调整 CORS 配置,从环境变量注入值很方便,因为它消除了应用程序感知其自身执行环境的需要。请参阅下面的示例以澄清。 ```ts app.use('*', async (c, next) => { const corsMiddlewareHandler = cors({ origin: c.env.CORS_ORIGIN, }) return corsMiddlewareHandler(c, next) }) ``` ## 与 Vite 一起使用 当将 Hono 与 Vite 一起使用时,你应该通过在 `vite.config.ts` 中将 `server.cors` 设置为 `false` 来禁用 Vite 的内置 CORS 功能。这可以防止与 Hono 的 CORS 中间件冲突。 ```ts // vite.config.ts import { cloudflare } from '@cloudflare/vite-plugin' import { defineConfig } from 'vite' export default defineConfig({ server: { cors: false, // 禁用 Vite 的内置 CORS 设置 }, plugins: [cloudflare()], }) ``` # CSRF 保护 此中间件通过检查 `Origin` header 和 `Sec-Fetch-Site` header 来防止 CSRF 攻击。如果任一验证通过,则允许请求。 中间件仅验证以下请求: - 使用不安全的 HTTP 方法(不是 GET、HEAD 或 OPTIONS) - 具有可以由 HTML 表单发送的内容类型(`application/x-www-form-urlencoded`、`multipart/form-data` 或 `text/plain`) 不发送 `Origin` headers 的旧浏览器,或使用反向代理删除这些 headers 的环境,可能无法正常工作。在此类环境中,请使用其他 CSRF 令牌方法。 ## 导入 ```ts import { Hono } from 'hono' import { csrf } from 'hono/csrf' ``` ## 用法 ```ts const app = new Hono() // 默认:origin 和 sec-fetch-site 验证 app.use(csrf()) // 允许特定来源 app.use(csrf({ origin: 'https://myapp.example.com' })) // 允许多个来源 app.use( csrf({ origin: [ 'https://myapp.example.com', 'https://development.myapp.example.com', ], }) ) // 允许特定 sec-fetch-site 值 app.use(csrf({ secFetchSite: 'same-origin' })) app.use(csrf({ secFetchSite: ['same-origin', 'none'] })) // 动态 origin 验证 强烈建议验证协议以确保匹配 `$`。 你绝不应该进行正向匹配。 app.use( '*', csrf({ origin: (origin) => /https:\/\/(\w+\.)?myapp\.example\.com$/.test(origin), }) ) // 动态 sec-fetch-site 验证 app.use( csrf({ secFetchSite: (secFetchSite, c) => { // 始终允许 same-origin if (secFetchSite === 'same-origin') return true // 为 webhook 端点允许 cross-site if ( secFetchSite === 'cross-site' && c.req.path.startsWith('/webhook/') ) { return true } return false }, }) ) ``` ## 选项 ### origin: `string` | `string[]` | `Function` 指定 CSRF 保护的允许来源。 - **`string`**:单个允许的来源(例如 `'https://example.com'`) - **`string[]`**:允许的来源数组 - **`Function`**:自定义处理器 `(origin: string, context: Context) => boolean` 用于灵活的来源验证和绕过逻辑 **默认**:仅与请求 URL 相同的来源 函数处理器接收请求的 `Origin` header 值和请求上下文,允许基于请求属性(如路径、headers 或其他上下文数据)进行动态验证。 ### secFetchSite: `string` | `string[]` | `Function` 使用 [Fetch Metadata](https://web.dev/articles/fetch-metadata) 指定 CSRF 保护的允许 Sec-Fetch-Site header 值。 - **`string`**:单个允许的值(例如 `'same-origin'`) - **`string[]`**:允许的值数组(例如 `['same-origin', 'none']`) - **`Function`**:自定义处理器 `(secFetchSite: string, context: Context) => boolean` 用于灵活验证 **默认**:仅允许 `'same-origin'` 标准 Sec-Fetch-Site 值: - `same-origin`:来自同一来源的请求 - `same-site`:来自同一站点(不同子域)的请求 - `cross-site`:来自不同站点的请求 - `none`:不是来自网页的请求(例如,浏览器地址栏、书签) 函数处理器接收请求的 `Sec-Fetch-Site` header 值和请求上下文,支持基于请求属性的动态验证。 # ETag 中间件 使用此中间件,你可以轻松添加 ETag headers。 ## 导入 ```ts import { Hono } from 'hono' import { etag } from 'hono/etag' ``` ## 用法 ```ts const app = new Hono() app.use('/etag/*', etag()) app.get('/etag/abc', (c) => { return c.text('Hono is cool') }) ``` ## 保留的 headers 304 响应必须包括在等效 200 OK 响应中发送的 headers。默认的 headers 是 Cache-Control、Content-Location、Date、ETag、Expires 和 Vary。 如果你想添加发送的 header,你可以使用 `retainedHeaders` 选项和包含默认 headers 的 `RETAINED_304_HEADERS` 字符串数组变量: ```ts import { etag, RETAINED_304_HEADERS } from 'hono/etag' // ... app.use( '/etag/*', etag({ retainedHeaders: ['x-message', ...RETAINED_304_HEADERS], }) ) ``` ## 选项 ### weak: `boolean` 定义是否使用 [弱验证](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests#weak_validation)。如果设置为 `true`,则在值的前缀添加 `w/`。默认值为 `false`。 ### retainedHeaders: `string[]` 你希望在 304 响应中保留的 headers。 ### generateDigest: `(body: Uint8Array) => ArrayBuffer | Promise` 自定义摘要生成函数。默认情况下,它使用 `SHA-1`。此函数使用响应体作为 `Uint8Array` 调用,并应返回哈希作为 `ArrayBuffer` 或其 Promise。 # IP Restriction 中间件 IP Restriction 中间件是根据用户的 IP 地址限制对资源访问的中间件。 ## 导入 ```ts import { Hono } from 'hono' import { ipRestriction } from 'hono/ip-restriction' ``` ## 用法 对于在 Bun 上运行的应用程序,如果你只想允许来自本地的访问,你可以按以下方式编写。在 `denyList` 中指定你想要拒绝的规则,在 `allowList` 中指定你想要允许的规则。 ```ts import { Hono } from 'hono' import { getConnInfo } from 'hono/bun' import { ipRestriction } from 'hono/ip-restriction' const app = new Hono() app.use( '*', ipRestriction(getConnInfo, { denyList: [], allowList: ['127.0.0.1', '::1'], }) ) app.get('/', (c) => c.text('Hello Hono!')) ``` 将适合你的环境的 [ConnInfo 辅助工具](/docs/helpers/conninfo) 中的 `getConninfo` 作为 `ipRestriction` 的第一个参数传递。例如,对于 Deno,它将如下所示: ```ts import { getConnInfo } from 'hono/deno' import { ipRestriction } from 'hono/ip-restriction' //... app.use( '*', ipRestriction(getConnInfo, { // ... }) ) ``` ## 规则 请按照以下说明编写规则。 ### IPv4 - `192.168.2.0` - 静态 IP 地址 - `192.168.2.0/24` - CIDR 表示法 - `*` - 所有地址 ### IPv6 - `::1` - 静态 IP 地址 - `::1/10` - CIDR 表示法 - `*` - 所有地址 ## 错误处理 要自定义错误,请在第三个参数中返回 `Response`。 ```ts app.use( '*', ipRestriction( getConnInfo, { denyList: ['192.168.2.0/24'], }, async (remote, c) => { return c.text(`Blocking access from ${remote.addr}`, 403) } ) ) ``` # JSX Renderer 中间件 JSX Renderer 中间件允许你在使用 `c.render()` 函数渲染 JSX 时设置布局,而无需使用 `c.setRenderer()`。此外,它通过使用 `useRequestContext()` 使得可以在组件中访问 Context 实例。 ## 导入 ```ts import { Hono } from 'hono' import { jsxRenderer, useRequestContext } from 'hono/jsx-renderer' ``` ## 用法 ```jsx const app = new Hono() app.get( '/page/*', jsxRenderer(({ children }) => { return (
Menu
{children}
) }) ) app.get('/page/about', (c) => { return c.render(

About me!

) }) ``` ## 选项 ### docType: `boolean` | `string` 如果你不想在 HTML 开头添加 DOCTYPE,请将 `docType` 选项设置为 `false`。 ```tsx app.use( '*', jsxRenderer( ({ children }) => { return ( {children} ) }, { docType: false } ) ) ``` 你可以指定 DOCTYPE。 ```tsx app.use( '*', jsxRenderer( ({ children }) => { return ( {children} ) }, { docType: '', } ) ) ``` ### stream: `boolean` | `Record` 如果你将其设置为 `true` 或提供 Record 值,它将渲染为流式响应。 ```tsx const AsyncComponent = async () => { await new Promise((r) => setTimeout(r, 1000)) // 睡眠 1 秒 return
Hi!
} app.get( '*', jsxRenderer( ({ children }) => { return (

SSR Streaming

{children} ) }, { stream: true } ) ) app.get('/', (c) => { return c.render( loading...}> ) }) ``` 如果设置了 `true`,则添加以下 headers: ```ts { 'Transfer-Encoding': 'chunked', 'Content-Type': 'text/html; charset=UTF-8', 'Content-Encoding': 'Identity' } ``` 你可以通过指定 Record 值来自定义 header 值。 ### 基于函数的选项 你可以传递接收 `Context` 对象的函数而不是静态选项对象。这允许你根据请求上下文(如环境变量或请求参数)动态设置选项。 ```tsx app.use( '*', jsxRenderer( ({ children }) => { return ( {children} ) }, (c) => ({ stream: c.req.header('X-Enable-Streaming') === 'true', }) ) ) ``` 作为一个具体示例,你可以使用它在使用 `` 生成静态站点 (SSG) 时禁用流式,通过使用 [`isSSGContext`](/docs/helpers/ssg#isssgcontext) 辅助工具: ```tsx app.use( '*', jsxRenderer( ({ children }) => { return (
) }, (c) => ({ stream: !isSSGContext(c), }) ) ) ``` ## 嵌套布局 `Layout` 组件支持嵌套布局。 ```tsx app.use( jsxRenderer(({ children }) => { return ( {children} ) }) ) const blog = new Hono() blog.use( jsxRenderer(({ children, Layout }) => { return (
{children}
) }) ) app.route('/blog', blog) ``` ## `useRequestContext()` `useRequestContext()` 返回 Context 实例。 ```tsx import { useRequestContext, jsxRenderer } from 'hono/jsx-renderer' const app = new Hono() app.use(jsxRenderer()) const RequestUrlBadge: FC = () => { const c = useRequestContext() return {c.req.url} } app.get('/page/info', (c) => { return c.render(
You are accessing:
) }) ``` ::: warning 你不能在 Deno 的 `precompile` JSX 选项中使用 `useRequestContext()`。请使用 `react-jsx`: ```json "compilerOptions": { "jsx": "precompile", // [!code --] "jsx": "react-jsx", // [!code ++] "jsxImportSource": "hono/jsx" } } ``` ::: ## 扩展 `ContextRenderer` 通过按以下方式定义 `ContextRenderer`,你可以将附加内容传递给渲染器。例如,当你想根据页面更改 head 标签的内容时,这很方便。 ```tsx declare module 'hono' { interface ContextRenderer { ( content: string | Promise, props: { title: string } ): Response } } const app = new Hono() app.get( '/page/*', jsxRenderer(({ children, title }) => { return ( {title}
Menu
{children}
) }) ) app.get('/page/favorites', (c) => { return c.render(
  • Eating sushi
  • Watching baseball games
, { title: 'My favorites', } ) }) ``` # JWK Auth 中间件 JWK Auth 中间件通过使用 JWK (JSON Web Key) 验证令牌来认证请求。它检查 `Authorization` header 和其他配置来源(如 cookies,如果指定)。它使用提供的 `keys` 验证令牌,如果指定则从 `jwks_uri` 获取 keys,并且如果设置了 `cookie` 选项则支持从 cookies 提取令牌。 ## 此中间件验证的内容 对于每个令牌,`jwk()`: - 解析并验证 JWT header 格式。 - 需要 `kid` header 并通过 `kid` 查找匹配的 key。 - 拒绝对称算法(`HS256`、`HS384`、`HS512`)。 - 要求 header `alg` 包含在配置的 `alg` 允许列表中。 - 如果匹配的 JWK 有 `alg` 字段,则要求它与 JWT header `alg` 匹配。 - 使用匹配的 key 验证令牌签名。 - 默认情况下,验证基于时间的 claims:`nbf`、`exp` 和 `iat`。 可以使用 `verification` 选项配置可选的 claim 验证: - `iss`:提供时验证颁发者。 - `aud`:提供时验证受众。 如果你需要在上述之外进行额外的令牌检查(例如,自定义应用程序级授权规则),请在 `jwk()` 之后在自己的中间件中添加它们。 :::info 从客户端发送的 Authorization header 必须具有指定的方案。 示例:`Bearer my.token.value` 或 `Basic my.token.value` ::: ## 导入 ```ts import { Hono } from 'hono' import { jwk } from 'hono/jwk' import { verifyWithJwks } from 'hono/jwt' ``` ## 用法 ```ts const app = new Hono() app.use( '/auth/*', jwk({ jwks_uri: `https://${backendServer}/.well-known/jwks.json`, alg: ['RS256'], }) ) app.get('/auth/page', (c) => { return c.text('You are authorized') }) ``` 获取 payload: ```ts const app = new Hono() app.use( '/auth/*', jwk({ jwks_uri: `https://${backendServer}/.well-known/jwks.json`, alg: ['RS256'], }) ) app.get('/auth/page', (c) => { const payload = c.get('jwtPayload') return c.json(payload) // 例如:{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 } }) ``` 匿名访问: ```ts const app = new Hono() app.use( '/auth/*', jwk({ jwks_uri: (c) => `https://${c.env.authServer}/.well-known/jwks.json`, alg: ['RS256'], allow_anon: true, }) ) app.get('/auth/page', (c) => { const payload = c.get('jwtPayload') return c.json(payload ?? { message: 'hello anon' }) }) ``` ## 在中间件之外使用 `verifyWithJwks` `verifyWithJwks` 实用函数可用于在 Hono 中间件上下文之外验证 JWT 令牌,例如在 SvelteKit SSR 页面或其他服务器端环境中: ```ts const id_payload = await verifyWithJwks( id_token, { jwks_uri: 'https://your-auth-server/.well-known/jwks.json', allowedAlgorithms: ['RS256'], }, { cf: { cacheEverything: true, cacheTtl: 3600 }, } ) ``` ## 配置 JWKS fetch 请求选项 要配置如何从 `jwks_uri` 检索 JWKS,请将 fetch 请求选项作为 `jwk()` 的第二个参数传递。 此参数是 `RequestInit`,仅用于 JWKS fetch 请求。 ```ts const app = new Hono() app.use( '/auth/*', jwk( { jwks_uri: `https://${backendServer}/.well-known/jwks.json`, alg: ['RS256'], }, { headers: { Authorization: 'Bearer TOKEN', }, } ) ) ``` ## 选项 ### alg: `AsymmetricAlgorithm[]` 用于令牌验证的允许非对称算法数组。 可用类型为 `RS256` | `RS384` | `RS512` | `PS256` | `PS384` | `PS512` | `ES256` | `ES384` | `ES512` | `EdDSA`。 ### keys: `HonoJsonWebKey[] | (c: Context) => Promise` 你的公钥的值,或返回它们的函数。函数接收 Context 对象。 ### jwks_uri: `string` | `(c: Context) => Promise` 如果设置了此值,则尝试从此 URI 获取 JWKs,期望具有 `keys` 的 JSON 响应,并将其添加到提供的 `keys` 选项。你也可以传递回调函数来使用 Context 动态确定 JWKS URI。 ### allow_anon: `boolean` 如果将此值设置为 `true`,则没有有效令牌的请求将被允许通过中间件。使用 `c.get('jwtPayload')` 检查请求是否已认证。默认值为 `false`。 ### cookie: `string` 如果设置了此值,则使用该值作为键从 cookie header 中检索值,然后将其验证为令牌。 ### headerName: `string` 用于查找 JWT 令牌的 header 名称。默认值为 `Authorization`。 ### verification: `VerifyOptions` 配置除签名验证之外的 claim 验证行为: - `iss`:预期颁发者。 - `aud`:预期受众。 - `exp`、`nbf`、`iat`:默认启用,如果需要可以禁用。 # JWT Auth 中间件 JWT Auth 中间件通过验证 JWT 令牌提供认证。 如果未设置 `cookie` 选项,中间件将检查 `Authorization` header。你可以使用 `headerName` 选项自定义 header 名称。 :::info 从客户端发送的 Authorization header 必须具有指定的方案。 示例:`Bearer my.token.value` 或 `Basic my.token.value` ::: ## 导入 ```ts import { Hono } from 'hono' import { jwt } from 'hono/jwt' import type { JwtVariables } from 'hono/jwt' ``` ## 用法 ```ts // 指定变量类型以推断 `c.get('jwtPayload')`: type Variables = JwtVariables const app = new Hono<{ Variables: Variables }>() app.use( '/auth/*', jwt({ secret: 'it-is-very-secret', alg: 'HS256', }) ) app.get('/auth/page', (c) => { return c.text('You are authorized') }) ``` 获取 payload: ```ts const app = new Hono() app.use( '/auth/*', jwt({ secret: 'it-is-very-secret', alg: 'HS256', issuer: 'my-trusted-issuer', }) ) app.get('/auth/page', (c) => { const payload = c.get('jwtPayload') return c.json(payload) // 例如:{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022, "iss": "my-trusted-issuer" } }) ``` ::: tip `jwt()` 只是一个中间件函数。如果你想使用环境变量(例如 `c.env.JWT_SECRET`),你可以按以下方式使用: ```js app.use('/auth/*', (c, next) => { const jwtMiddleware = jwt({ secret: c.env.JWT_SECRET, alg: 'HS256', }) return jwtMiddleware(c, next) }) ``` ::: ## 选项 ### secret: `string` 你的密钥的值。 ### alg: `string` 用于验证的算法类型。 可用类型为 `HS256` | `HS384` | `HS512` | `RS256` | `RS384` | `RS512` | `PS256` | `PS384` | `PS512` | `ES256` | `ES384` | `ES512` | `EdDSA`。 ### cookie: `string` 如果设置了此值,则使用该值作为键从 cookie header 中检索值,然后将其验证为令牌。 ### headerName: `string` 用于查找 JWT 令牌的 header 名称。默认值为 `Authorization`。 ```ts app.use( '/auth/*', jwt({ secret: 'it-is-very-secret', alg: 'HS256', headerName: 'x-custom-auth-header', }) ) ``` ### verifyOptions: `VerifyOptions` 控制令牌验证的选项。 #### verifyOptions.iss: `string | RexExp` 用于令牌验证的预期颁发者。如果未设置此选项,则**不**检查 `iss` claim。 #### verifyOptions.nbf: `boolean` 如果存在 `nbf`(not before)claim 并且设置为 `true`,则将验证它。默认值为 `true`。 #### verifyOptions.iat: `boolean` 如果存在 `iat`(issued at)claim 并且设置为 `true`,则将验证它。默认值为 `true`。 #### verifyOptions.exp: `boolean` 如果存在 `exp`(expiration time)claim 并且设置为 `true`,则将验证它。默认值为 `true`。 # Language 中间件 Language Detector 中间件自动从各种来源确定用户的首选语言(区域设置),并通过 `c.get('language')` 提供。检测策略包括查询参数、cookies、headers 和 URL 路径段。非常适合国际化(i18n)和特定区域设置的内容。 ## 导入 ```ts import { Hono } from 'hono' import { languageDetector } from 'hono/language' ``` ## 基本用法 从查询字符串、cookie 和 header 检测语言(默认顺序),并回退到英语: ```ts const app = new Hono() app.use( languageDetector({ supportedLanguages: ['en', 'ar', 'ja'], // 必须包含回退 fallbackLanguage: 'en', // 必需 }) ) app.get('/', (c) => { const lang = c.get('language') return c.text(`Hello! Your language is ${lang}`) }) ``` ### 客户端示例 ```sh # 通过路径 curl http://localhost:8787/ar/home # 通过查询参数 curl http://localhost:8787/?lang=ar # 通过 cookie curl -H 'Cookie: language=ja' http://localhost:8787/ # 通过 header curl -H 'Accept-Language: ar,en;q=0.9' http://localhost:8787/ ``` ## 默认配置 ```ts export const DEFAULT_OPTIONS: DetectorOptions = { order: ['querystring', 'cookie', 'header'], lookupQueryString: 'lang', lookupCookie: 'language', lookupFromHeaderKey: 'accept-language', lookupFromPathIndex: 0, caches: ['cookie'], ignoreCase: true, fallbackLanguage: 'en', supportedLanguages: ['en'], cookieOptions: { sameSite: 'Strict', secure: true, maxAge: 365 * 24 * 60 * 60, httpOnly: true, }, debug: false, } ``` ## 关键行为 ### 检测工作流程 1. **顺序**:默认按此顺序检查来源: - 查询参数 (?lang=ar) - Cookie (language=ar) - Accept-Language header 2. **缓存**:将检测到的语言存储在 cookie 中(默认 1 年) 3. **回退**:如果没有有效检测,则使用 `fallbackLanguage`(必须在 `supportedLanguages` 中) ## 高级配置 ### 自定义检测顺序 优先考虑 URL 路径检测(例如 /en/about): ```ts app.use( languageDetector({ order: ['path', 'cookie', 'querystring', 'header'], lookupFromPathIndex: 0, // /en/profile → index 0 = 'en' supportedLanguages: ['en', 'ar'], fallbackLanguage: 'en', }) ) ``` ### 渐进式区域设置匹配 当检测到的区域设置代码(如 `ja-JP`)不在 `supportedLanguages` 中时,中间件会渐进式地截断子标签以找到匹配项。例如,`zh-Hant-CN` 将尝试 `zh-Hant`,然后尝试 `zh`。始终首选精确匹配。 ```ts app.use( languageDetector({ supportedLanguages: ['en', 'ja', 'zh-Hant'], fallbackLanguage: 'en', }) ) // Accept-Language: ja-JP → 匹配 'ja' // Accept-Language: zh-Hant-CN → 匹配 'zh-Hant' ``` ### 语言代码转换 规范化复杂代码(例如 en-US → en): ```ts app.use( languageDetector({ convertDetectedLanguage: (lang) => lang.split('-')[0], supportedLanguages: ['en', 'ja'], fallbackLanguage: 'en', }) ) ``` ### Cookie 配置 ```ts app.use( languageDetector({ lookupCookie: 'app_lang', caches: ['cookie'], cookieOptions: { path: '/', // Cookie 路径 sameSite: 'Lax', // Cookie same-site 策略 secure: true, // 仅通过 HTTPS 发送 maxAge: 86400 * 365, // 1 年过期 httpOnly: true, // 无法通过 JavaScript 访问 domain: '.example.com', // 可选:特定域 }, }) ) ``` 要禁用 cookie 缓存: ```ts languageDetector({ caches: false, }) ``` ### 调试 记录检测步骤: ```ts languageDetector({ debug: true, // 显示:"Detected from querystring: ar" }) ``` ## 选项参考 ### 基本选项 | 选项 | 类型 | 默认值 | 必需 | 描述 | | :------------------- | :--------------- | :------------------------------------ | :------- | :--------------------- | | `supportedLanguages` | `string[]` | `['en']` | 是 | 允许的语言代码 | | `fallbackLanguage` | `string` | `'en'` | 是 | 默认语言 | | `order` | `DetectorType[]` | `['querystring', 'cookie', 'header']` | 否 | 检测顺序 | | `debug` | `boolean` | `false` | 否 | 启用日志记录 | ### 检测选项 | 选项 | 类型 | 默认值 | 描述 | | :-------------------- | :------- | :------------------ | :------------------- | | `lookupQueryString` | `string` | `'lang'` | 查询参数名称 | | `lookupCookie` | `string` | `'language'` | Cookie 名称 | | `lookupFromHeaderKey` | `string` | `'accept-language'` | Header 名称 | | `lookupFromPathIndex` | `number` | `0` | 路径段索引 | ### Cookie 选项 | 选项 | 类型 | 默认值 | 描述 | | :----------------------- | :---------------------------- | :----------- | :------------------- | | `caches` | `CacheType[] \| false` | `['cookie']` | 缓存设置 | | `cookieOptions.path` | `string` | `'/'` | Cookie 路径 | | `cookieOptions.sameSite` | `'Strict' \| 'Lax' \| 'None'` | `'Strict'` | SameSite 策略 | | `cookieOptions.secure` | `boolean` | `true` | 仅 HTTPS | | `cookieOptions.maxAge` | `number` | `31536000` | 过期时间(秒) | | `cookieOptions.httpOnly` | `boolean` | `true` | JS 可访问性 | | `cookieOptions.domain` | `string` | `undefined` | Cookie 域 | ### 高级选项 | 选项 | 类型 | 默认值 | 描述 | | :------------------------ | :------------------------- | :---------- | :------------------------ | | `ignoreCase` | `boolean` | `true` | 不区分大小写匹配 | | `convertDetectedLanguage` | `(lang: string) => string` | `undefined` | 语言代码转换器 | ## 验证和错误处理 - `fallbackLanguage` 必须在 `supportedLanguages` 中(设置时抛出错误) - `lookupFromPathIndex` 必须 ≥ 0 - 无效配置在中间件初始化期间抛出错误 - 失败的检测静默使用 `fallbackLanguage` ## 常见示例 ### 基于路径的路由 ```ts app.get('/:lang/home', (c) => { const lang = c.get('language') // 'en', 'ar' 等 return c.json({ message: getLocalizedContent(lang) }) }) ``` ### 多种支持的语言 ```ts languageDetector({ supportedLanguages: ['en', 'en-GB', 'ar', 'ar-EG'], convertDetectedLanguage: (lang) => lang.replace('_', '-'), // 规范化 }) ``` # Logger 中间件 它是一个简单的记录器。 ## 导入 ```ts import { Hono } from 'hono' import { logger } from 'hono/logger' ``` ## 用法 ```ts const app = new Hono() app.use(logger()) app.get('/', (c) => c.text('Hello Hono!')) ``` ## 记录详情 Logger 中间件为每个请求记录以下详情: - **传入请求**:记录 HTTP 方法、请求路径和传入请求。 - **传出响应**:记录 HTTP 方法、请求路径、响应状态码和请求/响应时间。 - **状态码着色**:响应状态码进行颜色编码,以提高可见性和快速识别状态类别。不同的状态码类别由不同的颜色表示。 - **经过时间**:请求/响应周期所花费的时间以人类可读的格式记录,以毫秒 (ms) 或秒 (s) 为单位。 通过使用 Logger 中间件,你可以轻松监控 Hono 应用程序中的请求和响应流,并快速识别任何问题或性能瓶颈。 你还可以通过提供自己的 `PrintFunc` 函数来进一步扩展中间件,以实现定制的记录行为。 ::: tip 要禁用_状态码着色_,你可以设置 `NO_COLOR` 环境变量。这是在记录库中禁用 ANSI 颜色转义代码的常见方法,并在 中描述。请注意,Cloudflare Workers 没有 `process.env` 对象,因此将默认为纯文本日志输出。 ::: ## PrintFunc Logger 中间件接受可选的 `PrintFunc` 函数作为参数。此函数允许你自定义记录器并添加额外的日志。 ## 选项 ### fn: `PrintFunc(str: string, ...rest: string[])` - `str`:由记录器传递。 - `...rest`:要打印到控制台的其他字符串属性。 ### 示例 将自定义 `PrintFunc` 函数设置到 Logger 中间件: ```ts export const customLogger = (message: string, ...rest: string[]) => { console.log(message, ...rest) } app.use(logger(customLogger)) ``` 在路由中设置自定义记录器: ```ts app.post('/blog', (c) => { // 路由逻辑 customLogger('Blog saved:', `Path: ${blog.url},`, `ID: ${blog.id}`) // 输出 // <-- POST /blog // Blog saved: Path: /blog/example, ID: 1 // --> POST /blog 201 93ms // 返回 Context }) ``` # Method Override 中间件 此中间件根据 form、header 或 query 的值执行指定方法的 handler(与请求的实际方法不同),并返回其响应。 ## 导入 ```ts import { Hono } from 'hono' import { methodOverride } from 'hono/method-override' ``` ## 用法 ```ts const app = new Hono() // 如果未指定选项,则使用 form 中的 `_method` 的值, // 例如 DELETE,作为方法。 app.use('/posts', methodOverride({ app })) app.delete('/posts', (c) => { // .... }) ``` ## 例如 由于 HTML 表单无法发送 DELETE 方法,你可以将值 `DELETE` 放入名为 `_method` 的属性中并发送。然后 `app.delete()` 的 handler 将执行。 HTML 表单: ```html
``` 应用程序: ```ts import { methodOverride } from 'hono/method-override' const app = new Hono() app.use('/posts', methodOverride({ app })) app.delete('/posts', () => { // ... }) ``` 你可以更改默认值或使用 header 值和 query 值: ```ts app.use('/posts', methodOverride({ app, form: '_custom_name' })) app.use( '/posts', methodOverride({ app, header: 'X-METHOD-OVERRIDE' }) ) app.use('/posts', methodOverride({ app, query: '_method' })) ``` ## 选项 ### app: `Hono` 你的应用程序中使用的 `Hono` 实例。 ### form: `string` 包含方法名的值的 Form 键。 默认值为 `_method`。 ### header: `boolean` 包含方法名的值的 Header 名称。 ### query: `boolean` 包含方法名的值的 Query 参数键。 # Pretty JSON 中间件 Pretty JSON 中间件为 JSON 响应体启用"_JSON 美化打印_"。 将 `?pretty` 添加到 URL 查询参数,JSON 字符串将被美化。 ```js // GET / {"project":{"name":"Hono","repository":"https://github.com/honojs/hono"}} ``` 将变为: ```js // GET /?pretty { "project": { "name": "Hono", "repository": "https://github.com/honojs/hono" } } ``` ## 导入 ```ts import { Hono } from 'hono' import { prettyJSON } from 'hono/pretty-json' ``` ## 用法 ```ts const app = new Hono() app.use(prettyJSON()) // 带有选项:prettyJSON({ space: 4 }) app.get('/', (c) => { return c.json({ message: 'Hono!' }) }) ``` ## 选项 ### space: `number` 缩进的空格数。默认值为 `2`。 ### query: `string` 应用的查询字符串名称。默认值为 `pretty`。 ### force: `boolean` 设置为 `true` 时,JSON 响应将始终美化,无论查询参数如何。默认值为 `false`。 # Request ID 中间件 Request ID 中间件为每个请求生成唯一的 ID,你可以在 handlers 中使用它。 ::: info **Node.js**:此中间件使用 `crypto.randomUUID()` 生成 ID。全局 `crypto` 在 Node.js 20 或更高版本中引入。因此,在更早的版本中可能会出现错误。在这种情况下,请指定 `generator`。但是,如果你使用 [Node.js 适配器](https://github.com/honojs/node-server),它会自动全局设置 `crypto`,因此不需要这样做。 ::: ## 导入 ```ts import { Hono } from 'hono' import { requestId } from 'hono/request-id' ``` ## 用法 你可以通过应用了 Request ID 中间件的 handlers 和中间件中的 `requestId` 变量访问 Request ID。 ```ts const app = new Hono() app.use('*', requestId()) app.get('/', (c) => { return c.text(`Your request id is ${c.get('requestId')}`) }) ``` 如果你想明确指定类型,请导入 `RequestIdVariables` 并在 `new Hono()` 的泛型中传递它。 ```ts import type { RequestIdVariables } from 'hono/request-id' const app = new Hono<{ Variables: RequestIdVariables }>() ``` ### 设置 Request ID 如果你在 header(默认:`X-Request-Id`)中设置自定义 request ID,中间件将使用该值而不是生成新值: ```ts const app = new Hono() app.use('*', requestId()) app.get('/', (c) => { return c.text(`${c.get('requestId')}`) }) const res = await app.request('/', { headers: { 'X-Request-Id': 'your-custom-id', }, }) console.log(await res.text()) // your-custom-id ``` 如果你想禁用此功能,请将 [`headerName` 选项](#headername-string) 设置为空字符串。 ## 选项 ### limitLength: `number` Request ID 的最大长度。默认值为 `255`。 ### headerName: `string` 用于 Request ID 的 header 名称。默认值为 `X-Request-Id`。 ### generator: `(c: Context) => string` Request ID 生成函数。默认情况下,它使用 `crypto.randomUUID()`。 ## 平台特定的 Request IDs 一些平台(如 AWS Lambda)已经为每个请求生成自己的 Request IDs。 无需任何额外配置,此中间件不知道这些特定的 Request IDs 并生成新的 Request ID。这可能在查看应用程序日志时导致混淆。 要统一这些 ID,请使用 `generator` 函数捕获平台特定的 Request ID 并在此中间件中使用它。 ### 平台特定链接 - AWS Lambda - [AWS 文档:Context 对象](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-context.html) - [Hono:访问 AWS Lambda 对象](/docs/getting-started/aws-lambda#access-aws-lambda-object) - Cloudflare - [Cloudflare Ray ID](https://developers.cloudflare.com/fundamentals/reference/cloudflare-ray-id/) - Deno - [Deno 博客上的 Request ID](https://deno.com/blog/zero-config-debugging-deno-opentelemetry#:~:text=s%20automatically%20have-,unique%20request%20IDs,-associated%20with%20them) - Fastly - [Fastly 文档:req.xid](https://www.fastly.com/documentation/reference/vcl/variables/client-request/req-xid/) # Secure Headers 中间件 Secure Headers 中间件简化了安全 headers 的设置。部分受到 Helmet 功能的启发,它允许你控制特定安全 headers 的激活和停用。 ## 导入 ```ts import { Hono } from 'hono' import { secureHeaders } from 'hono/secure-headers' ``` ## 用法 你可以使用默认的最佳设置。 ```ts const app = new Hono() app.use(secureHeaders()) ``` 你可以通过将它们设置为 false 来抑制不必要的 headers。 ```ts const app = new Hono() app.use( '*', secureHeaders({ xFrameOptions: false, xXssProtection: false, }) ) ``` 你可以使用字符串覆盖默认的 header 值。 ```ts const app = new Hono() app.use( '*', secureHeaders({ strictTransportSecurity: 'max-age=63072000; includeSubDomains; preload', xFrameOptions: 'DENY', xXssProtection: '1', }) ) ``` ## 支持的选项 每个选项对应以下 Header 键值对。 | 选项 | Header | 值 | 默认值 | | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------- | | - | X-Powered-By | (删除 Header) | True | | contentSecurityPolicy | [Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) | 用法:[设置 Content-Security-Policy](#setting-content-security-policy) | 无设置 | | contentSecurityPolicyReportOnly | [Content-Security-Policy-Report-Only](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only) | 用法:[设置 Content-Security-Policy](#setting-content-security-policy) | 无设置 | | trustedTypes | [Trusted Types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types) | 用法:[设置 Content-Security-Policy](#setting-content-security-policy) | 无设置 | | requireTrustedTypesFor | [Require Trusted Types For](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/require-trusted-types-for) | 用法:[设置 Content-Security-Policy](#setting-content-security-policy) | 无设置 | | crossOriginEmbedderPolicy | [Cross-Origin-Embedder-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy) | require-corp | **False** | | crossOriginResourcePolicy | [Cross-Origin-Resource-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy) | same-origin | True | | crossOriginOpenerPolicy | [Cross-Origin-Opener-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy) | same-origin | True | | originAgentCluster | [Origin-Agent-Cluster](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin-Agent-Cluster) | ?1 | True | | referrerPolicy | [Referrer-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy) | no-referrer | True | | reportingEndpoints | [Reporting-Endpoints](https://www.w3.org/TR/reporting-1/#header) | 用法:[设置 Content-Security-Policy](#setting-content-security-policy) | 无设置 | | reportTo | [Report-To](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-to) | 用法:[设置 Content-Security-Policy](#setting-content-security-policy) | 无设置 | | strictTransportSecurity | [Strict-Transport-Security](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security) | max-age=15552000; includeSubDomains | True | | xContentTypeOptions | [X-Content-Type-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options) | nosniff | True | | xDnsPrefetchControl | [X-DNS-Prefetch-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control) | off | True | | xDownloadOptions | [X-Download-Options](https://learn.microsoft.com/en-us/archive/blogs/ie/ie8-security-part-v-comprehensive-protection#mime-handling-force-save) | noopen | True | | xFrameOptions | [X-Frame-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) | SAMEORIGIN | True | | xPermittedCrossDomainPolicies | [X-Permitted-Cross-Domain-Policies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Permitted-Cross-Domain-Policies) | none | True | | xXssProtection | [X-XSS-Protection](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection) | 0 | True | | permissionPolicy | [Permissions-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy) | 用法:[设置 Permission-Policy](#setting-permission-policy) | 无设置 | ## 中间件冲突 在处理操作相同 header 的中间件时,请注意指定顺序。 在这种情况下,Secure-headers 运行并删除 `x-powered-by`: ```ts const app = new Hono() app.use(secureHeaders()) app.use(poweredBy()) ``` 在这种情况下,Powered-By 运行并添加 `x-powered-by`: ```ts const app = new Hono() app.use(poweredBy()) app.use(secureHeaders()) ``` ## 设置 Content-Security-Policy ```ts const app = new Hono() app.use( '/test', secureHeaders({ reportingEndpoints: [ { name: 'endpoint-1', url: 'https://example.com/reports', }, ], // 或 alternatively // reportTo: [ // { // group: 'endpoint-1', // max_age: 10886400, // endpoints: [{ url: 'https://example.com/reports' }], // }, // ], contentSecurityPolicy: { defaultSrc: ["'self'"], baseUri: ["'self'"], childSrc: ["'self'"], connectSrc: ["'self'"], fontSrc: ["'self'", 'https:', 'data:'], formAction: ["'self'"], frameAncestors: ["'self'"], frameSrc: ["'self'"], imgSrc: ["'self'", 'data:'], manifestSrc: ["'self'"], mediaSrc: ["'self'"], objectSrc: ["'none'"], reportTo: 'endpoint-1', reportUri: '/csp-report', sandbox: ['allow-same-origin', 'allow-scripts'], scriptSrc: ["'self'"], scriptSrcAttr: ["'none'"], scriptSrcElem: ["'self'"], styleSrc: ["'self'", 'https:', "'unsafe-inline'"], styleSrcAttr: ['none'], styleSrcElem: ["'self'", 'https:', "'unsafe-inline'"], upgradeInsecureRequests: [], workerSrc: ["'self'"], }, }) ) ``` ### `nonce` 属性 你可以通过将从 `hono/secure-headers` 导入的 `NONCE` 添加到 `scriptSrc` 或 `styleSrc`,将 [`nonce` 属性](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce) 添加到 `script` 或 `style` 元素: ```tsx import { secureHeaders, NONCE } from 'hono/secure-headers' import type { SecureHeadersVariables } from 'hono/secure-headers' // 指定变量类型以推断 `c.get('secureHeadersNonce')`: type Variables = SecureHeadersVariables const app = new Hono<{ Variables: Variables }>() // 将预定义的 nonce 值设置为 `scriptSrc`: app.get( '*', secureHeaders({ contentSecurityPolicy: { scriptSrc: [NONCE, 'https://allowed1.example.com'], }, }) ) // 从 `c.get('secureHeadersNonce')` 获取值: app.get('/', (c) => { return c.html( {/** contents */} `} Hello! ) }) ``` ### 作为功能组件 由于 `html` 返回 HtmlEscapedString,它可以作为完全功能组件而无需使用 JSX。 #### 使用 `html` 来加速处理而不是 `memo` ```typescript const Footer = () => html`
My Address...
` ``` ### 接收 props 并嵌入值 ```typescript interface SiteData { title: string description: string image: string children?: any } const Layout = (props: SiteData) => html` ${props.title} ${props.children} ` const Content = (props: { siteData: SiteData; name: string }) => (

Hello {props.name}

) app.get('/', (c) => { const props = { name: 'World', siteData: { title: 'Hello <> World', description: 'This is a description', image: 'https://example.com/image.png', }, } return c.html() }) ``` ## `raw()` ```ts app.get('/', (c) => { const name = 'John "Johnny" Smith' return c.html(html`

I'm ${raw(name)}.

`) }) ``` ## 技巧 由于这些库,Visual Studio Code 和 vim 也将模板字面量解释为 HTML,允许应用语法高亮和格式化。 - - # JWT 认证辅助工具 此辅助工具提供用于编码、解码、签名和验证 JSON Web Tokens (JWTs) 的函数。JWTs 通常在 Web 应用程序中用于认证和授权。此辅助工具提供强大的 JWT 功能,支持各种加密算法。 ## 导入 要使用此辅助工具,你可以按以下方式导入: ```ts import { decode, sign, verify } from 'hono/jwt' ``` ::: info [JWT 中间件](/docs/middleware/builtin/jwt) 也从 `hono/jwt` 导入 `jwt` 函数。 ::: ## `sign()` 此函数通过使用指定的算法和密钥对 payload 进行编码和签名来生成 JWT 令牌。 ```ts sign( payload: unknown, secret: string, alg?: 'HS256'; ): Promise; ``` ### 示例 ```ts import { sign } from 'hono/jwt' const payload = { sub: 'user123', role: 'admin', exp: Math.floor(Date.now() / 1000) + 60 * 5, // 令牌 5 分钟后过期 } const secret = 'mySecretKey' const token = await sign(payload, secret) ``` ### 选项
#### payload: `unknown` 要签名的 JWT payload。你可以包含其他 claims,如 [Payload Validation](#payload-validation) 中所述。 #### secret: `string` 用于 JWT 验证或签名的密钥。 #### alg: [AlgorithmTypes](#supported-algorithmtypes) 用于 JWT 签名或验证的算法。默认是 HS256。 ## `verify()` 此函数检查 JWT 令牌是否真实且仍然有效。它确保令牌未被篡改,并且仅在你添加了 [Payload Validation](#payload-validation) 时检查有效性。 ```ts verify( token: string, secret: string, alg: 'HS256'; issuer?: string | RegExp; ): Promise; ``` ### 示例 ```ts import { verify } from 'hono/jwt' const tokenToVerify = 'token' const secretKey = 'mySecretKey' const decodedPayload = await verify(tokenToVerify, secretKey, 'HS256') console.log(decodedPayload) ``` ### 选项
#### token: `string` 要验证的 JWT 令牌。 #### secret: `string` 用于 JWT 验证或签名的密钥。 #### alg: [AlgorithmTypes](#supported-algorithmtypes) 用于 JWT 签名或验证的算法。 #### issuer: `string | RegExp` 用于 JWT 验证的预期颁发者。 ## `decode()` 此函数在不执行签名验证的情况下解码 JWT 令牌。它从令牌中提取并返回 header 和 payload。 ```ts decode(token: string): { header: any; payload: any }; ``` ### 示例 ```ts import { decode } from 'hono/jwt' // 解码 JWT 令牌 const tokenToDecode = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJzdWIiOiAidXNlcjEyMyIsICJyb2xlIjogImFkbWluIn0.JxUwx6Ua1B0D1B0FtCrj72ok5cm1Pkmr_hL82sd7ELA' const { header, payload } = decode(tokenToDecode) console.log('Decoded Header:', header) console.log('Decoded Payload:', payload) ``` ### 选项
#### token: `string` 要解码的 JWT 令牌。 > `decode` 函数允许你在_**不**_ 执行验证的情况下检查 JWT 令牌的 header 和 payload。这对于调试或从 JWT 令牌中提取信息很有用。 ## Payload 验证 验证 JWT 令牌时,会执行以下 payload 验证: - `exp`:检查令牌是否未过期。 - `nbf`:检查令牌是否在指定时间之前未被使用。 - `iat`:检查令牌是否未在未来签发。 - `iss`:检查令牌是否由受信任的颁发者签发。 如果你打算在验证期间执行这些检查,请确保你的 JWT payload 包含这些字段(作为对象)。 ## 自定义错误类型 该模块还定义了自定义错误类型来处理 JWT 相关错误。 - `JwtAlgorithmNotImplemented`:表示未实现请求的 JWT 算法。 - `JwtTokenInvalid`:表示 JWT 令牌无效。 - `JwtTokenNotBefore`:表示令牌在其有效日期之前被使用。 - `JwtTokenExpired`:表示令牌已过期。 - `JwtTokenIssuedAt`:表示令牌中的 "iat" claim 不正确。 - `JwtTokenIssuer`:表示令牌中的 "iss" claim 不正确。 - `JwtTokenSignatureMismatched`:表示令牌中的签名不匹配。 ## 支持的 AlgorithmTypes 该模块支持以下 JWT 加密算法: - `HS256`:使用 SHA-256 的 HMAC - `HS384`:使用 SHA-384 的 HMAC - `HS512`:使用 SHA-512 的 HMAC - `RS256`:使用 SHA-256 的 RSASSA-PKCS1-v1_5 - `RS384`:使用 SHA-384 的 RSASSA-PKCS1-v1_5 - `RS512`:使用 SHA-512 的 RSASSA-PKCS1-v1_5 - `PS256`:使用 SHA-256 和 MGF1 with SHA-256 的 RSASSA-PSS - `PS384`:使用 SHA-386 和 MGF1 with SHA-386 的 RSASSA-PSS - `PS512`:使用 SHA-512 和 MGF1 with SHA-512 的 RSASSA-PSS - `ES256`:使用 P-256 和 SHA-256 的 ECDSA - `ES384`:使用 P-384 和 SHA-384 的 ECDSA - `ES512`:使用 P-521 和 SHA-512 的 ECDSA - `EdDSA`:使用 Ed25519 的 EdDSA # Proxy 辅助工具 Proxy 辅助工具在将 Hono 应用程序用作(反向)代理时提供有用的函数。 ## 导入 ```ts import { Hono } from 'hono' import { proxy } from 'hono/proxy' ``` ## `proxy()` `proxy()` 是用于代理的 `fetch()` API 包装器。参数和返回值与 `fetch()` 相同(代理特定选项除外)。 `Accept-Encoding` header 被替换为当前运行时可以处理的编码。不必要的响应 headers 被删除,并返回可以从 handler 发送的 `Response` 对象。 ### 示例 简单用法: ```ts app.get('/proxy/:path', (c) => { return proxy(`http://${originServer}/${c.req.param('path')}`) }) ``` 复杂用法: ```ts app.get('/proxy/:path', async (c) => { const res = await proxy( `http://${originServer}/${c.req.param('path')}`, { headers: { ...c.req.header(), // 可选,仅在需要转发所有请求数据(包括凭据)时指定。 'X-Forwarded-For': '127.0.0.1', 'X-Forwarded-Host': c.req.header('host'), Authorization: undefined, // 不传播包含在 c.req.header('Authorization') 中的请求 headers }, } ) res.headers.delete('Set-Cookie') return res }) ``` 或者你可以将 `c.req` 作为参数传递。 ```ts app.all('/proxy/:path', (c) => { return proxy(`http://${originServer}/${c.req.param('path')}`, { ...c.req, // 可选,仅在需要转发所有请求数据(包括凭据)时指定。 headers: { ...c.req.header(), 'X-Forwarded-For': '127.0.0.1', 'X-Forwarded-Host': c.req.header('host'), Authorization: undefined, // 不传播包含在 c.req.header('Authorization') 中的请求 headers }, }) }) ``` 你可以使用 `customFetch` 选项覆盖默认全局 `fetch` 函数: ```ts app.get('/proxy', (c) => { return proxy('https://example.com/', { customFetch, }) }) ``` ### Connection Header 处理 默认情况下,`proxy()` 忽略 `Connection` header 以防止 Hop-by-Hop Header 注入攻击。你可以使用 `strictConnectionProcessing` 选项启用严格的 RFC 9110 合规性: ```ts // 默认行为(推荐用于不受信任的客户端) app.get('/proxy/:path', (c) => { return proxy(`http://${originServer}/${c.req.param('path')}`, c.req) }) // 严格的 RFC 9110 合规性(仅在受信任的环境中使用) app.get('/internal-proxy/:path', (c) => { return proxy(`http://${internalServer}/${c.req.param('path')}`, { ...c.req, strictConnectionProcessing: true, }) }) ``` ### `ProxyFetch` `proxy()` 的类型定义为 `ProxyFetch`,如下所示 ```ts interface ProxyRequestInit extends Omit { raw?: Request customFetch?: (request: Request) => Promise strictConnectionProcessing?: boolean headers?: | HeadersInit | [string, string][] | Record | Record } interface ProxyFetch { ( input: string | URL | Request, init?: ProxyRequestInit ): Promise } ``` # Route 辅助工具 Route 辅助工具为调试和中间件开发提供增强的路由信息。它允许你访问有关匹配路由和当前正在处理的路由的详细信息。 ## 导入 ```ts import { Hono } from 'hono' import { matchedRoutes, routePath, baseRoutePath, basePath, } from 'hono/route' ``` ## 用法 ### 基本路由信息 ```ts const app = new Hono() app.get('/posts/:id', (c) => { const currentPath = routePath(c) // '/posts/:id' const routes = matchedRoutes(c) // 匹配路由的数组 return c.json({ path: currentPath, totalRoutes: routes.length, }) }) ``` ### 与子应用程序一起使用 ```ts const app = new Hono() const apiApp = new Hono() apiApp.get('/posts/:id', (c) => { return c.json({ routePath: routePath(c), // '/posts/:id' baseRoutePath: baseRoutePath(c), // '/api' basePath: basePath(c), // '/api'(带有实际参数) }) }) app.route('/api', apiApp) ``` ## `matchedRoutes()` 返回与当前请求匹配的所有路由的数组,包括中间件。 ```ts app.all('/api/*', (c, next) => { console.log('API middleware') return next() }) app.get('/api/users/:id', (c) => { const routes = matchedRoutes(c) // 返回:[ // { method: 'ALL', path: '/api/*', handler: [Function] }, // { method: 'GET', path: '/api/users/:id', handler: [Function] } // ] return c.json({ routes: routes.length }) }) ``` ## `routePath()` 返回为当前 handler 注册的路由路径模式。 ```ts app.get('/posts/:id', (c) => { console.log(routePath(c)) // '/posts/:id' return c.text('Post details') }) ``` ### 与 index 参数一起使用 你可以选择传递 index 参数来获取特定位置的路由路径,类似于 `Array.prototype.at()`。 ```ts app.all('/api/*', (c, next) => { return next() }) app.get('/api/users/:id', (c) => { console.log(routePath(c, 0)) // '/api/*'(第一个匹配的路由) console.log(routePath(c, -1)) // '/api/users/:id'(最后一个匹配的路由) return c.text('User details') }) ``` ## `baseRoutePath()` 返回当前路由在路由中指定的基础路径模式。 ```ts const subApp = new Hono() subApp.get('/posts/:id', (c) => { return c.text(baseRoutePath(c)) // '/:sub' }) app.route('/:sub', subApp) ``` ### 与 index 参数一起使用 你可以选择传递 index 参数来获取特定位置的基础路由路径,类似于 `Array.prototype.at()`。 ```ts app.all('/api/*', (c, next) => { return next() }) const subApp = new Hono() subApp.get('/users/:id', (c) => { console.log(baseRoutePath(c, 0)) // '/'(第一个匹配的路由) console.log(baseRoutePath(c, -1)) // '/api'(最后一个匹配的路由) return c.text('User details') }) app.route('/api', subApp) ``` ## `basePath()` 返回来自实际请求的带有嵌入式参数的基础路径。 ```ts const subApp = new Hono() subApp.get('/posts/:id', (c) => { return c.text(basePath(c)) // '/api'(对于对 '/api/posts/123' 的请求) }) app.route('/:sub', subApp) ``` # SSG 辅助工具 SSG 辅助工具从你的 Hono 应用程序生成静态站点。它将检索已注册路由的内容并将它们保存为静态文件。 ## 用法 ### 手动 如果你有以下简单的 Hono 应用程序: ```tsx // index.tsx const app = new Hono() app.get('/', (c) => c.html('Hello, World!')) app.use('/about', async (c, next) => { c.setRenderer((content) => { return c.html(

{content}

) }) await next() }) app.get('/about', (c) => { return c.render( <> Hono SSG PageHello! ) }) export default app ``` 对于 Node.js,创建如下构建脚本: ```ts // build.ts import app from './index' import { toSSG } from 'hono/ssg' import fs from 'fs/promises' toSSG(app, fs) ``` 执行脚本后,文件将输出如下: ```bash ls ./static about.html index.html ``` ### Vite 插件 使用 `@hono/vite-ssg` Vite 插件,你可以轻松处理此过程。 有关更多详细信息,请参阅: https://github.com/honojs/vite-plugins/tree/main/packages/ssg ## toSSG `toSSG` 是生成静态站点的主要函数,接受应用程序和文件系统模块作为参数。它基于以下内容: ### 输入 toSSG 的参数在 ToSSGInterface 中指定。 ```ts export interface ToSSGInterface { ( app: Hono, fsModule: FileSystemModule, options?: ToSSGOptions ): Promise } ``` - `app` 指定已注册路由的 `new Hono()`。 - `fs` 指定以下对象,假设 `node:fs/promise`。 ```ts export interface FileSystemModule { writeFile(path: string, data: string | Uint8Array): Promise mkdir( path: string, options: { recursive: boolean } ): Promise } ``` ### 使用 Deno 和 Bun 的适配器 如果你想在 Deno 或 Bun 上使用 SSG,则为每个文件系统提供了 `toSSG` 函数。 对于 Deno: ```ts import { toSSG } from 'hono/deno' toSSG(app) // 第二个参数是类型为 `ToSSGOptions` 的选项。 ``` 对于 Bun: ```ts import { toSSG } from 'hono/bun' toSSG(app) // 第二个参数是类型为 `ToSSGOptions` 的选项。 ``` ### 选项 选项在 ToSSGOptions 接口中指定。 ```ts export interface ToSSGOptions { dir?: string concurrency?: number extensionMap?: Record plugins?: SSGPlugin[] } ``` - `dir` 是静态文件的输出目的地。默认值为 `./static`。 - `concurrency` 是同时生成的文件并发数。默认值为 `2`。 - `extensionMap` 是包含 `Content-Type` 作为键和扩展名字符串作为值的映射。这用于确定输出文件的文件扩展名。 - `plugins` 是扩展静态站点生成过程功能的 SSG 插件数组。 ### 输出 `toSSG` 返回以下 Result 类型的结果。 ```ts export interface ToSSGResult { success: boolean files: string[] error?: Error } ``` ## 生成文件 ### 路由和文件名 以下规则适用于已注册的路由信息和生成的文件名。默认 `./static` 的行为如下: - `/` -> `./static/index.html` - `/path` -> `./static/path.html` - `/path/` -> `./static/path/index.html` ### 文件扩展名 文件扩展名取决于每个路由返回的 `Content-Type`。例如,来自 `c.html` 的响应保存为 `.html`。 如果你想自定义文件扩展名,请设置 `extensionMap` 选项。 ```ts import { toSSG, defaultExtensionMap } from 'hono/ssg' // 将 `application/x-html` 内容保存为 `.html` toSSG(app, fs, { extensionMap: { 'application/x-html': 'html', ...defaultExtensionMap, }, }) ``` 请注意,以斜杠结尾的路径无论扩展名如何都保存为 index.ext。 ```ts // 保存到 ./static/html/index.html app.get('/html/', (c) => c.html('html')) // 保存到 ./static/text/index.txt app.get('/text/', (c) => c.text('text')) ``` ## 中间件 介绍支持 SSG 的内置中间件。 ### ssgParams 你可以使用类似于 Next.js 的 `generateStaticParams` 的 API。 示例: ```ts app.get( '/shops/:id', ssgParams(async () => { const shops = await getShops() return shops.map((shop) => ({ id: shop.id })) }), async (c) => { const shop = await getShop(c.req.param('id')) if (!shop) { return c.notFound() } return c.render(

{shop.name}

) } ) ``` ### isSSGContext `isSSGContext` 是一个辅助函数,如果当前应用程序在由 `toSSG` 触发的 SSG 上下文中运行,则返回 `true`。 ```ts app.get('/page', (c) => { if (isSSGContext(c)) { return c.text('This is generated by SSG') } return c.text('This is served dynamically') }) ``` ### disableSSG 设置了 `disableSSG` 中间件的路由将从 `toSSG` 的静态文件生成中排除。 ```ts app.get('/api', disableSSG(), (c) => c.text('an-api')) ``` ### onlySSG 设置了 `onlySSG` 中间件的路由将在 `toSSG` 执行后被 `c.notFound()` 覆盖。 ```ts app.get('/static-page', onlySSG(), (c) => c.html(

Welcome to my site

)) ``` ## 插件 插件允许你扩展静态站点生成过程的功能。它们使用 hooks 在生成过程的不同阶段进行自定义。 ### 默认插件 默认情况下,`toSSG` 使用 `defaultPlugin`,它跳过非 200 状态响应(如重定向、错误或 404)。这可以防止为不成功的响应生成文件。 ```ts import { toSSG, defaultPlugin } from 'hono/ssg' // 未指定插件时自动应用 defaultPlugin toSSG(app, fs) // 等价于: toSSG(app, fs, { plugins: [defaultPlugin] }) ``` 如果你指定了自定义插件,`defaultPlugin` **不**会自动包含。要在添加自定义插件的同时保持默认行为,请显式包含它: ```ts toSSG(app, fs, { plugins: [defaultPlugin, myCustomPlugin], }) ``` ### 重定向插件 `redirectPlugin` 为返回 HTTP 重定向响应(301、302、303、307、308)的路由生成 HTML 重定向页面。生成的 HTML 包含 `` 标签和规范链接。 ```ts import { toSSG, redirectPlugin, defaultPlugin } from 'hono/ssg' toSSG(app, fs, { plugins: [redirectPlugin(), defaultPlugin()], }) ``` 例如,如果你的应用程序有: ```ts app.get('/old', (c) => c.redirect('/new')) ``` `redirectPlugin` 将在 `/old.html` 生成一个包含 meta refresh 重定向到 `/new` 的 HTML 文件。 > [!NOTE] > 与 `defaultPlugin` 一起使用时,将 `redirectPlugin` 放在 `defaultPlugin` **之前**。由于 `defaultPlugin` 跳过非 200 响应,先放置它会阻止 `redirectPlugin` 处理重定向响应。 ### Hook 类型 插件可以使用以下 hooks 来自定义 `toSSG` 过程: ```ts export type BeforeRequestHook = (req: Request) => Request | false export type AfterResponseHook = (res: Response) => Response | false export type AfterGenerateHook = ( result: ToSSGResult ) => void | Promise ``` - **BeforeRequestHook**:在处理每个请求之前调用。返回 `false` 跳过路由。 - **AfterResponseHook**:在接收每个响应之后调用。返回 `false` 跳过文件生成。 - **AfterGenerateHook**:在整个生成过程完成后调用。 ### 插件接口 ```ts export interface SSGPlugin { beforeRequestHook?: BeforeRequestHook | BeforeRequestHook[] afterResponseHook?: AfterResponseHook | AfterResponseHook[] afterGenerateHook?: AfterGenerateHook | AfterGenerateHook[] } ``` ### 基本插件示例 仅过滤 GET 请求: ```ts const getOnlyPlugin: SSGPlugin = { beforeRequestHook: (req) => { if (req.method === 'GET') { return req } return false }, } ``` 按状态码过滤: ```ts const statusFilterPlugin: SSGPlugin = { afterResponseHook: (res) => { if (res.status === 200 || res.status === 500) { return res } return false }, } ``` 记录生成的文件: ```ts const logFilesPlugin: SSGPlugin = { afterGenerateHook: (result) => { if (result.files) { result.files.forEach((file) => console.log(file)) } }, } ``` ### 高级插件示例 这是一个创建生成 `sitemap.xml` 文件的 sitemap 插件的示例: ```ts // plugins.ts import fs from 'node:fs/promises' import path from 'node:path' import type { SSGPlugin } from 'hono/ssg' import { DEFAULT_OUTPUT_DIR } from 'hono/ssg' export const sitemapPlugin = (baseURL: string): SSGPlugin => { return { afterGenerateHook: (result, fsModule, options) => { const outputDir = options?.dir ?? DEFAULT_OUTPUT_DIR const filePath = path.join(outputDir, 'sitemap.xml') const urls = result.files.map((file) => new URL(file, baseURL).toString() ) const siteMapText = ` ${urls.map((url) => `${url}`).join('\n')} ` fsModule.writeFile(filePath, siteMapText) }, } } ``` 应用插件: ```ts import app from './index' import { toSSG } from 'hono/ssg' import { sitemapPlugin } from './plugins' toSSG(app, fs, { plugins: [ getOnlyPlugin, statusFilterPlugin, logFilesPlugin, sitemapPlugin('https://example.com'), ], }) ``` # Streaming 辅助工具 Streaming 辅助工具提供用于流式响应的方法。 ## 导入 ```ts import { Hono } from 'hono' import { stream, streamText, streamSSE } from 'hono/streaming' ``` ## `stream()` 它返回简单的流式响应作为 `Response` 对象。 ```ts app.get('/stream', (c) => { return stream(c, async (stream) => { // 写入中止时执行的过程。 stream.onAbort(() => { console.log('Aborted!') }) // 写入 Uint8Array。 await stream.write(new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f])) // 管道可读流。 await stream.pipe(anotherReadableStream) }) }) ``` ## `streamText()` 它返回带有 `Content-Type:text/plain`、`Transfer-Encoding:chunked` 和 `X-Content-Type-Options:nosniff` headers 的流式响应。 ```ts app.get('/streamText', (c) => { return streamText(c, async (stream) => { // 写入带有新行 ('\n') 的文本。 await stream.writeln('Hello') // 等待 1 秒。 await stream.sleep(1000) // 写入不带新行的文本。 await stream.write(`Hono!`) }) }) ``` ::: warning 如果你正在为 Cloudflare Workers 开发应用程序,流式可能在 Wrangler 上无法正常工作。如果是这样,请为 `Content-Encoding` header 添加 `Identity`。 ```ts app.get('/streamText', (c) => { c.header('Content-Encoding', 'Identity') return streamText(c, async (stream) => { // ... }) }) ``` ::: ## `streamSSE()` 它允许你无缝流式传输 Server-Sent Events (SSE)。 ```ts const app = new Hono() let id = 0 app.get('/sse', async (c) => { return streamSSE(c, async (stream) => { while (true) { const message = `It is ${new Date().toISOString()}` await stream.writeSSE({ data: message, event: 'time-update', id: String(id++), }) await stream.sleep(1000) } }) }) ``` ## 错误处理 流式辅助工具的第三个参数是错误处理器。 此参数是可选的,如果你不指定它,错误将作为控制台错误输出。 ```ts app.get('/stream', (c) => { return stream( c, async (stream) => { // 写入中止时执行的过程。 stream.onAbort(() => { console.log('Aborted!') }) // 写入 Uint8Array。 await stream.write( new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]) ) // 管道可读流。 await stream.pipe(anotherReadableStream) }, (err, stream) => { stream.writeln('An error occurred!') console.error(err) } ) }) ``` 流将在回调执行后自动关闭。 ::: warning 如果流式辅助工具的回调函数抛出错误,Hono 的 `onError` 事件将不会触发。 `onError` 是在发送响应之前处理错误并覆盖响应的钩子。但是,当回调函数执行时,流式已经开始,因此无法覆盖。 ::: # Testing 辅助工具 Testing 辅助工具提供函数,使 Hono 应用程序的测试更容易。 ## 导入 ```ts import { Hono } from 'hono' import { testClient } from 'hono/testing' ``` ## `testClient()` `testClient()` 函数将 Hono 实例作为第一个参数,并返回根据 Hono 应用程序的路由进行类型化的对象,类似于 [Hono Client](/docs/guides/rpc#client)。这允许你在测试中以类型安全的方式调用已定义的路由,并具有编辑器自动完成功能。 **关于类型推断的重要说明:** 为了让 `testClient` 正确推断路由类型并提供自动完成,**你必须使用链接方法直接在 `Hono` 实例上定义路由**。 类型推断依赖于类型通过链接的 `.get()`、`.post()` 等调用流动。如果你在创建 Hono 实例后单独定义路由(如 "Hello World" 示例中显示的常见模式:`const app = new Hono(); app.get(...)`),`testClient` 将没有特定路由的必要类型信息,你将无法获得类型安全的客户端功能。 **示例:** 此示例有效,因为 `.get()` 方法直接链接到 `new Hono()` 调用: ```ts // index.ts const app = new Hono().get('/search', (c) => { const query = c.req.query('q') return c.json({ query: query, results: ['result1', 'result2'] }) }) export default app ``` ```ts // index.test.ts import { Hono } from 'hono' import { testClient } from 'hono/testing' import { describe, it, expect } from 'vitest' // 或你首选的测试运行器 import app from './app' describe('Search Endpoint', () => { // 从应用程序实例创建测试客户端 const client = testClient(app) it('should return search results', async () => { // 使用类型化客户端调用端点 // 注意查询参数的类型安全性(如果在路由中定义) // 以及通过 .$get() 直接访问 const res = await client.search.$get({ query: { q: 'hono' }, }) // 断言 expect(res.status).toBe(200) expect(await res.json()).toEqual({ query: 'hono', results: ['result1', 'result2'], }) }) }) ``` 要在测试中包含 headers,请将它们作为调用中的第二个参数传递。第二个参数还可以将 `init` 属性作为 `RequestInit` 对象,允许你设置 headers、method、body 等。在此处了解有关 `init` 属性的更多信息 [here](/docs/guides/rpc#init-option)。 ```ts // index.test.ts import { Hono } from 'hono' import { testClient } from 'hono/testing' import { describe, it, expect } from 'vitest' // 或你首选的测试运行器 import app from './app' describe('Search Endpoint', () => { // 从应用程序实例创建测试客户端 const client = testClient(app) it('should return search results', async () => { // 在 headers 中包含令牌并设置内容类型 const token = 'this-is-a-very-clean-token' const res = await client.search.$get( { query: { q: 'hono' }, }, { headers: { Authorization: `Bearer ${token}`, 'Content-Type': `application/json`, }, } ) // 断言 expect(res.status).toBe(200) expect(await res.json()).toEqual({ query: 'hono', results: ['result1', 'result2'], }) }) }) ``` # WebSocket 辅助工具 WebSocket 辅助工具是用于 Hono 应用程序中服务器端 WebSocket 的辅助工具。 目前 Cloudflare Workers / Pages、Deno 和 Bun 适配器可用。 ## 导入 ::: code-group ```ts [Cloudflare Workers] import { Hono } from 'hono' import { upgradeWebSocket } from 'hono/cloudflare-workers' ``` ```ts [Deno] import { Hono } from 'hono' import { upgradeWebSocket } from 'hono/deno' ``` ```ts [Bun] import { Hono } from 'hono' import { upgradeWebSocket, websocket } from 'hono/bun' // ... export default { fetch: app.fetch, websocket, } ``` ::: 如果你使用 Node.js,你可以使用 [@hono/node-ws](https://github.com/honojs/middleware/tree/main/packages/node-ws)。 ## `upgradeWebSocket()` `upgradeWebSocket()` 返回用于处理 WebSocket 的 handler。 ```ts const app = new Hono() app.get( '/ws', upgradeWebSocket((c) => { return { onMessage(event, ws) { console.log(`Message from client: ${event.data}`) ws.send('Hello from server!') }, onClose: () => { console.log('Connection closed') }, } }) ) ``` 可用的事件: - `onOpen` - 目前,Cloudflare Workers 不支持。 - `onMessage` - `onClose` - `onError` ::: warning 如果你在具有 WebSocket 辅助工具的路由上使用修改 headers 的中间件(例如,应用 CORS),你可能会遇到错误,说你无法修改不可变的 headers。这是因为 `upgradeWebSocket()` 也在内部更改 headers。 因此,如果你同时使用 WebSocket 辅助工具和中间件,请小心。 ::: ## RPC 模式 使用 WebSocket 辅助工具定义的 handlers 支持 RPC 模式。 ```ts // server.ts const wsApp = app.get( '/ws', upgradeWebSocket((c) => { //... }) ) export type WebSocketApp = typeof wsApp // client.ts const client = hc('http://localhost:8787') const socket = client.ws.$ws() // 客户端的 WebSocket 对象 ``` ## 示例 请参阅使用 WebSocket 辅助工具的示例。 ### 服务器和客户端 ```ts // server.ts import { Hono } from 'hono' import { upgradeWebSocket } from 'hono/cloudflare-workers' const app = new Hono().get( '/ws', upgradeWebSocket(() => { return { onMessage: (event) => { console.log(event.data) }, } }) ) export default app ``` ```ts // client.ts import { hc } from 'hono/client' import type app from './server' const client = hc('http://localhost:8787') const ws = client.ws.$ws(0) ws.addEventListener('open', () => { setInterval(() => { ws.send(new Date().toString()) }, 1000) }) ``` ### Bun with JSX ```tsx import { Hono } from 'hono' import { upgradeWebSocket, websocket } from 'hono/bun' import { html } from 'hono/html' const app = new Hono() app.get('/', (c) => { return c.html(
{html` `} ) }) const ws = app.get( '/ws', upgradeWebSocket((c) => { let intervalId return { onOpen(_event, ws) { intervalId = setInterval(() => { ws.send(new Date().toString()) }, 200) }, onClose() { clearInterval(intervalId) }, } }) ) export default { fetch: app.fetch, websocket, } ``` # Best Practices Hono 非常灵活。你可以随心所欲地编写应用程序。然而,有一些最佳实践最好遵循。 ## Don't make "Controllers" when possible 在可能的情况下,你不应该创建"Ruby on Rails-like Controllers"。 ```ts // 🙁 // 一个 RoR-like Controller const booksList = (c: Context) => { return c.json('list books') } app.get('/books', booksList) ``` 问题与类型有关。例如,路径参数无法在 Controller 中推断,除非编写复杂的泛型。 ```ts // 🙁 // 一个 RoR-like Controller const bookPermalink = (c: Context) => { const id = c.req.param('id') // 无法推断路径参数 return c.json(`get ${id}`) } ``` 因此,你不需要创建 RoR-like controllers,应该在路径定义后直接编写 handlers。 ```ts // 😃 app.get('/books/:id', (c) => { const id = c.req.param('id') // 可以推断路径参数 return c.json(`get ${id}`) }) ``` ## `factory.createHandlers()` in `hono/factory` 如果你仍然想创建 RoR-like Controller,使用 `hono/factory` 中的 `factory.createHandlers()`。如果使用这个,类型推断将正常工作。 ```ts import { createFactory } from 'hono/factory' import { logger } from 'hono/logger' // ... // 😃 const factory = createFactory() const middleware = factory.createMiddleware(async (c, next) => { c.set('foo', 'bar') await next() }) const handlers = factory.createHandlers(logger(), middleware, (c) => { return c.json(c.var.foo) }) app.get('/api', ...handlers) ``` ## Building a larger application 使用 `app.route()` 来构建更大的应用程序,而无需创建"Ruby on Rails-like Controllers"。 如果你的应用程序有 `/authors` 和 `/books` 端点,并且你想从 `index.ts` 分离文件,创建 `authors.ts` 和 `books.ts`。 ```ts // authors.ts import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.json('list authors')) app.post('/', (c) => c.json('create an author', 201)) app.get('/:id', (c) => c.json(`get ${c.req.param('id')}`)) export default app ``` ```ts // books.ts import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.json('list books')) app.post('/', (c) => c.json('create a book', 201)) app.get('/:id', (c) => c.json(`get ${c.req.param('id')}`)) export default app ``` 然后,导入它们并使用 `app.route()` 挂载到路径 `/authors` 和 `/books`。 ```ts // index.ts import { Hono } from 'hono' import authors from './authors' import books from './books' const app = new Hono() // 😃 app.route('/authors', authors) app.route('/books', books) export default app ``` ### If you want to use RPC features 上面的代码在普通用例中效果很好。但是,如果你想使用 `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 export type AppType = typeof app ``` 如果你将 `app` 的类型传递给 `hc`,它将获得正确的类型。 ```ts import type { AppType } from './authors' import { hc } from 'hono/client' // 😃 const client = hc('http://localhost') // 正确推断类型 ``` 更多详细信息,请查看 [RPC page](/docs/guides/rpc#using-rpc-with-larger-applications)。 # Create-hono `create-hono` 支持的命令行选项 - 这是运行 `npm create hono@latest`、`npx create-hono@latest` 或 `pnpm create hono@latest` 时运行的项目初始化工具。 > [!NOTE] > **为什么有这个页面?** 安装/快速入门示例通常显示最小的 `npm create hono@latest my-app` 命令。`create-hono` 支持一些有用的标志,你可以传递这些标志来自动化和自定义项目创建(选择 templates、跳过提示、选择包管理器、使用本地缓存等)。 ## Passing arguments: 当你使用 `npm create`(或 `npx`)时,用于初始化脚本的参数必须放在 `--` 之后。`--` 之后的任何内容都会转发给初始化脚本。 ::: code-group ```sh [npm] # 转发参数到 create-hono(npm 需要 `--`) npm create hono@latest my-app -- --template cloudflare-workers ``` ```sh [yarn] # "--template cloudflare-workers" 选择 Cloudflare Workers template yarn create hono my-app --template cloudflare-workers ``` ```sh [pnpm] # "--template cloudflare-workers" 选择 Cloudflare Workers template pnpm create hono@latest my-app --template cloudflare-workers ``` ```sh [bun] # "--template cloudflare-workers" 选择 Cloudflare Workers template bun create hono@latest my-app --template cloudflare-workers ``` ```sh [deno] # "--template cloudflare-workers" 选择 Cloudflare Workers template deno init --npm hono@latest my-app --template cloudflare-workers ``` ::: ## Commonly used arguments | Argument | Description | Example | | :---------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------ | | `--template