spinny:~/writing $ vim hono-framework-guide.md
1~2多年来,选择 JavaScript Web 框架意味着接受权衡:Express 通用但慢且绑定 Node,Fastify 快但只能用于 Node,Next.js 功能完整但沉重。当边缘运行时——Cloudflare Workers、Deno Deploy、Bun、Vercel Edge——出现时,这些框架显露出局限:依赖不兼容、bundle 巨大、API 绑定 Node 的 `req`/`res`。3~4[Hono](https://hono.dev)(日语意为"火焰" 🔥)是现代答案。一个不到 14KB 的框架,完全基于 Web Standards(`Request`、`Response`、`fetch`),在任何 JavaScript 运行时上都能运行。同一份代码可部署到 Cloudflare Workers、Bun、Deno、Node.js、Vercel、Netlify 和 AWS Lambda——无需修改。5~6## 为什么选 Hono7~8Hono 在三件事上比所有人做得都好:9~101. **性能。** `RegExpRouter` 将所有路由模式编译成单个正则表达式,避免传统路由器的线性循环。基准测试超过每秒 40 万次操作,使 Hono 跻身 JavaScript 生态系统中最快的路由器之列。112. **可移植性。** Web Standards 意味着零 Node 依赖。同一个 `app.fetch` 在 Cloudflare Worker 中作为默认导出、传递给 `Bun.serve`、挂载在 Deno 服务器上,或通过 `@hono/node-server` 适配。123. **TypeScript 优先的 DX。** 路径参数被推断为字面量类型,端到端类型安全的 RPC 客户端,能推断输入输出类型的验证器。自动补全几乎像心灵感应。13~14```mermaid15graph LR16 Client[Client] -->|Request| App[app.fetch]17 App --> MW1[Middleware 1]18 MW1 --> MW2[Middleware 2]19 MW2 --> Router[RegExpRouter]20 Router --> Handler[Route Handler]21 Handler --> Context[c.json / c.text]22 Context -->|Response| Client23 App -.->|deploy| CF[Cloudflare Workers]24 App -.->|deploy| Bun[Bun]25 App -.->|deploy| Deno[Deno]26 App -.->|deploy| Node[Node.js]27 App -.->|deploy| Vercel[Vercel]28```29~30## 开始使用31~32最快的方式是官方启动器,它会为你选择的运行时搭建项目。33~34```bash35npm create hono@latest my-api36cd my-api37npm install38npm run dev39```40~41启动器会询问使用哪个模板:`cloudflare-workers`、`bun`、`deno`、`nodejs`、`vercel`、`aws-lambda`、`nextjs` 等。它会选择合适的配置和部署脚本。要快速尝试,也可以从单个文件开始:42~43```typescript44// src/index.ts45import { Hono } from 'hono'46~47const app = new Hono()48~49app.get('/', (c) => c.text('Hello Hono!'))50~51export default app52```53~54在 Cloudflare Workers 上这就够了。在 Bun:`Bun.serve({ fetch: app.fetch, port: 3000 })`。在 Node:来自 `@hono/node-server` 的 `serve({ fetch: app.fetch })`。接口完全相同。55~56## 路由57~58路由用 HTTP 动词方法声明,支持参数、通配符和正则。59~60```typescript61import { Hono } from 'hono'62~63const app = new Hono()64~65app.get('/', (c) => c.text('Home'))66app.get('/posts/:id', (c) => {67 const id = c.req.param('id') // string,已类型化68 return c.json({ id })69})70app.get('/posts/:id/comments/:commentId', (c) => {71 const { id, commentId } = c.req.param()72 return c.json({ id, commentId })73})74app.get('/files/*', (c) => c.text('Wildcard'))75app.post('/posts', async (c) => {76 const body = await c.req.json()77 return c.json({ created: body }, 201)78})79```80~81参数被推断为字面量类型:TypeScript 知道 `c.req.param('id')` 仅当你在模式中声明了 `:id` 时才返回 `string`。拼错名字是编译时错误。82~83### 分组路由84~85`app.route()` 允许将子应用作为模块组合,每个都有自己的前缀。86~87```typescript88// routes/posts.ts89import { Hono } from 'hono'90~91const posts = new Hono()92posts.get('/', (c) => c.json({ posts: [] }))93posts.get('/:id', (c) => c.json({ id: c.req.param('id') }))94export default posts95~96// src/index.ts97import { Hono } from 'hono'98import posts from './routes/posts'99~100const app = new Hono()101app.route('/posts', posts)102```103~104嵌套路由继承根应用的 base path 和类型,所以 RPC 客户端能看到整个结构。105~106## Context 对象107~108每个 handler 都接收一个 `c`——当前请求的 Context。这是你需要学习的唯一 API,用于读取输入和产生输出。109~110```typescript111app.post('/echo', async (c) => {112 // 读取113 const userAgent = c.req.header('User-Agent')114 const page = c.req.query('page')115 const body = await c.req.json()116 const env = c.env // bindings(Cloudflare 上的 KV、D1、secrets)117~118 // 在中间件和 handler 之间共享变量119 c.set('requestId', crypto.randomUUID())120 const id = c.get('requestId')121~122 // 响应123 c.header('X-Request-Id', id)124 c.status(200)125 return c.json({ userAgent, page, body, id })126})127```128~129主要响应方法是 `c.text`、`c.json`、`c.html`、`c.body`(raw)和 `c.redirect`。所有方法都接受状态码作为第二个参数。130~131## 中间件:洋葱模型132~133中间件是 `(c, next) => ...` 函数,可以在 handler 前后运行代码。组合起来形成经典的洋葱模型:第一个注册的中间件最先开始,最后结束。134~135```typescript136import { Hono } from 'hono'137import { logger } from 'hono/logger'138import { cors } from 'hono/cors'139import { secureHeaders } from 'hono/secure-headers'140~141const app = new Hono()142~143app.use('*', logger())144app.use('*', secureHeaders())145app.use('/api/*', cors({ origin: 'https://spinny.dev' }))146~147app.use('*', async (c, next) => {148 const start = performance.now()149 await next()150 c.header('X-Response-Time', `${performance.now() - start}ms`)151})152```153~154`await next()` 将控制权交给下一个中间件。其后的代码在响应阶段运行,即在 handler 产生结果之后。在 `next()` 之前返回 `Response` 会短路链。155~156### 内置中间件157~158Hono 自带丰富的生产就绪中间件,可从 `hono/...` 导入:159~160| 中间件 | 用途 |161|-----------|---------|162| `logger` | 方法、路径、状态、时长的结构化日志 |163| `cors` | 可按 origin、方法、headers 配置的 CORS |164| `csrf` | 基于 origin 的 CSRF 防护 |165| `secureHeaders` | 设置 CSP、HSTS、X-Frame-Options |166| `bearerAuth` / `basicAuth` | 开箱即用的 Bearer/Basic 认证 |167| `jwt` | 用 `jose` 验证和签名 JWT |168| `etag` | 生成 ETag 并处理 304 |169| `cache` | 通过 Web Cache API 缓存 |170| `compress` | 响应 gzip/deflate |171| `bodyLimit` | 拒绝超过阈值的 body |172| `timing` | 用于性能分析的 Server-Timing 头 |173~174### 类型安全的自定义中间件175~176要用类型化变量扩展 `Context`,使用 `createMiddleware`:177~178```typescript179import { createMiddleware } from 'hono/factory'180~181type AuthVars = { userId: string; role: 'user' | 'admin' }182~183export const requireAuth = createMiddleware<{ Variables: AuthVars }>(184 async (c, next) => {185 const token = c.req.header('Authorization')?.replace('Bearer ', '')186 if (!token) return c.json({ error: 'Unauthorized' }, 401)187~188 const payload = await verifyJwt(token)189 c.set('userId', payload.sub)190 c.set('role', payload.role)191 await next()192 }193)194~195// 使用196app.get('/me', requireAuth, (c) => {197 const userId = c.var.userId // string,已类型化198 return c.json({ userId })199})200```201~202中间件下游,`c.var.userId` 无需任何 cast 即被类型化。这会贯穿整个链。203~204## 用 Zod 验证205~206`@hono/zod-validator` 将 Zod 接入请求周期。定义 schema,应用到路由,得到已验证、已类型化的输入。207~208```typescript209import { Hono } from 'hono'210import { zValidator } from '@hono/zod-validator'211import { z } from 'zod'212~213const createPost = z.object({214 title: z.string().min(1).max(200),215 body: z.string().min(1),216 tags: z.array(z.string()).default([]),217})218~219app.post(220 '/posts',221 zValidator('json', createPost),222 (c) => {223 const data = c.req.valid('json') // 按 schema 类型化224 return c.json({ ok: true, post: data }, 201)225 }226)227```228~229如果 body 验证失败,Hono 会在调用 handler 之前以 Zod 错误响应 400。也可以验证 `query`、`param`、`header`、`cookie` 和 `form`。230~231## RPC:端到端类型安全的客户端232~233真正让 Hono 与众不同的特性是 **RPC 模式**。从服务器导出应用类型,`hc` 客户端导入它,你就获得完整的自动补全——包括路径、查询、body、headers 和响应——无需 codegen 或 OpenAPI。234~235```typescript236// server.ts237import { Hono } from 'hono'238import { zValidator } from '@hono/zod-validator'239import { z } from 'zod'240~241const app = new Hono()242 .get('/posts/:id', (c) =>243 c.json({ id: c.req.param('id'), title: 'Hello' })244 )245 .post(246 '/posts',247 zValidator('json', z.object({ title: z.string(), body: z.string() })),248 (c) => c.json({ ok: true }, 201)249 )250~251export type AppType = typeof app252export default app253```254~255```typescript256// client.ts257import { hc } from 'hono/client'258import type { AppType } from './server'259~260const client = hc<AppType>('https://api.spinny.dev')261~262const res = await client.posts[':id'].$get({ param: { id: '42' } })263if (res.ok) {264 const data = await res.json() // { id: string, title: string }265 console.log(data.title)266}267~268const created = await client.posts.$post({269 json: { title: '你好', body: 'Hono 是火焰' }, // 已验证270})271```272~273在服务器上重命名一个路由,客户端的 TypeScript 立即在 CI 中失败。这与 tRPC 同样有优势,但基于标准 HTTP,没有特定中间件,bundle 极小。274~275### 状态码区分276~277如果返回不同状态,客户端会自动区分。278~279```typescript280.get('/posts/:id', (c) => {281 const post = findPost(c.req.param('id'))282 if (!post) return c.json({ error: 'not found' }, 404)283 return c.json({ post }, 200)284})285```286~287```typescript288const res = await client.posts[':id'].$get({ param: { id } })289if (res.status === 404) {290 const { error } = await res.json() // { error: string }291}292if (res.status === 200) {293 const { post } = await res.json() // { post: Post }294}295```296~297## 路由器与性能298~299Hono 提供五种具有不同权衡的路由器。默认是 `SmartRouter`,它在启动时测量哪个路由器最适合你的路由并锁定。300~301| 路由器 | 优势 | 何时使用 |302|--------|-----------|-------------|303| `RegExpRouter` | 速度最快,编译正则 | 大多数 API 的默认 |304| `TrieRouter` | 支持所有模式 | RegExp 处理不了的复杂模式 |305| `SmartRouter` | 自动选择最佳 | 推荐默认 |306| `LinearRouter` | 注册超快 | One-shot worker,冷启动关键 |307| `PatternRouter` | 最小 bundle(<15KB) | 极端尺寸限制 |308~309对于频繁冷启动的无状态 worker,`LinearRouter` 跳过初始编译成本,在测量注册加匹配时比 `find-my-way` 快 33 倍。310~311```typescript312import { Hono } from 'hono'313import { LinearRouter } from 'hono/router/linear-router'314~315const app = new Hono({ router: new LinearRouter() })316```317~318## 多运行时部署319~320同一个 `export default app` 仅通过更换入口文件就能切换目标。321~322### Cloudflare Workers323~324```typescript325// src/index.ts326import { Hono } from 'hono'327~328type Bindings = { MY_KV: KVNamespace; DB: D1Database }329const app = new Hono<{ Bindings: Bindings }>()330~331app.get('/cache/:key', async (c) => {332 const value = await c.env.MY_KV.get(c.req.param('key'))333 return c.json({ value })334})335~336export default app337```338~339部署:`npx wrangler deploy`。KV/D1/R2/Queues 绑定原生支持。340~341### Bun342~343```typescript344import { Hono } from 'hono'345const app = new Hono()346app.get('/', (c) => c.text('Bun + Hono'))347~348Bun.serve({ fetch: app.fetch, port: 3000 })349```350~351### Node.js352~353```typescript354import { serve } from '@hono/node-server'355import { Hono } from 'hono'356~357const app = new Hono()358app.get('/', (c) => c.text('Node + Hono'))359~360serve({ fetch: app.fetch, port: 3000 })361```362~363### Deno364~365```typescript366import { Hono } from 'jsr:@hono/hono'367const app = new Hono()368app.get('/', (c) => c.text('Deno + Hono'))369Deno.serve(app.fetch)370```371~372### Vercel373~374```typescript375// api/[[...route]].ts376import { Hono } from 'hono'377import { handle } from 'hono/vercel'378~379const app = new Hono().basePath('/api')380app.get('/hello', (c) => c.json({ msg: 'Hello from Vercel' }))381~382export const GET = handle(app)383export const POST = handle(app)384```385~386相同的业务代码、相同的路由、相同的中间件。仅适配器不同。387~388## 真实示例:带认证和数据库的 REST API389~390把所有部分组合起来。一个 Cloudflare Workers + D1 上的博客 API,带 JWT 认证、验证和 RPC。391~392```typescript393import { Hono } from 'hono'394import { jwt } from 'hono/jwt'395import { logger } from 'hono/logger'396import { cors } from 'hono/cors'397import { zValidator } from '@hono/zod-validator'398import { z } from 'zod'399~400type Bindings = { DB: D1Database; JWT_SECRET: string }401type Variables = { jwtPayload: { sub: string } }402~403const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()404~405app.use('*', logger())406app.use('/api/*', cors({ origin: 'https://spinny.dev', credentials: true }))407~408const auth = (c: any, next: any) =>409 jwt({ secret: c.env.JWT_SECRET })(c, next)410~411const api = app.basePath('/api')412~413api.get('/posts', async (c) => {414 const { results } = await c.env.DB415 .prepare('SELECT id, title, created_at FROM posts ORDER BY created_at DESC LIMIT 50')416 .all()417 return c.json({ posts: results })418})419~420api.post(421 '/posts',422 auth,423 zValidator('json', z.object({424 title: z.string().min(1).max(200),425 body: z.string().min(1),426 })),427 async (c) => {428 const { title, body } = c.req.valid('json')429 const userId = c.var.jwtPayload.sub430 const result = await c.env.DB431 .prepare('INSERT INTO posts (title, body, author_id) VALUES (?, ?, ?) RETURNING id')432 .bind(title, body, userId)433 .first<{ id: number }>()434 return c.json({ id: result?.id }, 201)435 }436)437~438api.onError((err, c) => {439 console.error(err)440 return c.json({ error: 'Internal error' }, 500)441})442~443export type AppType = typeof api444export default app445```446~447React 客户端以完整的类型安全消费它:448~449```typescript450import { hc } from 'hono/client'451import type { AppType } from '../api/src/index'452~453const api = hc<AppType>(import.meta.env.VITE_API_URL)454~455const res = await api.posts.$post({456 json: { title: '2026 年的 Hono', body: '...' },457}, {458 headers: { Authorization: `Bearer ${token}` },459})460```461~462## 测试463~464`app.request()` 让你无需启动 HTTP 服务器即可测试路由。这是与生产相同的路径,在内存中执行。465~466```typescript467import { describe, it, expect } from 'vitest'468import app from '../src/index'469~470describe('GET /api/posts', () => {471 it('返回帖子列表', async () => {472 const res = await app.request('/api/posts')473 expect(res.status).toBe(200)474 const body = await res.json()475 expect(body.posts).toBeInstanceOf(Array)476 })477})478```479~480在 Cloudflare Workers 上,`@cloudflare/vitest-pool-workers` 在带模拟 binding 的真实 Worker 内运行相同的测试——最大真实性,零部署。481~482## 最佳实践483~484### 1. 链式定义路由485~486```typescript487const app = new Hono()488 .get('/posts', handler1)489 .post('/posts', handler2)490 .get('/posts/:id', handler3)491```492~493链式让 `app` 的类型在每条路由后保持最新,对 RPC 客户端至关重要。在不同行用 `app.get(...)` 单独定义会破坏推断。494~495### 2. 导出类型,而非实现496~497客户端必须导入 `AppType`,而不是 app。`import type` 确保前端 build 不包含后端代码。498~499### 3. 每个领域一个路由器500~501一个子应用用于 `posts`,一个用于 `users`,一个用于 `webhooks`。用 `app.route()` 组合,每个都拥有自己的中间件。结构无需巨型文件即可扩展。502~503### 4. 始终在边界验证504~505每个外部输入(body、query、header)都必须经过 `zValidator`。不要信任下游数据:即使没有运行时验证的 TypeScript cast 也是潜伏的 bug。506~507### 5. 依赖 binding,而非全局客户端508~509在 Cloudflare 上,通过 `c.env` 访问 KV/D1/R2。无全局单例,无跨 Worker 持久化的连接。无状态模型是特性,不是限制。510~511### 6. 优化路由器前先测量512~513默认的 `SmartRouter` 适用于 95% 的情况。仅在分析并发现真实瓶颈后才切换路由器。514~515## 结论516~517到 2026 年,Hono 已成为用 TypeScript 构建边缘就绪 API 的事实标准。Web Standards、性能、类型安全和可移植性的组合,恰好解决了束缚传统框架的问题:运行时锁定、沉重的 bundle、客户端与服务器之间脆弱的类型系统。518~519你将用它构建 Cloudflare Workers 上的微服务,替换 Node 单体中的 Express,构建 Vercel 上的函数,或 Bun 上的 API。同样的知识在所有地方都通用——这正是它在未来几年成为稳健投资的原因。520~521> **入门检查清单:**522>523> - [x] `npm create hono@latest` 并选择你的运行时模板524> - [x] 用链式定义路由(`.get(...).post(...)`)525> - [x] 添加 `logger`、`cors`、`secureHeaders` 作为全局中间件526> - [x] 用 `@hono/zod-validator` 验证每个输入527> - [x] 导出 `AppType` 并用类型安全的 `hc` 客户端消费 API528> - [x] 用 `app.request()` 编写测试——无需 HTTP 服务器529> - [x] 用 `wrangler deploy`(CF)、`vercel deploy` 或你运行时的 bundler 部署530~
NORMAL · hono-framework-guide.md [readonly]530 lines · :q to close