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.tsts
// 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,
}));性能建议
- 缓存静态数据 — 使用
hono/cache中间件 - 使用 D1 预编译语句 — 永远不要内联用户输入
- 流式返回长响应 — 使用
streamSSE或stream - 批量调用 LLM — 不要按 NPC 逐个调用,按 prompt 相似度分批
- 精简中间件 — 每个中间件都会消耗 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 环境中运行测试。