全局中间件
要点
- 全局中间件通过
app.use('*', ...)注册,所有请求都会经过 - 日志、安全头、CORS、请求 ID 这类横切关注点通常放在全局
- 全局中间件的注册顺序就是执行顺序,先注册的先接触请求、最后接触响应
- 全局中间件里尽量避免做重计算,否则会拖慢每一条路由
- 有些中间件看似适合全局,实际按路径挂载更合适,例如鉴权和限流
- 全局中间件与
app.route()组合使用时,子应用会继承父应用的全局栈
1. 注册方式
全局中间件的标志性写法是路径参数为 '*':
// src/index.ts
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { secureHeaders } from 'hono/secure-headers'
const app = new Hono()
// 三条全局中间件
app.use('*', logger())
app.use('*', cors())
app.use('*', secureHeaders())
app.get('/', (c) => c.text('Hello'))
app.get('/api/users', (c) => c.json([{ id: 1, name: 'Alice' }]))
export default app'*' 匹配任意路径。无论请求的是 /、/api/users 还是 /docs/intro,都会依次走过 logger、cors、secureHeaders。
如果换成具名路径或前缀,例如 app.use('/api/*', cors()),那就不是全局中间件,而是路径级中间件,下一节会展开。
2. 适合放在全局的中间件类型
经验上,适合全局挂载的中间件具备两个特征:
- 几乎每条路由都需要它
- 放在单个路由里会带来重复代码
常见的候选:
- logger():请求日志。运维排障时希望所有请求都有记录,包括被 404 或鉴权拦截的
- secureHeaders():安全响应头。浏览器访问任何页面都需要 CSP、X-Frame-Options 等防护
- cors():跨域配置。前后端分离项目里,几乎所有 API 都要响应预检请求
- requestId():请求 ID。分布式追踪的入口,每条请求都应该有一个可追溯的 ID
- timing():Server-Timing 头。浏览器性能面板会展示后端各阶段耗时
这几类中间件的共同点是「不做业务判断,只补充公共能力」。它们不会根据路径或用户角色改变行为,挂在全局是自然的选择。
3. 注册顺序与执行顺序
全局中间件按注册顺序执行。app.use('*', A) 在 app.use('*', B) 之前注册,A 就先于 B 接触请求。
结合洋葱模型,实际执行顺序是:
请求 → A 前段 → B 前段 → C 前段 → 路由处理 → C 后段 → B 后段 → A 后段 → 响应
一个典型的推荐顺序:
app.use('*', logger()) // 1. 日志最先,确保被拦截的请求也有记录
app.use('*', requestId()) // 2. 请求 ID 紧随其后,后续中间件可以读取
app.use('*', cors()) // 3. CORS 在鉴权之前,避免 OPTIONS 预检被拦截
app.use('*', secureHeaders()) // 4. 安全头与业务无关,放在这里即可
app.use('*', timing()) // 5. timing 包住整个链路,统计的耗时更完整顺序带来的差异在 CORS + 鉴权的组合里最明显。如果鉴权中间件在 CORS 之前,浏览器的 OPTIONS 预检请求不带 Authorization 头,会被直接 401。浏览器拿不到 CORS 头,跨域就失败了。这种问题调试起来往往要抓包才能看清,预防的方式就是把 CORS 放在鉴权之前。
4. 全局中间件的性能考量
全局中间件的代码会在每一条请求上运行。一个写得不够克制的实现,容易把整站拖慢。
两个常见陷阱:
4.1 在中间件里做同步 IO
// ❌ 每次请求都同步读文件
app.use('*', async (c, next) => {
const config = fs.readFileSync('/etc/app/config.json', 'utf-8')
c.set('config', JSON.parse(config))
await next()
})readFileSync 会阻塞 Node.js 的事件循环。改成在启动时加载一次,通过闭包引用:
// ✅ 启动时加载一次
const config = JSON.parse(fs.readFileSync('/etc/app/config.json', 'utf-8'))
app.use('*', async (c, next) => {
c.set('config', config)
await next()
})4.2 在中间件里重复分配大对象
// ❌ 每次请求都构造一份新的元数据
app.use('*', async (c, next) => {
const metadata = {
routes: Object.keys(app.routes),
middlewares: [...],
// ...
}
c.set('metadata', metadata)
await next()
})如果元数据在运行时不会变化,把它提到模块顶层,中间件里只做引用传递。
全局中间件的开销是乘法关系:单次耗时 × 请求量。单次多花 1ms,QPS 1000 的服务每秒就多消耗 1 秒的 CPU 时间。
5. 全局中间件与子应用
Hono 的 app.route() 可以把一个子应用挂载到主应用上。子应用内部的全局中间件只对子应用内的路由生效,主应用的全局中间件则对所有路由生效:
// src/apps/admin.ts
import { Hono } from 'hono'
import { logger } from 'hono/logger'
const admin = new Hono()
// 子应用的全局中间件,只作用于 /admin/*
admin.use('*', logger())
admin.get('/dashboard', (c) => c.text('Admin Dashboard'))
export default admin// src/index.ts
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import admin from './apps/admin'
const app = new Hono()
// 主应用的全局中间件,作用于所有路由
app.use('*', cors())
app.route('/admin', admin)
app.get('/', (c) => c.text('Home'))
export default app访问 /admin/dashboard 时,请求的处理链路是:
- 主应用的
cors()前段 - 子应用的
logger()前段 /admin/dashboard路由处理- 子应用的
logger()后段 - 主应用的
cors()后段
这种分层挂载的机制,让「公共能力全局化、业务特性局部化」成为可能。主应用负责跨域、日志、安全头等横切关注点,子应用各自决定是否需要额外的鉴权、限流等中间件。
6. 不适合全局挂载的中间件
并非所有中间件都适合挂在全局。判断标准是「是否每条路由都需要它」。
几类通常按路径挂载的中间件:
- 鉴权中间件:公开路由(首页、健康检查、登录)不需要鉴权,全局挂载会让这些路由也得带上 token,或者需要在中间件里写排除列表
- 限流中间件:不同路由的限流策略往往不同,读接口和写接口的阈值差别可以到 10 倍以上
- 请求体解析中间件:GET 请求没有 body,解析是浪费;文件上传路由需要的解析方式与 JSON 接口不同
- 角色校验中间件:
/admin/*和/user/*的权限模型不一样,放在一起会让中间件内部出现分支
这些中间件更适合在子应用或具体路由上挂载,避免全局中间件里出现过多的路径判断。
7. 一个全局中间件的完整示例
把这一节的要点组合起来,一个典型的全局中间件栈如下:
// src/index.ts
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { secureHeaders } from 'hono/secure-headers'
import { timing } from 'hono/timing'
import { requestId } from 'hono/request-id'
const app = new Hono()
// 全局中间件栈:按顺序注册
app.use('*', logger())
app.use('*', requestId())
app.use('*', cors({
origin: ['https://example.com', 'https://app.example.com'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization', 'X-Request-Id'],
exposeHeaders: ['X-Request-Id'],
maxAge: 86400,
}))
app.use('*', secureHeaders())
app.use('*', timing())
// 公开路由
app.get('/', (c) => c.text('Hello'))
app.get('/health', (c) => c.json({ status: 'ok' }))
// API 路由(后续会加鉴权中间件)
app.get('/api/users', (c) => c.json([]))
export default app这段代码里有几个值得注意的细节:
logger()放在最前,保证所有请求(包括被 CORS 或鉴权拦截的)都有日志requestId()紧随其后,后续中间件和路由可以通过c.get('requestId')读取cors()配置了exposeHeaders: ['X-Request-Id'],让前端 JS 也能读到这个响应头timing()放在最后注册(也就是最靠近路由),统计的耗时覆盖整个中间件栈
总结
全局中间件是 Hono 处理横切关注点的主要载体。通过 app.use('*', ...) 注册后,所有请求都会依次穿过这些中间件。
这一节涉及到的几个判断:
- 适合全局挂载的中间件:logger、secureHeaders、cors、requestId、timing 这类与路径无关、每条路由都需要的公共能力
- 注册顺序决定执行顺序:一般按
logger → requestId → cors → secureHeaders → timing排列,CORS 必须在鉴权之前 - 性能是乘法关系:全局中间件的单次耗时会被请求量放大,避免在内部做同步 IO 或重复分配
- 与子应用组合:主应用的全局中间件作用于所有路由,子应用的全局中间件只作用于自己的路由
- 不适合全局的中间件:鉴权、限流、角色校验、请求体解析通常按路径或路由挂载,避免中间件内部出现分支
下一篇看路由级中间件——当中间件只需要作用于 /api/* 或某一条具体路由时,该怎么组织。