1. 中间件的基本形式
Hono 的中间件长这样:
01import { Hono } from 'hono'0203const app = new Hono()0405app.use('*', async (c, next) => {06// 请求到达路由之前,做点事情07console.log('请求进来了')0809await next() // 把请求交给下一个中间件或路由处理1011// 路由处理完了,响应出去之前,做点事情12console.log('响应出去了')13})1415app.get('/', (c) => {16return c.text('Hello!')17})1819export default app
核心就是 await next()。它把中间件分成两半:next() 之前处理请求,next() 之后处理下游中间件或路由返回后的阶段。
你也可以把它理解成一个分叉点:
- 调用
await next():请求继续往后走,交给下一个中间件或最终路由 - 直接
return c.json(...)/return c.text(...):当前中间件就把请求结束掉,后面的逻辑不再执行
鉴权、权限判断、参数拦截,本质上都是在这里决定"放行"还是"拦截"。
2. 洋葱模型
如果你用过 Koa,这个概念不陌生。多个中间件的执行顺序像剥洋葱:
01app.use('*', async (c, next) => {02console.log('中间件 A - 进')03await next()04console.log('中间件 A - 出')05})0607app.use('*', async (c, next) => {08console.log('中间件 B - 进')09await next()10console.log('中间件 B - 出')11})1213app.get('/', (c) => {14console.log('路由处理')15return c.text('Hello!')16})
控制台输出:
1中间件 A - 进2中间件 B - 进3路由处理4中间件 B - 出5中间件 A - 出
请求从外到内穿过中间件,响应从内到外返回。先注册的中间件最先接触请求、最后接触返回阶段。
3. 自定义中间件:请求计时
一个实用的中间件——记录每个请求花了多长时间:
01import { Hono } from 'hono'0203const app = new Hono()0405// 请求计时中间件06app.use('*', async (c, next) => {07const start = Date.now()08await next()09const duration = Date.now() - start10console.log(`${c.req.method} ${c.req.path} - ${duration}ms`)11})1213app.get('/', (c) => {14return c.text('Hello!')15})1617export default app
await next() 前记录开始时间,await next() 后计算耗时。洋葱模型的经典用法。
4. 自定义中间件:简单鉴权
检查请求头里有没有合法的 API Key:
01import { Hono } from 'hono'0203const app = new Hono()0405// 鉴权中间件06// 这里为了先讲清概念,先省略 Hono 的类型声明07const authMiddleware = async (c, next) => {08const apiKey = c.req.header('X-API-Key')0910if (apiKey !== 'my-secret-key') {11return c.json({ error: 'Unauthorized' }, 401)12}1314await next()15}1617// 公开接口,不需要鉴权18app.get('/', (c) => {19return c.text('Public')20})2122// 需要鉴权的接口23app.get('/api/secret', authMiddleware, (c) => {24return c.json({ data: 'This is secret' })25})2627export default app
注意:如果鉴权失败,直接 return c.json(...) 就行,不调用 next(),请求就不会继续往下走。
如果你在 TypeScript 严格模式里写项目,后面最好把这类中间件补上类型,或者直接使用 Hono 提供的 createMiddleware() 来创建中间件。教程这里先把注意力放在执行流程本身。
5. 内置中间件
Hono 自带了一批开箱即用的中间件,不用自己造轮子:
01import { Hono } from 'hono'02import { cors } from 'hono/cors'03import { logger } from 'hono/logger'04import { timing } from 'hono/timing'05import { prettyJSON } from 'hono/pretty-json'06import { secureHeaders } from 'hono/secure-headers'0708const app = new Hono()0910// 请求日志:控制台打印每个请求的方法、路径、状态码、耗时11app.use('*', logger())1213// 跨域配置:允许前端跨域访问14app.use('*', cors({15origin: 'http://localhost:3000',16allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],17}))1819// Server-Timing 头:响应头里带上各阶段耗时,浏览器 DevTools 可以看到20app.use('*', timing())2122// 美化 JSON:请求 URL 加 ?pretty 参数时,返回格式化的 JSON23app.use('*', prettyJSON())2425// 安全响应头:自动设置 X-Frame-Options、X-Content-Type-Options 等26app.use('*', secureHeaders())2728app.get('/api/users', (c) => {29return c.json([30{ id: 1, name: 'Alice' },31{ id: 2, name: 'Bob' },32])33})3435export default app
每个中间件干的事情:
logger():控制台打印<-- GET /api/users和--> GET /api/users 200 12mscors():处理跨域预检请求(OPTIONS),设置Access-Control-Allow-*响应头timing():在响应头加Server-Timing字段,方便性能分析prettyJSON():访问/api/users?pretty时返回缩进后的 JSONsecureHeaders():一键设置安全相关的 HTTP 头,防 XSS、点击劫持等
6. 中间件挂载范围
中间件可以作用在不同范围:
01import { Hono } from 'hono'02import { logger } from 'hono/logger'03import { cors } from 'hono/cors'0405const app = new Hono()0607// 全局中间件:所有路由都会经过08app.use('*', logger())0910// 路径级中间件:只有 /api 开头的路由会经过11app.use('/api/*', cors())1213// 鉴权中间件,只用在 /api 下14const auth = async (c, next) => {15const token = c.req.header('Authorization')16if (!token) {17return c.json({ error: 'Unauthorized' }, 401)18}19await next()20}21app.use('/api/*', auth)2223// 单个路由级别:把中间件直接写在路由参数里24const adminOnly = async (c, next) => {25const role = c.req.header('X-Role')26if (role !== 'admin') {27return c.json({ error: 'Forbidden' }, 403)28}29await next()30}3132app.delete('/api/users/:id', adminOnly, (c) => {33const id = c.req.param('id')34return c.json({ message: `User ${id} deleted` })35})3637// 公开路由,不受 /api/* 的中间件影响38app.get('/', (c) => {39return c.text('Public homepage')40})4142export default app
三种粒度:
app.use('*', ...):全局,所有请求都过app.use('/api/*', ...):路径前缀匹配,只有/api/下的请求会过app.get('/path', middleware, handler):只对这一个路由生效
7. 执行顺序很重要
中间件按注册顺序执行。顺序搞错了会出问题:
01import { Hono } from 'hono'02import { cors } from 'hono/cors'03import { logger } from 'hono/logger'0405const app = new Hono()0607// 正确顺序:cors 在鉴权之前08// 因为浏览器跨域会先发 OPTIONS 预检请求09// 如果鉴权在 cors 之前,预检请求没带 token,直接被 401 了10app.use('*', logger())11app.use('*', cors())12app.use('/api/*', async (c, next) => {13const token = c.req.header('Authorization')14if (!token) {15return c.json({ error: 'Unauthorized' }, 401)16}17await next()18})1920app.get('/api/data', (c) => {21return c.json({ message: 'Protected data' })22})2324export default app
一般推荐的顺序:logger → cors → secureHeaders → 鉴权 → 业务逻辑。日志最先,这样所有请求(包括被拦截的)都能被记录到。
8. 用 c.set / c.get 在中间件和路由之间传数据
中间件处理完的结果,怎么传给后面的路由?用 c.set() 和 c.get():
01import { Hono } from 'hono'0203const app = new Hono()0405// 鉴权中间件:解析 token,把用户信息存到 context 里06app.use('/api/*', async (c, next) => {07const token = c.req.header('Authorization')0809if (!token) {10return c.json({ error: 'Unauthorized' }, 401)11}1213// 假设解析 token 得到了用户信息14const user = { id: 1, name: 'Alice', role: 'admin' }1516// 把用户信息存到 context 里17c.set('user', user)1819await next()20})2122// 路由里通过 c.get() 拿到用户信息23app.get('/api/profile', (c) => {24const user = c.get('user')25return c.json({ user })26})2728app.get('/api/admin', (c) => {29const user = c.get('user')3031if (user.role !== 'admin') {32return c.json({ error: 'Forbidden' }, 403)33}3435return c.json({ message: 'Welcome, admin' })36})3738export default app
c.set() / c.get() 是中间件与路由之间的通信通道。数据只在当前请求的生命周期内有效,不同请求之间互不影响。
和前一篇一样,这里为了先理解机制,先省略了类型声明。真实项目里,像 user 这种在中间件里写入、在路由里读取的数据,最好补上类型,不然编辑器很难准确提示字段。
9. 总结
中间件是 Hono 处理横切关注点的核心机制。洋葱模型让你可以在请求前后都插入逻辑,c.set() / c.get() 解决了中间件和路由之间的数据传递。内置中间件覆盖了大部分常见需求,自定义中间件也就是一个 async 函数的事。
下一篇看数据校验——怎么优雅地验证请求参数和请求体。