一、中间件到底是什么
中间件(middleware)是 Node Web 框架的核心机制:每个请求按顺序经过一系列函数,每个函数可以处理请求、决定是否放行给下一个,最后响应返回。
1 2 3 4 5
| app.use((req, res, next) => { console.log(`${req.method} ${req.url}`) next() })
|
二、Express:线性中间件
Express 中间件按注册顺序线性执行,next() 决定是否进入下一个:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| const express = require('express') const app = express()
app.use((req, res, next) => { console.log('中间件 1:日志') next() })
app.use('/api', (req, res, next) => { const token = req.headers.authorization if (!token) { return res.status(401).json({ message: '未登录' }) } next() })
app.get('/api/user', (req, res) => { res.json({ name: '张三' }) })
app.use((req, res) => { res.status(404).json({ message: '接口不存在' }) })
app.use((err, req, res, next) => { console.error(err) res.status(500).json({ message: '服务器错误' }) })
|
next(err) 的语义:传给 next 任何参数(除了字符串 ‘route’),Express 会跳过普通中间件,直接跳到错误处理中间件。
三、Koa:洋葱模型
Koa 的中间件可以 await,执行流程是”进进出出“的洋葱:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const Koa = require('koa') const app = new Koa()
app.use(async (ctx, next) => { console.log('1. 进入 A') await next() console.log('4. 离开 A') })
app.use(async (ctx, next) => { console.log('2. 进入 B') await next() console.log('3. 离开 B') })
|
为什么这样设计:await next() 之后的代码在”响应回来之后”执行,非常适合:
经典案例——响应耗时统计:
1 2 3 4 5 6
| app.use(async (ctx, next) => { const start = Date.now() await next() const ms = Date.now() - start ctx.set('X-Response-Time', `${ms}ms`) })
|
四、对比与选型
| 对比 |
Express |
Koa |
| 中间件模型 |
线性(next 只前进) |
洋葱(可出可入) |
| 异步支持 |
回调为主(新版支持 Promise) |
原生 async/await |
| 内置能力 |
路由、静态资源齐全 |
极简,路由要装 koa-router |
| 生态 |
最丰富 |
较丰富 |
| 适用 |
快速上手、老项目 |
需要精细控制、追求现代写法 |
日常选型:快速开发用 Express;对请求生命周期控制要求高、想要干净异步代码用 Koa。
五、手写一个迷你中间件框架
理解了原理,自己实现一个也很简单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| class MiniApp { constructor() { this.middlewares = [] }
use(fn) { this.middlewares.push(fn) return this }
handle(req, res) { let index = 0
const next = (err) => { if (err) { const errorHandler = this.middlewares.find((fn) => fn.length === 4) return errorHandler ? errorHandler(err, req, res, () => {}) : res.end('500') }
const fn = this.middlewares[index++] if (!fn) return
if (fn.length === 4) return next()
fn(req, res, next) }
next() } }
const app = new MiniApp()
app.use((req, res, next) => { console.log('日志:', req.url) next() })
app.use((req, res, next) => { if (req.url === '/private' && !req.token) { res.statusCode = 401 return res.end('Unauthorized') } next() })
app.use((req, res) => { res.end('Hello ' + req.url) })
app.handle({ url: '/private', token: '' }, { statusCode: 200, end: (s) => console.log('响应:', s) })
|
fn.length 是函数参数个数,Express 就靠这个区分错误处理中间件(4 个参数)和普通中间件——这也是为什么错误处理中间件必须写满 4 个参数。
常见坑
- 忘记
next():请求会卡住直到超时,中间件必须在合适时机放行或终结;
- 异步错误没交给框架:Express 4 里 async 中间件抛错要自己
next(err),或装 express-async-errors;
- 中间件顺序错误:
express.json()(解析 body)必须在用 req.body 的路由之前注册;
- 错误中间件放在最前面:它要放在所有可能抛错的路由之后,才能兜住错误。
总结
中间件的本质是”请求的管道 + 责任的传递“:每个中间件做一件事,next() 决定放行,错误统一收口。理解了 Express 的线性模型和 Koa 的洋葱模型,看任何 Node 框架的源码都能快速抓住主线。