Skip to content

Hono

整个后端的 HTTP 路由。轻量、快速、TypeScript 优先。

为什么选 Hono?

选择 Hono 作为后端路由,原因如下:

  • 轻量 — 运行时体积 <14KB,Workers 生态中冷启动最快
  • 🟦 TypeScript 优先 — 路由定义自带完整类型推导
  • 🌐 多运行时 — 兼容 Workers、Bun、Node、Deno
  • 🧩 中间件生态 — 认证、日志、CORS 等现成可用
  • 📐 上手简单 — 类 Express 的 API,学习曲线平缓

其他考虑过的方案:Itty Router(过于精简)、tRPC(对这个场景来说 RPC 味太重)。


基本配置

ts
import { Hono } from 'hono';

const app = new Hono<{ Bindings: Env }>();

app.get('/', (c) => c.text('AI World Sim API'));

export default app;

路由组织

将路由拆分为独立模块:

src/
├── router.ts           # 挂载所有子路由
└── routes/
    ├── world.ts
    ├── npc.ts
    ├── diary.ts
    ├── suggestions.ts
    ├── sse.ts
    └── admin.ts
ts
// router.ts
import { Hono } from 'hono';
import { worldRoutes } from './routes/world';
import { npcRoutes } from './routes/npc';

const app = new Hono<{ Bindings: Env }>();

app.route('/world', worldRoutes);
app.route('/npc', npcRoutes);

app.get('/', (c) => c.json({ name: 'AI World Sim API', version: '0.1.0' }));

export default app;

中间件栈

ts
import { logger } from 'hono/logger';
import { cors } from 'hono/cors';
import { secureHeaders } from 'hono/secure-headers';

app.use('*', logger());
app.use('*', secureHeaders());
app.use('*', cors({
  origin: ['https://aiworldsim.pages.dev', 'http://localhost:3000'],
  credentials: true,
}));

SSE 支持

Hono 通过 streamSSE 提供原生 SSE 支持:

ts
import { streamSSE } from 'hono/streaming';

app.get('/api/sse/connect', async (c) => {
  return streamSSE(c, async (stream) => {
    const subscriber = (event: TickEvent) => {
      stream.writeSSE({
        event: event.type,
        data: JSON.stringify(event),
      });
    };

    tickBroadcaster.on('tick', subscriber);

    stream.onAbort(() => {
      tickBroadcaster.off('tick', subscriber);
    });

    while (!stream.aborted) {
      await stream.sleep(1000);
    }
  });
});

类型安全的 Bindings

通过 Env 类型定义 Cloudflare 绑定:

ts
type Env = {
  DB: D1Database;
  AI: Ai;
  VECTORIZE: VectorizeIndex;
  TICK_DO: DurableObjectNamespace;
  ENVIRONMENT: 'development' | 'production';
};

const app = new Hono<{ Bindings: Env }>();

app.get('/api/npc/:id', async (c) => {
  const id = c.req.param('id');
  const npc = await c.env.DB.prepare(
    'SELECT * FROM npc WHERE id = ?'
  ).bind(id).first();
  return c.json(npc);
});

错误处理

统一的错误响应格式:

ts
app.onError((err, c) => {
  console.error(err);
  return c.json({
    error: {
      code: err.name || 'INTERNAL_ERROR',
      message: err.message || 'Something went wrong',
    },
  }, 500);
});

app.notFound((c) => {
  return c.json({
    error: { code: 'NOT_FOUND', message: `No route for ${c.req.path}` },
  }, 404);
});

使用 Zod 做参数校验

每个请求体都会经过校验:

ts
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';

const suggestionSchema = z.object({
  npcId: z.string().min(1),
  text: z.string().min(3).max(500),
});

app.post('/api/suggestions',
  zValidator('json', suggestionSchema),
  async (c) => {
    const body = c.req.valid('json');
    // ...
  }
);

认证(v1:最小化)

v1 版本没有玩家登录。建议提交是匿名的,按 IP 做速率限制。

ts
import { rateLimiter } from '@/middleware/rateLimiter';

app.use('/api/suggestions/*', rateLimiter({
  windowMs: 60_000,
  max: 5,
}));

性能建议

  1. 缓存静态数据 — 使用 hono/cache 中间件
  2. 使用 D1 预编译语句 — 永远不要内联用户输入
  3. 流式返回长响应 — 使用 streamSSEstream
  4. 批量调用 LLM — 不要按 NPC 逐个调用,按 prompt 相似度分批
  5. 精简中间件 — 每个中间件都会消耗 CPU

测试

ts
import { app } from './router';

describe('GET /world/state', () => {
  it('returns world state', async () => {
    const res = await app.request('/world/state', {
      method: 'GET',
    });
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body).toHaveProperty('worldTime');
  });
});

我们使用 @cloudflare/vitest-pool-workers 在真实的 Workers 环境中运行测试。

Released under the MIT License.