一、中间件到底是什么

中间件(middleware)是 Node Web 框架的核心机制:每个请求按顺序经过一系列函数,每个函数可以处理请求、决定是否放行给下一个,最后响应返回。

1
2
3
4
5
// Express 里最经典的中间件
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()

// 1. 日志中间件
app.use((req, res, next) => {
console.log('中间件 1:日志')
next()
})

// 2. 鉴权中间件
app.use('/api', (req, res, next) => {
const token = req.headers.authorization
if (!token) {
return res.status(401).json({ message: '未登录' }) // 拦截,不 next
}
next()
})

// 3. 路由
app.get('/api/user', (req, res) => {
res.json({ name: '张三' })
})

// 4. 404 兜底
app.use((req, res) => {
res.status(404).json({ message: '接口不存在' })
})

// 5. 错误处理中间件(4 个参数,必须带 err)
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')
})

// 执行顺序:1 → 2 → 3 → 4

为什么这样设计: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
// mini-framework.js
class MiniApp {
constructor() {
this.middlewares = []
}

use(fn) {
this.middlewares.push(fn)
return this
}

// 模拟处理一个请求
handle(req, res) {
let index = 0

// next 递归推进中间件
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

// 4 个参数的是错误处理中间件,跳过
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,直接结束
}
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 个参数。

常见坑

  1. 忘记 next():请求会卡住直到超时,中间件必须在合适时机放行或终结;
  2. 异步错误没交给框架:Express 4 里 async 中间件抛错要自己 next(err),或装 express-async-errors;
  3. 中间件顺序错误:express.json()(解析 body)必须在用 req.body 的路由之前注册;
  4. 错误中间件放在最前面:它要放在所有可能抛错的路由之后,才能兜住错误。

总结

中间件的本质是”请求的管道 + 责任的传递“:每个中间件做一件事,next() 决定放行,错误统一收口。理解了 Express 的线性模型和 Koa 的洋葱模型,看任何 Node 框架的源码都能快速抓住主线。