This is the full developer documentation for Hono. # Start of Hono documentation # Hono Hono - _**在日语中意为火焰🔥**_ - 是一个小型、简单且超快的基于 Web 标准构建的 Web 框架。 它适用于任何 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 标准。 - **多运行时** 🌍 - 适用于 Cloudflare Workers、Fastly Compute、Deno、Bun、AWS Lambda 或 Node.js。相同的代码可在所有平台上运行。 - **功能齐全** 🔋 - Hono 拥有内置中间件、自定义中间件、第三方中间件和辅助函数。功能齐全。 - **愉快的开发体验** 😃 - 超级清晰的 API。一流的 TypeScript 支持。现在,我们拥有了“类型”。 ## 用例 Hono 是一个类似于 Express 的简单 Web 应用框架,不含前端。 但它运行在 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) | Local AI Server | 带有记忆的无服务器 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 创建应用程序的演示。 ![一个展示快速迭代创建 hono 应用的 gif。](/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 世界中最快的路由器。它使用在调度前创建的单个大型正则表达式来匹配路由。配合 **SmartRouter**,它支持所有路由模式。 **LinearRouter** 注册路由非常快,因此适合每次初始化应用程序的环境。**PatternRouter** 简单地添加和匹配模式,使其小巧。 参见 [关于路由的更多信息](/docs/concepts/routers)。 ## Web 标准 得益于使用 **Web 标准**,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 标准的更多信息](/docs/concepts/web-standard)。 ## 中间件与辅助函数 **Hono 拥有许多中间件和辅助函数**。这使得“少写代码,多做事情”成为现实。 开箱即用,Hono 提供以下中间件和辅助函数: - [基本认证](/docs/middleware/builtin/basic-auth) - [Bearer 认证](/docs/middleware/builtin/bearer-auth) - [Body 限制](/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) - [美化 JSON](/docs/middleware/builtin/pretty-json) - [安全 Headers](/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` 对象,可以轻松访问 Request/Response。 此外,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 认证](https://github.com/honojs/middleware/tree/main/packages/clerk-auth) - [Cloudflare Access](https://github.com/honojs/middleware/tree/main/packages/cloudflare-access) - [OAuth 提供商](https://github.com/honojs/middleware/tree/main/packages/oauth-providers) - [OIDC 认证](https://github.com/honojs/middleware/tree/main/packages/oidc-auth) - [Firebase 认证](https://github.com/honojs/middleware/tree/main/packages/firebase-auth) - [验证 RSA JWT (JWKS)](https://github.com/wataruoguchi/verify-rsa-jwt-cloudflare-worker) - [Stytch 认证](https://github.com/honojs/middleware/tree/main/packages/stytch-auth) ### 验证器 - [Ajv 验证器](https://github.com/honojs/middleware/tree/main/packages/ajv-validator) - [ArkType 验证器](https://github.com/honojs/middleware/tree/main/packages/arktype-validator) - [Class 验证器](https://github.com/honojs/middleware/tree/main/packages/class-validator) - [Conform 验证器](https://github.com/honojs/middleware/tree/main/packages/conform-validator) - [Effect Schema 验证器](https://github.com/honojs/middleware/tree/main/packages/effect-validator) - [Standard Schema 验证器](https://github.com/honojs/middleware/tree/main/packages/standard-validator) - [TypeBox 验证器](https://github.com/honojs/middleware/tree/main/packages/typebox-validator) - [Typia 验证器](https://github.com/honojs/middleware/tree/main/packages/typia-validator) - [unknownutil 验证器](https://github.com/ryoppippi/hono-unknownutil-validator) - [Valibot 验证器](https://github.com/honojs/middleware/tree/main/packages/valibot-validator) - [Zod 验证器](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 配置](https://github.com/honojs/middleware/tree/main/packages/eslint-config) - [SSG 插件必备](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 指标](https://github.com/honojs/middleware/tree/main/packages/prometheus) - [Sentry](https://github.com/honojs/middleware/tree/main/packages/sentry) - [Pino 日志记录器](https://github.com/maou-shonen/hono-pino) ### 服务器 / 适配器 - [GraphQL 服务器](https://github.com/honojs/middleware/tree/main/packages/graphql-server) - [Node WebSocket 助手](https://github.com/honojs/middleware/tree/main/packages/node-ws) - [tRPC 服务器](https://github.com/honojs/middleware/tree/main/packages/trpc-server) ### 转译器 - [Bun 转译器](https://github.com/honojs/middleware/tree/main/packages/bun-transpiler) - [esbuild 转译器](https://github.com/honojs/middleware/tree/main/packages/esbuild-transpiler) ### UI / 渲染器 - [Qwik City](https://github.com/honojs/middleware/tree/main/packages/qwik-city) - [React 兼容性](https://github.com/honojs/middleware/tree/main/packages/react-compat) - [React 渲染器](https://github.com/honojs/middleware/tree/main/packages/react-renderer) ### 队列 / 任务处理 - [GlideMQ (消息队列 REST API + SSE)](https://github.com/avifenesh/glidemq-hono) ### 国际化 - [Intlayer i18n](https://intlayer.org/doc/environment/hono) ### 工具 - [Bun 压缩](https://github.com/honojs/middleware/tree/main/packages/bun-compress) - [Cap Checkpoint](https://capjs.js.org/guide/middleware/hono.html) - [事件发射器](https://github.com/honojs/middleware/tree/main/packages/event-emitter) - [地理位置](https://github.com/ktkongtong/hono-geo-middleware/tree/main/packages/middleware) - [Hono 速率限制器](https://github.com/rhinobase/hono-rate-limiter) - [Hono 问题详情 (RFC 9457)](https://github.com/paveg/hono-problem-details) - [Hono 简单 DI](https://github.com/maou-shonen/hono-simple-DI) - [幂等性 (Stripe 风格幂等键)](https://github.com/paveg/hono-idempotency) - [jsonv-ts (验证器,OpenAPI, MCP)](https://github.com/dswbx/jsonv-ts) - [MCP](https://github.com/honojs/middleware/tree/main/packages/mcp) - [RONIN (数据库)](https://github.com/ronin-co/hono-client) - [会话](https://github.com/honojs/middleware/tree/main/packages/session) - [tsyringe](https://github.com/honojs/middleware/tree/main/packages/tsyringe) - [基于 User Agent 的拦截器](https://github.com/honojs/middleware/tree/main/packages/ua-blocker) # Basic Auth 中间件 此中间件可以将基本认证应用于指定路径。 使用 Cloudflare Workers 或其他平台实现基本认证比看起来更复杂,但有了这个中间件,这就轻而易举了。 有关基本认证方案如何在底层工作的更多信息,请参阅 [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 挑战头的一部分。默认值为 `"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 头。 ```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 认证中间件 Bearer 认证中间件通过验证请求头中的 API 令牌来提供身份认证。 访问端点的 HTTP 客户端将添加 `Authorization` 头,并将 `Bearer {token}` 作为头值。 在终端中使用 `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 安全的 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 等处理器 ``` 如果您想自行验证令牌的值,请指定 `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 挑战头的一部分。默认值为 `""`。 查看更多:https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate#directives ### prefix: `string` Authorization 头值的前缀(或称为 `schema`)。默认值为 `"Bearer"`。 ### headerName: `string` 头名称。默认值为 `Authorization`。 ### hashFunction: `Function` 用于处理哈希的函数,以便安全地比较身份验证令牌。 ### verifyToken: `(token: string, c: Context) => boolean | Promise` 用于验证令牌的函数。 ### noAuthenticationHeader: `object` 自定义请求没有身份验证头时的错误响应。 - `wwwAuthenticateHeader`: `string | object | MessageFunction` - 自定义 WWW-Authenticate 头值。 - `message`: `string | object | MessageFunction` - 响应体的自定义消息。 `MessageFunction` 是 `(c: Context) => string | object | Promise`。 ### invalidAuthenticationHeader: `object` 自定义身份验证头格式无效时的错误响应。 - `wwwAuthenticateHeader`: `string | object | MessageFunction` - 自定义 WWW-Authenticate 头值。 - `message`: `string | object | MessageFunction` - 响应体的自定义消息。 ### invalidToken: `object` 自定义令牌无效时的错误响应。 - `wwwAuthenticateHeader`: `string | object | MessageFunction` - 自定义 WWW-Authenticate 头值。 - `message`: `string | object | MessageFunction` - 响应体的自定义消息。 # Body Limit 中间件 Body Limit 中间件可以限制请求体的文件大小。 如果存在,此中间件首先使用请求中 `Content-Length` 头部的值。 如果未设置,它会在流中读取主体,如果大于指定的文件大小,则执行错误处理程序。 ## 导入 ```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, // 此处填写你的值 }) ``` # 缓存中间件 缓存中间件使用 Web 标准的 [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache)。 缓存中间件目前支持使用自定义域名的 Cloudflare Workers 项目和使用 [Deno 1.26+](https://github.com/denoland/deno/releases/tag/v1.26.0) 的 Deno 项目。也可用于 Deno Deploy。 Cloudflare Workers 尊重 `Cache-Control` 头并返回缓存的响应。详细信息请参阅 [Cloudflare 文档上的缓存](https://developers.cloudflare.com/workers/runtime-apis/cache/)。Deno 不尊重头信息,因此如果您需要更新缓存,则需要实现自己的机制。 有关每个平台的说明,请参阅下面的 [用法](#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` 头的指令字符串。有关更多信息,请参阅 [MDN 文档](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)。当未提供此选项时,不会向请求添加 `Cache-Control` 头。 ### vary: `string` | `string[]` 设置响应中的 `Vary` 头。如果原始响应头已包含 `Vary` 头,则合并值,移除任何重复项。将其设置为 `*` 将导致错误。有关 Vary 头及其对缓存策略影响的更多详细信息,请参阅 [MDN 文档](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary)。 ### keyGenerator: `(c: Context) => string | Promise` 为 `cacheName` 存储中的每个请求生成键。这可用于基于请求参数或上下文参数缓存数据。默认为 `c.req.url`。 ### cacheableStatusCodes: `number[]` 应缓存的状态码数组。默认为 `[200]`。使用此选项缓存具有特定状态码的响应。 ```ts app.get( '*', cache({ cacheName: 'my-app', cacheControl: 'max-age=3600', cacheableStatusCodes: [200, 404, 412], }) ) ``` # 组合中间件 组合中间件将多个中间件函数组合成一个中间件。它提供三个函数: - `some` - 仅运行给定中间件中的一个。 - `every` - 运行所有给定的中间件。 - `except` - 仅当条件不满足时运行所有给定的中间件。 ## 导入 ```ts import { Hono } from 'hono' import { some, every, except } from 'hono/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 }))) ``` # 压缩中间件 此中间件根据 `Accept-Encoding` 请求头压缩响应体。 ::: 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` 头使用。如果未提供此选项且客户端在 `Accept-Encoding` 头中同时提供了两者,则优先使用 `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() }) // 您可以在处理程序外部访问变量。 const getMessage = () => { return getContext().var.message } app.get('/', (c) => { return c.text(getMessage()) }) ``` 在 Cloudflare Workers 上,您可以在处理程序外部访问绑定。 ```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'], }) ) // 或者你可以使用“函数” 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 头部的值。你也可以传递回调函数,例如 `origin: (origin) => (origin.endsWith('.example.com') ? origin : 'http://example.com')`。默认值是 `*`。 ### allowMethods: `string[]` | `(origin:string, c:Context) => string[]` "_Access-Control-Allow-Methods_" CORS 头部的值。你也可以传递回调函数来根据源动态确定允许的方法。默认值是 `['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH']`。 ### allowHeaders: `string[]` "_Access-Control-Allow-Headers_" CORS 头部的值。默认值是 `[]`。 ### maxAge: `number` "_Access-Control-Max-Age_" CORS 头部的值。 ### credentials: `boolean` "_Access-Control-Allow-Credentials_" CORS 头部的值。 ### exposeHeaders: `string[]` "_Access-Control-Expose-Headers_" CORS 头部的值。默认值是 `[]`。 ## 依赖环境的 CORS 配置 如果你想根据执行环境(如开发或生产)调整 CORS 配置,从环境变量注入值很方便,因为它消除了应用程序感知自身执行环境的需要。请参阅下面的示例以作说明。 ```ts app.use('*', async (c, next) => { const corsMiddlewareHandler = cors({ origin: c.env.CORS_ORIGIN, }) return corsMiddlewareHandler(c, next) }) ``` ## 与 Vite 一起使用 当与 Vite 一起使用 Hono 时,你应该在 `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` 标头和 `Sec-Fetch-Site` 标头来防止 CSRF 攻击。如果任一验证通过,则允许请求。 该中间件仅验证以下请求: - 使用不安全的 HTTP 方法(非 GET、HEAD 或 OPTIONS) - 具有 HTML 表单可以发送的内容类型(`application/x-www-form-urlencoded`、`multipart/form-data` 或 `text/plain`) 不发送 `Origin` 标头的旧浏览器,或使用反向代理移除这些标头的环境,可能无法正常工作。在此类环境中,请使用其他 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) => { // 始终允许同源 if (secFetchSite === 'same-origin') return true // 允许 webhook 端点的跨站请求 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` 标头值和请求上下文,允许基于请求属性(如路径、标头或其他上下文数据)进行动态验证。 ### secFetchSite: `string` | `string[]` | `Function` 指定 CSRF 保护允许的 Sec-Fetch-Site 标头值,使用 [Fetch Metadata](https://web.dev/articles/fetch-metadata)。 - **`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` 标头值和请求上下文,支持基于请求属性进行动态验证。 # ETag 中间件 使用此中间件,您可以轻松添加 ETag 头。 ## 导入 ```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') }) ``` ## 保留的响应头 304 响应必须包含等同于 200 OK 响应中应发送的响应头。默认的响应头包括 Cache-Control、Content-Location、Date、ETag、Expires 和 Vary。 如果您想添加发送的响应头,可以使用 `retainedHeaders` 选项和包含默认响应头的 `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 响应中保留的响应头。 ### generateDigest: `(body: Uint8Array) => ArrayBuffer | Promise` 一个自定义的摘要生成函数。默认情况下,它使用 `SHA-1`。此函数以 `Uint8Array` 形式的响应体作为调用参数,并应返回一个 `ArrayBuffer` 形式的哈希或其 Promise。 # IP 限制中间件 IP 限制中间件是一种根据用户的 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 渲染器中间件 JSX 渲染器中间件允许你在使用 `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`,将添加以下头信息: ```ts { 'Transfer-Encoding': 'chunked', 'Content-Type': 'text/html; charset=UTF-8', 'Content-Encoding': 'Identity' } ``` 你可以通过指定 Record 值来自定义头信息的值。 ### 基于函数的选项 你可以传递一个接收 `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 认证中间件 JWK 认证中间件通过使用 JWK (JSON Web Key) 验证令牌来认证请求。它检查 `Authorization` 头部以及其他配置的来源(如指定时的 cookies)。它使用提供的 `keys` 验证令牌,如果指定了 `jwks_uri` 则从中检索密钥,并且如果设置了 `cookie` 选项,则支持从 cookies 中提取令牌。 ## 此中间件验证的内容 对于每个令牌,`jwk()`: - 解析并验证 JWT 头部格式。 - 需要 `kid` 头部,并通过 `kid` 查找匹配密钥。 - 拒绝对称算法(`HS256`、`HS384`、`HS512`)。 - 要求头部 `alg` 包含在配置的 `alg` 允许列表中。 - 如果匹配的 JWK 具有 `alg` 字段,则要求它与 JWT 头部 `alg` 匹配。 - 使用匹配的密钥验证令牌签名。 - 默认情况下,验证基于时间的声明:`nbf`、`exp` 和 `iat`。 可选的声明验证可以通过 `verification` 选项配置: - `iss`:提供时验证颁发者。 - `aud`:提供时验证受众。 如果您需要上述之外的额外令牌检查(例如,自定义应用程序级授权规则),请在 `jwk()` 之后的自有中间件中添加它们。 :::info 客户端发送的 Authorization 头部必须具有指定的方案。 示例:`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 获取请求选项 要配置如何从 `jwks_uri` 检索 JWKS,请将 fetch 请求选项作为 `jwk()` 的第二个参数传递。 此参数为 `RequestInit`,仅用于 JWKS 获取请求。 ```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 获取 JWK,期望响应为包含 `keys` 的 JSON,这些 keys 将添加到提供的 `keys` 选项中。您也可以传递回调函数以使用 Context 动态确定 JWKS URI。 ### allow_anon: `boolean` 如果将此值设置为 `true`,则没有有效令牌的请求将被允许通过中间件。使用 `c.get('jwtPayload')` 检查请求是否已认证。默认为 `false`。 ### cookie: `string` 如果设置了此值,则使用该值作为键从 cookie 头部检索值,然后将其作为令牌进行验证。 ### headerName: `string` 用于查找 JWT 令牌的头部名称。默认为 `Authorization`。 ### verification: `VerifyOptions` 配置除签名验证之外的声明验证行为: - `iss`:预期的颁发者。 - `aud`:预期的受众。 - `exp`, `nbf`, `iat`:默认启用,如有需要可禁用。 # JWT 认证中间件 JWT 认证中间件通过验证 JWT 令牌提供身份认证。 如果未设置 `cookie` 选项,中间件将检查 `Authorization` 头。你可以使用 `headerName` 选项自定义头名称。 :::info 客户端发送的 Authorization 头必须具有指定的方案。 例如:`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') }) ``` 获取负载: ```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 头中检索值,然后将其作为令牌进行验证。 ### headerName: `string` 查找 JWT 令牌的头名称。默认为 `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` 声明。 #### verifyOptions.nbf: `boolean` 如果存在 `nbf`(not before)声明且此项设置为 `true`,则将对其进行验证。默认为 `true`。 #### verifyOptions.iat: `boolean` 如果存在 `iat`(issued at)声明且此项设置为 `true`,则将对其进行验证。默认为 `true`。 #### verifyOptions.exp: `boolean` 如果存在 `exp`(expiration time)声明且此项设置为 `true`,则将对其进行验证。默认为 `true`。 # 语言中间件 Language Detector 中间件自动从各种来源确定用户的首选语言(区域设置),并通过 `c.get('language')` 提供。检测策略包括查询参数、Cookie、Header 和 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 请求头 2. **缓存**:将检测到的语言存储在 Cookie 中(默认 1 年) 3. **回退**:如果没有有效的检测结果,则使用 `fallbackLanguage`(必须在 `supportedLanguages` 中) ## 高级配置 ### 自定义检测顺序 优先检测 URL 路径(例如 /en/about): ```ts app.use( languageDetector({ order: ['path', 'cookie', 'querystring', 'header'], lookupFromPathIndex: 0, // /en/profile → 索引 0 = 'en' supportedLanguages: ['en', 'ar'], fallbackLanguage: 'en', }) ) ``` ### 渐进式区域设置匹配 当检测到的区域设置代码(如 `ja-JP`)不在 `supportedLanguages` 中时,中间件会 progressively 截断子标签以找到匹配项。例如,`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 同站策略 secure: true, // 仅通过 HTTPS 发送 maxAge: 86400 * 365, // 1 年过期 httpOnly: true, // 无法通过 JavaScript 访问 domain: '.example.com', // 可选:特定域名 }, }) ) ``` 要禁用 Cookie 缓存: ```ts languageDetector({ caches: false, }) ``` ### 调试 记录检测步骤: ```ts languageDetector({ debug: true, // 显示:"从查询字符串检测到: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'` | 同站策略 | | `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('_', '-'), // 标准化 }) ``` # 日志中间件 它是一个简单的日志记录器。 ## 导入 ```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!')) ``` ## 日志详情 日志中间件会记录每个请求的以下详情: - **传入请求**:记录 HTTP 方法、请求路径和传入请求。 - **传出响应**:记录 HTTP 方法、请求路径、响应状态码和请求/响应时间。 - **状态码着色**:响应状态码采用颜色编码,以提高可见性并快速识别状态类别。不同的状态码类别由不同的颜色表示。 - **耗时**:请求/响应周期所花费的时间以人类可读的格式记录,单位为毫秒 (ms) 或秒 (s)。 通过使用日志中间件,您可以轻松监控 Hono 应用程序中的请求和响应流,并快速识别任何问题或性能瓶颈。 您还可以通过提供自己的 `PrintFunc` 函数来进一步扩展中间件,以实现定制的日志行为。 ::: tip 要禁用 _状态码着色_,您可以设置 `NO_COLOR` 环境变量。这是在日志库中禁用 ANSI 颜色转义码的常用方法,详见 。请注意,Cloudflare Workers 没有 `process.env` 对象,因此将默认为纯文本日志输出。 ::: ## PrintFunc 日志中间件接受一个可选的 `PrintFunc` 函数作为参数。此函数允许您自定义日志记录器并添加额外的日志。 ## 选项 ### fn: `PrintFunc(str: string, ...rest: string[])` - `str`:由日志记录器传递。 - `...rest`:要打印到控制台的其他字符串属性。 ### 示例 为日志中间件设置自定义 `PrintFunc` 函数: ```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 // 博客已保存:Path: /blog/example, ID: 1 // --> POST /blog 201 93ms // 返回 Context }) ``` # 方法覆盖中间件 此中间件根据表单、请求头或查询参数的值,执行指定方法(不同于请求的实际方法)的处理程序,并返回其响应。 ## 导入 ```ts import { Hono } from 'hono' import { methodOverride } from 'hono/method-override' ``` ## 用法 ```ts const app = new Hono() // 如果未指定选项,则使用表单中 `_method` 的值, // 例如 DELETE,作为方法。 app.use('/posts', methodOverride({ app })) app.delete('/posts', (c) => { // .... }) ``` ## 示例 由于 HTML 表单无法发送 DELETE 方法,您可以在名为 `_method` 的属性中放入值 `DELETE` 并发送。然后 `app.delete()` 的处理程序将被执行。 HTML 表单: ```html
``` 应用程序: ```ts import { methodOverride } from 'hono/method-override' const app = new Hono() app.use('/posts', methodOverride({ app })) app.delete('/posts', () => { // ... }) ``` 您可以更改默认值或使用请求头值和查询参数值: ```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` 包含方法名的值的表单键。 默认值为 `_method`。 ### header: `boolean` 包含方法名的值的请求头名称。 ### query: `boolean` 包含方法名的值的查询参数键。 # Pretty JSON 中间件 Pretty JSON 中间件为 JSON 响应体启用"_JSON 美化打印_"。 在 URL 查询参数中添加 `?pretty`,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,你可以在处理程序中使用它。 ::: 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 中间件的处理程序和中间件中通过 `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 如果你在请求头中设置了一个自定义的 request ID(默认:`X-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()) // 你的自定义 id ``` 如果你想禁用此功能,将 [`headerName` 选项](#headername-string) 设置为空字符串。 ## 选项 ### limitLength: `number` Request ID 的最大长度。默认值是 `255`。 ### headerName: `string` 用于 Request ID 的请求头名称。默认值是 `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 中间件简化了安全头部的设置。部分灵感来源于 Helmet 的功能,它允许您控制特定安全头部的激活和停用。 ## 导入 ```ts import { Hono } from 'hono' import { secureHeaders } from 'hono/secure-headers' ``` ## 用法 默认情况下,您可以使用最佳设置。 ```ts const app = new Hono() app.use(secureHeaders()) ``` 您可以通过将它们设置为 false 来抑制不必要的头部。 ```ts const app = new Hono() app.use( '*', secureHeaders({ xFrameOptions: false, xXssProtection: false, }) ) ``` 您可以使用字符串覆盖默认的头部值。 ```ts const app = new Hono() app.use( '*', secureHeaders({ strictTransportSecurity: 'max-age=63072000; includeSubDomains; preload', xFrameOptions: 'DENY', xXssProtection: '1', }) ) ``` ## 支持的选项 每个选项对应以下头部键值对。 | 选项 | 头部 | 值 | 默认 | | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------- | | - | X-Powered-By | (删除头部) | 启用 | | contentSecurityPolicy | [内容安全策略](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) | 用法:[设置内容安全策略](#设置内容安全策略) | 未设置 | | contentSecurityPolicyReportOnly | [内容安全策略仅报告](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only) | 用法:[设置内容安全策略](#设置内容安全策略) | 未设置 | | trustedTypes | [可信类型](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types) | 用法:[设置内容安全策略](#设置内容安全策略) | 未设置 | | requireTrustedTypesFor | [要求可信类型用于](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/require-trusted-types-for) | 用法:[设置内容安全策略](#设置内容安全策略) | 未设置 | | crossOriginEmbedderPolicy | [跨源嵌入者策略](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy) | require-corp | **禁用** | | crossOriginResourcePolicy | [跨源资源策略](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy) | same-origin | 启用 | | crossOriginOpenerPolicy | [跨源打开者策略](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy) | same-origin | 启用 | | originAgentCluster | [源代理集群](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin-Agent-Cluster) | ?1 | 启用 | | referrerPolicy | [引荐来源策略](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy) | no-referrer | 启用 | | reportingEndpoints | [报告端点](https://www.w3.org/TR/reporting-1/#header) | 用法:[设置内容安全策略](#设置内容安全策略) | 未设置 | | reportTo | [报告至](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-to) | 用法:[设置内容安全策略](#设置内容安全策略) | 未设置 | | strictTransportSecurity | [严格传输安全](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security) | max-age=15552000; includeSubDomains | 启用 | | xContentTypeOptions | [X-Content-Type-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options) | nosniff | 启用 | | xDnsPrefetchControl | [X-DNS-Prefetch-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control) | off | 启用 | | xDownloadOptions | [X-Download-Options](https://learn.microsoft.com/en-us/archive/blogs/ie/ie8-security-part-v-comprehensive-protection#mime-handling-force-save) | noopen | 启用 | | xFrameOptions | [X-Frame-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) | SAMEORIGIN | 启用 | | xPermittedCrossDomainPolicies | [X-Permitted-Cross-Domain-Policies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Permitted-Cross-Domain-Policies) | none | 启用 | | xXssProtection | [X-XSS-Protection](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection) | 0 | 启用 | | permissionPolicy | [权限策略](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy) | 用法:[设置权限策略](#设置权限策略) | 未设置 | ## 中间件冲突 在处理操作相同头部的中间件时,请注意指定顺序。 在这种情况下,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()) ``` ## 设置内容安全策略 ```ts const app = new Hono() app.use( '/test', secureHeaders({ reportingEndpoints: [ { name: 'endpoint-1', url: 'https://example.com/reports', }, ], // -- 或者另一种方式 // 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`,从而为 `script` 或 `style` 元素添加 [`nonce` 属性](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce): ```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( {/** 内容 */} `} Hello! ) }) ``` ### 作为函数组件使用 由于 `html` 返回一个 HtmlEscapedString,它可以作为功能齐全的组件使用,而无需使用 JSX。 #### 使用 `html` 而不是 `memo` 来加速过程 ```typescript const Footer = () => html`
My Address...
` ``` ### 接收属性并嵌入值 ```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 (JWT) 的函数。JWT 通常用于 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 验证](#payload-validation) 中所示。 #### secret: `string` 用于 JWT 验证或签名的密钥。 #### alg: [AlgorithmTypes](#supported-algorithmtypes) 用于 JWT 签名或验证的算法。默认是 HS256。 ## `verify()` 此函数检查 JWT 令牌是否真实且仍然有效。它确保令牌未被篡改,并且仅当你添加了 [Payload 验证](#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`: 表示令牌中的签名不匹配。 ## 支持的算法类型 该模块支持以下 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 以及带 SHA-256 的 MGF1 的 RSASSA-PSS - `PS384`: 使用 SHA-386 以及带 SHA-386 的 MGF1 的 RSASSA-PSS - `PS512`: 使用 SHA-512 以及带 SHA-512 的 MGF1 的 RSASSA-PSS - `ES256`: 使用 P-256 和 SHA-256 的 ECDSA - `ES384`: 使用 P-384 和 SHA-384 的 ECDSA - `ES512`: 使用 P-521 和 SHA-512 的 ECDSA - `EdDSA`: 使用 Ed25519 的 EdDSA # 代理助手 当将 Hono 应用程序用作(反向)代理时,代理助手提供有用的函数。 ## 导入 ```ts import { Hono } from 'hono' import { proxy } from 'hono/proxy' ``` ## `proxy()` `proxy()` 是用于代理的 `fetch()` API 包装器。参数和返回值与 `fetch()` 相同(代理特定选项除外)。 `Accept-Encoding` 标头被替换为当前运行时可以处理的编码。不必要的响应标头被移除,并返回一个可以从处理程序发送的 `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') 中包含的请求标头 }, } ) 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') 中包含的请求标头 }, }) }) ``` 你可以使用 `customFetch` 选项覆盖默认的全局 `fetch` 函数: ```ts app.get('/proxy', (c) => { return proxy('https://example.com/', { customFetch, }) }) ``` ### Connection 标头处理 默认情况下,`proxy()` 会忽略 `Connection` 标头以防止 Hop-by-Hop 标头注入攻击。你可以使用 `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 Helper 为调试和中间件开发提供了增强的路由信息。它允许你访问关于匹配路由和当前正在处理的路由的详细信息。 ## 导入 ```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()` 返回为当前处理程序注册的路由路径模式。 ```ts app.get('/posts/:id', (c) => { console.log(routePath(c)) // '/posts/:id' return c.text('Post details') }) ``` ### 与索引参数一起使用 你可以选择传递一个索引参数来获取特定位置的路由路径,类似于 `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) ``` ### 与索引参数一起使用 你可以选择传递一个索引参数来获取特定位置的基础路由路径,类似于 `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 刷新重定向到 `/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'), ], }) ``` # 流式助手 流式助手提供了用于流式响应的方法。 ## 导入 ```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` 头部的流式响应。 ```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` 头部添加 `Identity`。 ```ts app.get('/streamText', (c) => { c.header('Content-Encoding', 'Identity') return streamText(c, async (stream) => { // ... }) }) ``` ::: ## `streamSSE()` 它允许您无缝流式传输服务器发送事件 (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` 是一个在响应发送之前处理错误并覆盖响应的钩子。但是,当回调函数执行时,流已经启动,因此无法被覆盖。 ::: # 测试助手 测试助手提供了函数,使测试 Hono 应用程序变得更加容易。 ## 导入 ```ts import { Hono } from 'hono' import { testClient } from 'hono/testing' ``` ## `testClient()` `testClient()` 函数将 Hono 实例作为其第一个参数,并返回一个根据 Hono 应用程序路由类型化的对象,类似于 [Hono 客户端](/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', () => { // 从 app 实例创建测试客户端 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'], }) }) }) ``` 要在测试中包含头信息,请将它们作为调用中的第二个参数传递。第二个参数还可以接受一个 `init` 属性作为 `RequestInit` 对象,允许你设置头信息、方法、请求体等。有关 `init` 属性的更多信息,请参见 [此处](/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', () => { // 从 app 实例创建测试客户端 const client = testClient(app) it('should return search results', async () => { // 在头信息中包含 token 并设置内容类型 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 Helper WebSocket Helper 是 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 的处理程序。 ```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 Helper 的路由上使用修改请求头(例如应用 CORS)的中间件,您可能会遇到无法修改不可变请求头的错误。这是因为 `upgradeWebSocket()` 内部也会更改请求头。 因此,如果您同时使用 WebSocket Helper 和中间件,请小心。 ::: ## RPC 模式 使用 WebSocket Helper 定义的处理程序支持 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 Helper 的示例。 ### 服务器端和客户端 ```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) }) ``` ### 使用 JSX 的 Bun ```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, } ``` # 最佳实践 Hono 非常灵活。你可以按照自己喜欢的方式编写应用。 然而,有一些最佳实践最好遵循。 ## 尽可能不要创建“控制器” 如果可能,你不应该创建“类似 Ruby on Rails 的控制器”。 ```ts // 🙁 // 一个类似 RoR 的控制器 const booksList = (c: Context) => { return c.json('list books') } app.get('/books', booksList) ``` 问题与类型有关。例如,如果不编写复杂的泛型,无法在控制器中推断路径参数。 ```ts // 🙁 // 一个类似 RoR 的控制器 const bookPermalink = (c: Context) => { const id = c.req.param('id') // 无法推断路径参数 return c.json(`get ${id}`) } ``` 因此,你不需要创建类似 RoR 的控制器,应该直接在路径定义后编写处理程序。 ```ts // 😃 app.get('/books/:id', (c) => { const id = c.req.param('id') // 可以推断路径参数 return c.json(`get ${id}`) }) ``` ## `hono/factory` 中的 `factory.createHandlers()` 如果你仍然想创建类似 RoR 的控制器,请使用 [`hono/factory`](/docs/helpers/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) ``` ## 构建大型应用 使用 `app.route()` 来构建大型应用,而无需创建“类似 Ruby on Rails 的控制器”。 如果你的应用有 `/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 ``` ### 如果你想使用 RPC 功能 上面的代码适用于正常用例。 但是,如果你想使用 `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 页面](/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` 支持几个有用的标志,你可以传递这些标志来自动化和自定义项目创建(选择模板、跳过提示、选择包管理器、使用本地缓存等)。 ## 传递参数: 当你使用 `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 模板 yarn create hono my-app --template cloudflare-workers ``` ```sh [pnpm] # "--template cloudflare-workers" 选择 Cloudflare Workers 模板 pnpm create hono@latest my-app --template cloudflare-workers ``` ```sh [bun] # "--template cloudflare-workers" 选择 Cloudflare Workers 模板 bun create hono@latest my-app --template cloudflare-workers ``` ```sh [deno] # "--template cloudflare-workers" 选择 Cloudflare Workers 模板 deno init --npm hono@latest my-app --template cloudflare-workers ``` ::: ## 常用参数 | 参数 | 描述 | 示例 | | :---------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------ | | `--template