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,同一份代码跑遍所有平台。 - **电池全配** 🔋 —— 内置中间件、可自定义中间件、第三方中间件与助手函数,开箱即用。 - **愉悦的开发体验** 😃 —— 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) | 本地 AI 服务器 | 带有记忆功能的无服务器 AI Agent 流水线开源框架,_使用 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 服务器_。 | 还有以下团队也在生产环境中使用 Hono: - [Drivly](https://driv.ly/) - Cloudflare Workers - [repeat.dev](https://repeat.dev/) - Cloudflare Workers 想了解更多?请访问 [Who is using Hono in production?](https://github.com/orgs/honojs/discussions/1510)。 ## 一分钟体验 Hono 以下演示展示了如何使用 Hono 在 Cloudflare Workers 上创建应用。 ![演示动图:快速创建并迭代一个 Hono 应用](/images/sc.gif) ## 极速表现 **在 Cloudflare Workers 的各类路由器中,Hono 是最快的。** ``` 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 拥有大量中间件与助手函数**,真正实现“写得更少,做得更多”。 开箱即用的中间件与助手包括: - [Basic Authentication](/docs/middleware/builtin/basic-auth) - [Bearer Authentication](/docs/middleware/builtin/bearer-auth) - [Body Limit](/docs/middleware/builtin/body-limit) - [Cache](/docs/middleware/builtin/cache) - [Compress](/docs/middleware/builtin/compress) - [Context Storage](/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 Authentication](/docs/middleware/builtin/jwt) - [Logger](/docs/middleware/builtin/logger) - [Language](/docs/middleware/builtin/language) - [Pretty JSON](/docs/middleware/builtin/pretty-json) - [Secure Headers](/docs/middleware/builtin/secure-headers) - [SSG](/docs/helpers/ssg) - [Streaming](/docs/helpers/streaming) - [GraphQL Server](https://github.com/honojs/middleware/tree/main/packages/graphql-server) - [Firebase Authentication](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 编写,自带“**类型**”。 例如,路径参数会被推断为字面量类型。 ![屏幕截图展示了当 URL 带有参数时,Hono 能正确推断字面量类型。路径 "/entry/:date/:id" 会让请求参数变成 "date" 或 "id"](/images/ss.png) 借助 Validator 与 Hono Client `hc`,可以启用 RPC 模式。 在该模式下,你可以继续使用自己喜爱的校验器(如 Zod),轻松在服务端与客户端之间共享 API 规范,从而构建类型安全的应用。 查看 [Hono Stacks](/docs/concepts/stacks)。 # 第三方中间件 “第三方中间件”指的是那些未随 Hono 主包一同发布的中间件。 这些中间件大多基于外部库实现。 ### 认证 - [Auth.js(Next Auth)](https://github.com/honojs/middleware/tree/main/packages/auth-js) - [Clerk Auth](https://github.com/honojs/middleware/tree/main/packages/clerk-auth) - [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) ### 校验器 - [ArkType 校验器](https://github.com/honojs/middleware/tree/main/packages/arktype-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) - [Hono OpenAPI](https://github.com/rhinobase/hono-openapi) ### 其他 - [Bun Transpiler](https://github.com/honojs/middleware/tree/main/packages/bun-transpiler) - [esbuild Transpiler](https://github.com/honojs/middleware/tree/main/packages/esbuild-transpiler) - [Event Emitter](https://github.com/honojs/middleware/tree/main/packages/event-emitter) - [GraphQL Server](https://github.com/honojs/middleware/tree/main/packages/graphql-server) - [Hono Rate Limiter](https://github.com/rhinobase/hono-rate-limiter) - [Node WebSocket Helper](https://github.com/honojs/middleware/tree/main/packages/node-ws) - [Prometheus Metrics](https://github.com/honojs/middleware/tree/main/packages/prometheus) - [OpenTelemetry](https://github.com/honojs/middleware/tree/main/packages/otel) - [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) - [RONIN(数据库)](https://github.com/ronin-co/hono-client) - [Sentry](https://github.com/honojs/middleware/tree/main/packages/sentry) - [tRPC Server](https://github.com/honojs/middleware/tree/main/packages/trpc-server) - [Geo](https://github.com/ktkongtong/hono-geo-middleware/tree/main/packages/middleware) - [Hono Simple DI](https://github.com/maou-shonen/hono-simple-DI) - [Highlight.io](https://www.highlight.io/docs/getting-started/backend-sdk/js/hono) - [Apitally(API 监控与分析)](https://docs.apitally.io/frameworks/hono) - [Cap Checkpoint](https://capjs.js.org/guide/middleware/hono.html) # Basic Auth 中间件 这个中间件可以为指定路径启用 Basic 认证。 在 Cloudflare Workers 或其他平台上自己实现 Basic 认证往往比想象中复杂,但借助该中间件就能轻松搞定。 想了解 Basic 认证方案在幕后是如何运作的,请查阅 [MDN 文档](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#basic_authentication_scheme)。 ## Import ```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 挑战头一部分返回的领域(Realm)名称,默认值为 `"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`,用于在用户无效时返回自定义消息。 ## 更多选项 ### ...users:`{ username: string, password: string }[]` ## 使用示例 ### 定义多个用户 该中间件也允许传入额外参数,以对象形式定义更多 `username` 与 `password` 组合。 ```ts app.use( '/auth/*', basicAuth( { username: 'hono', password: 'acoolproject', // Define other params in the first object realm: 'www.example.com', }, { username: 'hono-admin', password: 'super-secure', // Cannot redefine other params here }, { username: 'hono-user-1', password: 'a-secret', // Or here } ) ) ``` 或者减少硬编码: ```ts import { users } from '../config/users' app.use( '/auth/*', basicAuth( { realm: 'www.example.com', ...users[0], }, ...users.slice(1) ) ) ``` # Bearer Auth 中间件 Bearer Auth 中间件会通过校验请求头中的 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] > 令牌必须匹配正则 `/[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` 自定义的令牌验证函数。 ### noAuthenticationHeaderMessage:`string | object | MessageFunction` `MessageFunction` 的签名为 `(c: Context) => string | object | Promise`,在请求未携带认证头时返回自定义消息。 ### invalidAuthenticationHeaderMessage:`string | object | MessageFunction` 当认证头无效时返回的自定义消息。 ### invalidTokenMessage:`string | object | MessageFunction` 当令牌无效时返回的自定义消息。 # Body Limit 中间件 Body Limit 中间件可以限制请求正文(Body)的大小。 该中间件会优先读取请求中的 `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, // 在此填入你的值 }) ``` # Cache 中间件 Cache 中间件使用 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], }) ) ``` # 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 执行第一个返回成功的中间件。中间件按顺序应用,只要某个中间件成功结束,后续中间件就不会再执行。 ```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` 压缩响应正文。 ::: info **注意**:在 Cloudflare Workers 与 Deno Deploy 上,响应正文会自动压缩,因此无需使用此中间件。 **Bun**:该中间件依赖 `CompressionStream`,而 bun 目前尚未支持。 ::: ## 导入 ```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`,请在 `wrangler.toml` 中添加 [`nodejs_compat` 或 `nodejs_als` 标志](https://developers.cloudflare.com/workers/configuration/compatibility-dates/#nodejs-compatibility-flag)。 ::: ## 导入 ```ts import { Hono } from 'hono' import { contextStorage, getContext } 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 中,还可以在处理器外部访问绑定(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) } ``` # 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` 对应 CORS 头 `_Access-Control-Allow-Origin_` 的值。也可以传入回调函数,例如 `origin: (origin) => (origin.endsWith('.example.com') ? origin : 'http://example.com')`。默认值为 `*`。 ### allowMethods:`string[]` | `(origin: string, c: Context) => string[]` 对应 `_Access-Control-Allow-Methods_` 头的值。也可以传入回调函数,根据来源动态决定允许的方法。默认值为 `['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH']`。 ### allowHeaders:`string[]` 对应 `_Access-Control-Allow-Headers_` 头的值。默认值为 `[]`。 ### maxAge:`number` 对应 `_Access-Control-Max-Age_` 头的值。 ### credentials:`boolean` 对应 `_Access-Control-Allow-Credentials_` 头的值。 ### exposeHeaders:`string[]` 对应 `_Access-Control-Expose-Headers_` 头的值。默认值为 `[]`。 ## 根据环境配置 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) - Content-Type 为 HTML 表单可发送的类型(`application/x-www-form-urlencoded`、`multipart/form-data` 或 `text/plain`) 旧版浏览器可能不会发送 `Origin` 头,或运行在移除了这些头部的反向代理环境中,此时该中间件效果会受限,建议改用其他基于 CSRF Token 的方案。 ## 导入 ```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'] })) // 动态校验来源 // 强烈建议验证协议并确保以 `$` 结尾匹配。 // 切勿使用前缀匹配。 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` 利用 [Fetch Metadata](https://web.dev/articles/fetch-metadata) 机制,指定允许通过 CSRF 校验的 `Sec-Fetch-Site` 请求头: - **`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 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 中间件允许你在渲染 JSX 时,通过 `c.render()` 设置页面布局,而无需调用 `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` 或传入一个对象时,会以流式响应的方式渲染。 ```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' } ``` 传入对象时,可自定义这些头部的具体取值。 ## 嵌套布局 通过 `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 中若启用 JSX 的 `precompile` 选项,将无法使用 `useRequestContext()`。请改用 `react-jsx`: ```json "compilerOptions": { "jsx": "precompile", // [!code --] "jsx": "react-jsx", // [!code ++] "jsxImportSource": "hono/jsx" } } ``` ::: ## 扩展 `ContextRenderer` 通过如下方式扩展 `ContextRenderer`,即可向渲染器传入额外数据。例如针对不同页面修改 `` 中的内容。 ```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` 请求头及其他配置来源(如设置了 `cookie` 选项时的 Cookie)。中间件会使用提供的 `keys` 验证令牌,或在指定 `jwks_uri` 时从该地址拉取公钥;如果设置了 `cookie` 选项,还会从 Cookie 中提取令牌。 :::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`, }) ) 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`, }) ) 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`, 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', }, { cf: { cacheEverything: true, cacheTtl: 3600 }, } ) ``` ## 选项 ### keys:`HonoJsonWebKey[] | (c: Context) => Promise` 公钥数组,或返回公钥数组的函数。若传入函数,将收到 Context 作为参数。 ### jwks_uri:`string` | `(c: Context) => Promise` 若设置该值,将会从对应 URI 拉取 JWK 列表(JSON 中的 `keys` 字段),并与 `keys` 选项中提供的公钥合并。也可以传入回调函数,根据 Context 动态生成 URI。 ### allow_anon:`boolean` 设为 `true` 时,即使请求未携带有效令牌也允许通过。可通过 `c.get('jwtPayload')` 判断请求是否已认证。默认值为 `false`。 ### cookie:`string` 若设置该值,将使用该键名从 Cookie 头中提取令牌并进行验证。 ### headerName:`string` 要读取 JWT 的请求头名称,默认为 `Authorization`。 # JWT Auth 中间件 JWT Auth 中间件会通过验证 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', }) ) 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', 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, }) return jwtMiddleware(c, next) }) ``` ::: ## 选项 ### secret:`string` 用于签名的密钥。 ### cookie:`string` 若设置该值,将使用对应键名从 Cookie 头中提取令牌并进行验证。 ### alg:`string` 用于验证的算法类型,默认值为 `HS256`。 可用算法包括:`HS256` | `HS384` | `HS512` | `RS256` | `RS384` | `RS512` | `PS256` | `PS384` | `PS512` | `ES256` | `ES384` | `ES512` | `EdDSA`。 ### headerName:`string` 要读取 JWT 的请求头名称,默认是 `Authorization`。 ```ts app.use( '/auth/*', jwt({ secret: 'it-is-very-secret', headerName: 'x-custom-auth-header', }) ) ``` ### verifyOptions:`VerifyOptions` 用于控制令牌验证的选项。 #### verifyOptions.iss:`string | RegExp` 期望的发行者(issuer)。若未设置,则不会校验 `iss` 声明。 #### verifyOptions.nbf:`boolean` 当设置为 `true` 时,如果令牌包含 `nbf`(not before)声明,将进行校验。默认值为 `true`。 #### verifyOptions.iat:`boolean` 当设置为 `true` 时,如果令牌包含 `iat`(issued at)声明,将进行校验。默认值为 `true`。 #### verifyOptions.exp:`boolean` 当设置为 `true` 时,如果令牌包含 `exp`(expiration)声明,将进行校验。默认值为 `true`。 # Language 中间件 Language Detector 中间件会自动识别用户首选语言(Locale),并通过 `c.get('language')` 提供结果。它支持从查询参数、Cookie、请求头以及 URL 路径等多种来源检测语言,非常适合用在国际化(i18n)与按地区定制内容的场景。 ## 导入 ```ts import { Hono } from 'hono' import { languageDetector } from 'hono/language' ``` ## 基础用法 以下示例会按照默认顺序(查询参数 → Cookie → 请求头)检测语言,并在无法识别时回退到英文: ```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/ # 通过请求头 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`) ## 高级配置 ### 自定义检测顺序 优先从路径(如 `/en/about`)检测: ```ts app.use( languageDetector({ order: ['path', 'cookie', 'querystring', 'header'], lookupFromPathIndex: 0, // /en/profile → 索引 0 对应 'en' supportedLanguages: ['en', 'ar'], fallbackLanguage: 'en', }) ) ``` ### 转换语言代码 对复杂的语言代码进行归一化(例如 `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', // SameSite 策略 secure: true, // 仅通过 HTTPS 发送 maxAge: 86400 * 365, // 有效期 1 年 httpOnly: true, // 前端 JS 不可访问 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'` | 请求头名称 | | `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 方法、请求路径、响应状态码以及请求/响应耗时。 - **状态码着色**:不同范围的状态码会使用不同颜色,方便快速识别。 - **耗时显示**:请求-响应的耗时以可读的格式输出(毫秒或秒)。 借助 Logger 中间件,你可以更轻松地监控 Hono 应用的请求/响应流,并快速定位问题或性能瓶颈。 你还可以传入自定义的 `PrintFunc` 来扩展日志输出方式。 ## PrintFunc Logger 中间件可以接收一个可选的 `PrintFunc` 参数,用于自定义日志行为或追加额外信息。 ## 选项 ### fn:`PrintFunc(str: string, ...rest: string[])` - `str`:由 Logger 中间件传入的主消息。 - `...rest`:需要一并输出到控制台的其他字符串。 ### 示例 为 Logger 中间件提供自定义 `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 // Blog saved: Path: /blog/example, ID: 1 // --> POST /blog 201 93ms // 返回 Context }) ``` # Method Override 中间件 该中间件会根据表单、请求头或查询参数中的配置,将请求转交给与原始方法不同的处理器并返回其响应。 ## 导入 ```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 响应正文启用“美化打印”。 只需在 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`。 # 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` 变量访问该 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 当请求头中包含自定义 ID(默认读取 `X-Request-Id`)时,中间件会直接使用该值而不再生成新的 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` 请求 ID 的最大长度,默认值为 `255`。 ### headerName:`string` 用于读取请求 ID 的请求头名称,默认值为 `X-Request-Id`。 ### generator:`(c: Context) => string` 自定义的请求 ID 生成函数,默认使用 `crypto.randomUUID()`。 # 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 | (删除该头) | True | | contentSecurityPolicy | [Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) | 用法:见[配置 Content-Security-Policy](#setting-content-security-policy) | No Setting | | 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) | No Setting | | 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) | No Setting | | 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) | No Setting | | 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) | No Setting | ## 中间件冲突 如果多个中间件会修改同一个头部,需要注意执行顺序。 如下所示,Secure Headers 先执行,会移除 `x-powered-by`: ```ts const app = new Hono() app.use(secureHeaders()) app.use(poweredBy()) ``` 而以下顺序会先执行 Powered-By,再由 Secure Headers 保留其结果: ```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', }, ], // 或者使用 reportTo // 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', 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): ```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 }>() // 为 scriptSrc 设置预定义的 nonce 值: 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 Token(JWT)。JWT 常用于 Web 应用中的身份验证与授权,本助手支持多种密码算法,提供完备的 JWT 能力。 ## 导入 使用示例: ```ts import { decode, sign, verify } from 'hono/jwt' ``` ::: info [JWT 中间件](/docs/middleware/builtin/jwt) 同样从 `hono/jwt` 中导入 `jwt` 函数。 ::: ## `sign()` 该函数会对载荷进行编码,并使用指定算法与密钥生成 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 校验](#payload-校验) 添加更多声明。 #### secret: `string` 用于签名或验证 JWT 的密钥。 #### alg: [AlgorithmTypes](#支持的-algorithmtypes) JWT 的签名/验证算法,默认为 HS256。 ## `verify()` 该函数会校验 JWT 的真实性与有效性,确保 Token 未被篡改;若你提供了 [Payload 校验](#payload-校验) 所需字段,也会同步检查这些约束。 ```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) console.log(decodedPayload) ``` ### 配置项
#### token: `string` 待验证的 JWT。 #### secret: `string` 用于签名或验证 JWT 的密钥。 #### alg: [AlgorithmTypes](#支持的-algorithmtypes) JWT 的签名/验证算法,默认为 HS256。 #### issuer: `string | RegExp` 期望的签发者,用于验证。 ## `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,适合调试或提取信息。 ## Payload 校验 在验证 JWT 时,会针对以下载荷字段执行检查: - `exp`:确认 Token 是否已过期。 - `nbf`:确认 Token 是否在允许使用的时间之前被调用。 - `iat`:确认 Token 的签发时间不是未来。 - `iss`:确认 Token 是否由可信的签发者生成。 如果你希望在验证过程中启用这些检查,请确保 JWT 载荷中包含相应的字段。 ## 自定义错误类型 模块定义了若干自定义错误,便于处理与 JWT 相关的问题。 - `JwtAlgorithmNotImplemented`:请求的 JWT 算法尚未实现。 - `JwtTokenInvalid`:JWT 无效。 - `JwtTokenNotBefore`:Token 在允许时间之前被使用。 - `JwtTokenExpired`:Token 已过期。 - `JwtTokenIssuedAt`:Token 的 `iat` 声明不正确。 - `JwtTokenIssuer`:Token 的 `iss` 声明不正确。 - `JwtTokenSignatureMismatched`:Token 签名不匹配。 ## 支持的 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(SHA-256) 的 RSASSA-PSS - `PS384`:基于 SHA-384 与 MGF1(SHA-384) 的 RSASSA-PSS - `PS512`:基于 SHA-512 与 MGF1(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` 请求头,并清理不必要的响应头,最终返回一个可直接用于响应处理器的 `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, }) }) ``` ### `ProxyFetch` `proxy()` 的类型定义为 `ProxyFetch`,如下所示: ```ts interface ProxyRequestInit extends Omit { raw?: Request customFetch?: (request: Request) => Promise 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()` 返回当前处理器注册的路由路径模式。 ```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, head) => { return c.html( {head.title ?? ''}

{content}

) }) await next() }) app.get('/about', (c) => { return c.render('Hello!', { title: 'Hono SSG Page' }) }) 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` 插件可以更轻松地完成以上流程。 详情请参阅: 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` 会返回如下结果类型: ```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')) ``` ## 中间件 Hono 提供了一些内置中间件来辅助 SSG。 ### ssgParams 与 Next.js 的 `generateStaticParams` 类似,你可以按下列方式生成静态参数: ```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}

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

)) ``` ## 插件 通过插件可以扩展静态站点生成流程,它们通过钩子在不同阶段定制行为。 ### 钩子类型 插件可使用以下钩子定制 `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` 的插件: ```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` 头部的流式响应。 ```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()` 用于无缝推送 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) } }) }) ``` ## 错误处理 Streaming 助手的第三个参数是错误处理函数。该参数可选,如果未提供,错误会被输出到控制台。 ```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 如果 Streaming 助手的回调函数抛出错误,将不会触发 Hono 的 `onError` 事件。 `onError` 用于在响应发送前拦截错误并覆盖响应。然而在回调执行时,流已经开始传输,因而无法再覆盖。 ::: # Testing 助手 Testing 助手提供了一些函数,帮助你更轻松地测试 Hono 应用。 ## 导入 ```ts import { Hono } from 'hono' import { testClient } from 'hono/testing' ``` ## `testClient()` `testClient()` 接收一个 Hono 实例作为第一个参数,返回一个按路由类型定义好的对象,类似于 [Hono Client](/docs/guides/rpc#client)。借助它可以在测试中以类型安全的方式调用各路由,并享受编辑器的自动补全。 **关于类型推断的重要说明:** 为了让 `testClient` 正确推断路由类型并提供自动补全,**必须直接在 `Hono` 实例上通过链式调用定义路由**。 类型推断依赖 `.get()`、`.post()` 等链式调用中流动的类型。如果你按照传统的 “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'], }) }) }) ``` 如需在测试请求中携带请求头,可将其作为第二个参数传入。该参数也支持 `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', () => { 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 助手 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 的处理器。 ```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 助手的路由上同时使用会修改头部的中间件(例如 CORS),可能会遇到 “无法修改不可变头部” 的错误,因为 `upgradeWebSocket()` 内部也会修改头部。使用 WebSocket 助手与中间件时请格外注意。 ::: ## RPC 模式 通过 WebSocket 助手定义的处理器可配合 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 + JSX ```tsx import { Hono } from 'hono' import { createBunWebSocket } from 'hono/bun' import { html } from 'hono/html' const { upgradeWebSocket, websocket } = createBunWebSocket() 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}`) }) ``` ## 使用模式验证请求 如果需要验证请求,请使用模式验证器,而不要手写验证逻辑。 ```ts import { z } from 'zod' import { zValidator } from '@hono/zod-validator' app.post( '/auth', zValidator( 'json', z.object({ email: z.string().email(), password: z.string().min(8), }) ), async (c) => { const { email, password } = c.req.valid('json') return c.json({ email, password }) } ) ``` ## 在调用 `app.get()` 之前定义配置 这是来自 JavaScript 模块的最佳实践: 在调用函数之前先定义配置对象。 ```ts const route = { method: 'GET', path: '/', } app.get(route, (c) => c.text('Hello')) ``` ## 不要为了分组而重复使用 `app.route` 只有在需要为同一路径提供多个 HTTP 方法时,才应该使用 `app.route`。 如果只是想划分路由分组,就会显得多此一举。 ```ts const app = new Hono() const books = app.route('/books') books.get('/', (c) => c.json(['Hello'])) books.get('/:id', (c) => c.json({ id: c.req.param('id') })) ``` 在这种情况下就显得冗余了。直接写成下面这样能得到相同的效果。 ```ts app.get('/books', (c) => c.json(['Hello'])) app.get('/books/:id', (c) => c.json({ id: c.req.param('id') })) ``` ## 更倾向使用 `app.on` 而不是 `app.xxx` Hono 提供了 `app.on`,可以为同一路径定义多个 HTTP 方法。建议使用 `app.on`,而不是 `app.xxx`。 ```ts app.on(['GET', 'POST'], '/books', (c) => { return c.json(['Hello']) }) ``` ## 使用 `c.req.param()` 而非 `c.req.param['id']` 请使用函数形式的 `c.req.param()`,而不是访问器 `c.req.param['id']`,以避免触发 getter。 ```ts app.get('/books/:id', (c) => { const id = c.req.param('id') return c.json({ id }) }) ``` ## 用模板字符串拼接响应 拼接响应文本时,请使用模板字符串,而不是字符串相加。 ```ts app.get('/hello/:name', (c) => { const { name } = c.req.param() return c.text(`Hello ${name}!`) }) ``` ## 日志 使用 `console.log` 和 `console.error` 输出日志信息。 ```ts app.get('/hello', async (c) => { const start = Date.now() const res = await c.req.parseBody() const end = Date.now() console.log(`[${c.req.method}] ${c.req.path} - ${end - start}ms`) return c.json(res) }) ``` ## 用 `c.var` 复用逻辑 如果有重复使用的逻辑,可以通过 `c.set` 和 `c.var` 进行复用。 ```ts app.use('*', async (c, next) => { c.set('database', await getDatabase()) await next() }) app.get('/users', async (c) => { const db = c.var.database const users = await db.users.findMany() return c.json(users) }) ``` ## 等待 Response `Response` 只能读取一次。等待响应会消耗它。 ```ts app.get('/', async (c) => { const res = await fetch('https://example.com') const json = await res.json() return c.json(json) }) ``` # 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