一、引言
裸用 addEventListener("fetch") 写边缘函数,三五条路由还能忍,一旦叠加鉴权、CORS、限流、多路由,代码就会迅速退化成「if 瀑布」。边缘函数框架解决的正是这个问题:用声明式的路由 + 中间件管道把横切逻辑组织起来,让边缘代码像写 Express 一样清晰。
本文聚焦两个主流轻量框架——Hono 与 Itty Router,拆解它们的路由模型、中间件机制、类型方案、运行时适配与测试部署,并给出「框架选型」的决策建议。适合已经在 Cloudflare Workers / Pages Functions 上写过基础函数、想升级工程化的读者。
二、先理解边缘运行时:Fetch API 标准
2.1 边缘函数运行的接口契约
几乎所有边缘运行时(Workers、Pages Functions、Deno Deploy、Bun、Vercel Edge)都实现了同一套 Web 标准接口:
请求:Request(method / url / headers / body)
响应:Response(status / headers / body)
入口:export default { fetch(request, env, ctx) } 或 addEventListener('fetch')
这套契约的好处是「一次编写,多处部署」。框架只需要在这层标准接口上做抽象,就能实现跨运行时的可移植性。
2.2 从裸 fetch 到框架的演进
// 裸写:路由全靠手写
export default {
async fetch(request, env) {
const url = new URL(request.url)
if (url.pathname === '/api/users' && request.method === 'GET') {
return Response.json({ users: [] })
}
if (url.pathname === '/health') {
return Response.json({ ok: true })
}
return new Response('Not Found', { status: 404 })
},
}
// 框架:路由声明式,中间件可复用
import { Hono } from 'hono'
const app = new Hono()
app.get('/api/users', (c) => c.json({ users: [] }))
app.get('/health', (c) => c.json({ ok: true }))
app.notFound((c) => c.text('Not Found', 404))
export default app
心法:框架不是「加魔法」,而是把 Web 标准接口的组织方式标准化——路由表、中间件链、上下文对象,这三件套就是边缘框架的全部核心。
三、Hono:类 Express 的边缘框架
3.1 为什么 Hono 适合边缘
Hono 是专为边缘运行时设计的 TypeScript 框架,几个关键特性:
| 特性 | 说明 |
|---|---|
| 零依赖路由 | 基于 Trie 树的路径匹配,编译后极小(约 14KB) |
| 类 Express 心智 | app.get / app.use / app.post 上手成本低 |
| 多运行时适配 | Workers / Deno / Bun / Node / Vercel 一套代码 |
| 一等 TypeScript | 路由级类型推断,c.req.param 自动带类型 |
| 中间件生态 | 官方提供 jwt、logger、cors、cache 等中间件 |
3.2 Hono 基础路由与上下文
import { Hono } from 'hono'
const app = new Hono()
// 路径参数 + 类型推断
app.get('/users/:id', (c) => {
const id = c.req.param('id') // id 自动推断为 string
return c.json({ id })
})
// query 与 header
app.get('/search', (c) => {
const q = c.req.query('q') ?? ''
const lang = c.req.header('Accept-Language')
return c.json({ q, lang })
})
// 设置响应头
app.post('/api/echo', async (c) => {
const body = await c.req.json()
c.header('X-Echoed', 'true')
return c.json(body)
})
export default app
细节:
c.req.param('id')是 Hono 对类型系统最友好的设计——路由字面量里的占位符能推导出参数名,配合生成器连c.req.param('id')的类型都能自动得到。
3.3 Hono 的辅助函数
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
const app = new Hono()
app.use('*', logger())
app.use('/api/*', cors({ origin: 'https://example.com' }))
app.get('/', (c) => c.text('Hello'))
app.notFound((c) => c.json({ error: 'not_found' }, 404))
app.onError((err, c) => {
console.error(err)
return c.json({ error: 'internal' }, 500)
})
Hono 把 notFound 与 onError 也做成「路由」,这比裸 try-catch 更干净:404 与 500 都有唯一的处理点,中间件和路由共用同一套上下文。
四、Itty Router:极简路由库
4.1 Itty Router 的定位
Itty Router 是另一条路线:极简。它不是一个全功能框架,而是一个「路由 + 中间件」的最小集合,核心代码只有几百行,无任何依赖,适合:
- 只想做路由分发、不想学框架
- 希望包体最小(< 1KB)
- 与现有 Worker 代码渐进式整合
4.2 Itty Router 基本用法
import { Router } from 'itty-router'
const router = Router()
router
.get('/', () => new Response('home'))
.get('/users/:id', (req) => {
const { id } = req.params
return Response.json({ id })
})
.post('/api/echo', async (req) => {
const body = await req.json()
return Response.json(body)
})
.all('*', () => new Response('Not Found', { status: 404 }))
export default {
async fetch(request) {
return router.handle(request)
},
}
对比:Itty Router 直接暴露原生
Request/Response,不做上下文封装——这意味着更少学习成本,也意味着「辅助方法」要自己写(如 JSON 响应封装)。Hono 则是「自带电池」,开箱即用。
4.3 Itty Router 中间件
import { Router } from 'itty-router'
const router = Router()
// 中间件:在路由之前执行,可提前返回响应
const withAuth = (request) => {
const token = request.headers.get('Authorization')
if (!token) {
return new Response('Unauthorized', { status: 401 })
}
// 挂到 request 上,供后续路由读取
request.user = { id: 'u_123' }
}
router
.get('/me', withAuth, (request) => {
return Response.json({ user: request.user })
})
.get('/health', () => Response.json({ ok: true }))
export default {
async fetch(request) {
return router.handle(request)
},
}
Itty Router 的中间件就是「路由处理器数组里的任意函数」——它先于终处理器执行,若返回 Response 则短路,否则继续传参。这种「数组函数」模型直观且零魔法。
五、中间件模式:横切逻辑的组织方式
5.1 中间件链的执行模型
请求 → [logger] → [cors] → [auth] → [rate-limit] → 业务路由 → 响应
↑ 任一中间件 return Response 即短路,后续不再执行
5.2 用中间件实现统一鉴权
以 Hono 为例,把鉴权从业务路由里剥离:
import { Hono } from 'hono'
import { getCookie } from 'hono/cookie'
const app = new Hono()
// 鉴权中间件:校验会话,失败直接 401
app.use('/admin/*', async (c, next) => {
const session = getCookie(c, 'session')
const user = await validateSession(session)
if (!user) return c.json({ error: 'unauthorized' }, 401)
c.set('user', user) // 把用户挂到上下文
await next() // 放行
})
app.get('/admin/dashboard', (c) => {
const user = c.get('user') // 类型安全地读取
return c.json({ welcome: user.name })
})
export default app
中间件模式的收益:
- 职责隔离:鉴权、CORS、日志各管一段,业务路由不再出现「if 没有 token」。
- 可组合:中间件按需挂载,
/public/*不挂鉴权、/admin/*挂鉴权。 - 可测试:每个中间件可以独立注入测试。
5.3 中间件里的异步与错误
app.use('*', async (c, next) => {
const start = Date.now()
try {
await next()
} finally {
const ms = Date.now() - start
c.header('X-Timing', `${ms}ms`)
}
})
铁律:中间件里
await next()之后写「后处理」——这是与 Express 完全一致的洋葱模型。finally 保证无论下游成功还是抛错,计时逻辑都会执行。
六、类型安全与 RPC 风格开发
6.1 路由级类型推断
import { Hono } from 'hono'
import { hc } from 'hono/client' // RPC 客户端
// 服务端定义
const app = new Hono()
app.get('/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ id, name: 'Alice', age: 30 })
})
export type AppType = typeof app
export default app
// 客户端:完全类型安全的 fetch
// const client = hc<AppType>('/')
// const res = await client.users[':id'].$get({ param: { id: '1' } })
// const data = await res.json() // 类型为 { id: string; name: string; age: number }
这是 Hono 最独特的卖点:服务端路由字面量直接生成类型化的客户端。前后端共享一个 AppType,路径、query、响应体全部静态检查,边缘 API 的联调成本降到最低。
6.2 类型安全的 env 与绑定
import { Hono } from 'hono'
// 定义绑定类型
type Env = {
Bindings: {
KV: KVNamespace
API_KEY: string
}
}
const app = new Hono<Env>()
app.get('/secret', (c) => {
const key = c.env.API_KEY // 类型为 string
return c.json({ masked: key.slice(0, 4) + '****' })
})
通过泛型把 c.env 的类型固定下来,避免在代码里到处 (env as any)。绑定错名字、错类型在编译期就能发现。
七、运行时适配、测试与部署
7.1 一个框架适配多个运行时
// workers.ts
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('hello'))
export default app
# 同一套代码,多目标构建
npx wrangler deploy --outdir dist/workers # Cloudflare Workers
npx wrangler pages deploy dist # Cloudflare Pages
deno run --allow-net main.ts # Deno Deploy
bun run src/index.ts # Bun / 本地开发
Hono 官方维护各运行时的适配层,c.req / c.res 会按平台差异自动桥接。选型时可以「先 Hono 后部署」,不锁死单一平台。
7.2 单元测试:用标准 Request/Response
// test/edge.test.ts
import { Hono } from 'hono'
import app from '../src/app'
const req = (path: string, init?: RequestInit) =>
app.request(path, init)
Deno.test('GET /health 返回 ok', async () => {
const res = await req('/health')
assertEquals(res.status, 200)
assertEquals(await res.json(), { ok: true })
})
Deno.test('GET /users/:id 带鉴权', async () => {
const res = await req('/admin/dashboard', {
headers: { Cookie: 'session=bad' },
})
assertEquals(res.status, 401)
})
测试的关键在于:框架不要求真实网络,直接 app.request() 就能构造请求——边缘函数的测试因此可以完全本地化、无需 mock 平台。
7.3 部署与版本回滚
# 生产部署(自动带版本号)
npx wrangler deploy --env production
# 回滚到上一个版本
npx wrangler rollback
# 灰度:先部署 10% 流量观察
npx wrangler deploy --env production --percentage 10
结合 部署与回滚策略 里的金丝雀思路,边缘函数同样可以按百分比放流量,把发布风险控制在最小范围。
八、总结
边缘函数框架的选型与落地,核心要点如下:
- 先吃透运行时契约:Request / Response / fetch 入口是边缘世界的通用语言,框架只是组织方式的标准化。
- Hono 适合完整工程:中间件生态、类型推断、RPC 客户端、多运行时适配,适合想严肃写边缘服务的团队。
- Itty Router 适合极简切入:几百行无依赖,适合只想做路由分发、与既有 Worker 渐进整合的场景。
- 中间件是横切逻辑的家:鉴权、CORS、日志、限流全部抽成中间件,业务路由保持干净。
- 类型安全值得投资:
c.env绑定类型、路由级param推断、hc()RPC 客户端,能把一类「手滑 bug」消灭在编译期。 - 测试不用 mock 网络:
app.request()直接本地构造请求,边缘代码可以跑出覆盖率。 - 部署与回滚要配套:用平台的版本回滚 + 百分比灰度,别让边缘函数成为不可回滚的「一次性代码」。
边缘函数不是「把代码塞到离用户近的地方」这么简单——它是带工程纪律的分布式计算。框架选对、中间件组织好,再配合 边缘缓存策略 的缓存分层与 API 网关与 BFF 的聚合思想,边缘服务才能真正又小又稳。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。