Skip to content

中间件

题目描述

实现一个“简化版” Express 风格中间件系统,支持:

  • 链式串行执行中间件 (req, next)
  • 错误处理中间件 (err, req, next)
  • 支持同步 / Promise / async 函数形式(通过手动调用 next 驱动)
  • 通过 next(error?) 进行错误切换

当前实现是一个最小可工作的责任链,与真实 Express / Koa 仍有差异:

  • 不支持真正的“洋葱模型”(next() 之后的代码不会等待后续异步完成)
  • 没有 res / 响应对象概念,所有状态附加在 req
  • 不支持返回值控制、也没有全局最终回调 / 完成通知
  • 不支持 next('route')、不支持路由匹配、挂载路径、正则、参数注入等

核心知识点

1. 责任链 + 函数参数长度分派

  • 普通中间件:function(req, next)length === 2
  • 错误中间件:function(err, req, next)length === 3
  • use() 内根据 func.length 插入不同数组:cbHandlers / errHandlers
  • 没有其它类型判定(多余参数、默认参数会影响 length 需谨慎)

2. 执行驱动机制(start(req)

内部维护两个独立索引:

变量作用
idx当前正常中间件序号
errIdx当前错误中间件序号

调度函数 next(nextErr)

javascript
if (nextErr) {
  func = errHandlers[errIdx++]        // 进入错误处理链
  args = [nextErr, req, next]
} else {
  func = cbHandlers[idx++]            // 进入正常链
  args = [req, next]
}

当某个错误中间件调用 next()(无错误)后,会继续回到正常链的“下一个”位置(不会回退 / 重试)。

3. 异步支持的边界

  • 通过 Promise.resolve(func(...args)).catch(error => next(error)) 包裹执行
  • 但:start() / next() 不返回 Promise,因此:
    • 无法 await app.start(req) 得知执行完成时间
    • 中间件里写:
      js
      someAsync().then(() => next())
      console.log('after')
      after 会立即执行,不具备“洋葱式 after 钩子”效果
  • 想要“前后包裹(before/after)”语义需对框架进行 Promise 链或栈式封装改造

4. 错误传播语义

场景结果
正常中间件抛同步异常被 try/catch 捕获 → next(error) → 进入错误链
正常中间件返回 rejected Promise.catch 捕获 → next(error)
错误中间件抛异常 / reject再次进入下一个错误中间件(递进 errIdx)
错误中间件调用 next()(无参数)回到正常链剩余部分
错误中间件不调用 next链终止(静默结束)

没有“最终未处理错误回调”;若所有错误中间件都调用 next(err) 且耗尽,将静默结束(无 throw)。

5. 中止与防御性

  • 访问越界:funcundefined → 判空后不再递归 → 链结束
  • 重复调用 next():索引继续递增,可能“跳过”后续或造成逻辑混乱(无防重入保护)
  • next() 并不防止多次错误传播(可能层层进入 errHandlers 直到耗尽)

6. 典型用法模式

javascript
const app = new Middleware()

app.use((req, next) => {
  req.log = ['start']
  next()
})

app.use(async (req, next) => {
  await new Promise(r => setTimeout(r, 10))
  req.async = true
  next()
})

app.use((err, req, next) => { // 错误中间件
  req.errorHandled = err.message
  next() // 继续后续正常链
})

app.use((req, next) => {
  req.done = true
  next()
})

app.start({ url: '/demo' })

7. 与 Express / Koa 的关键差异

能力当前实现ExpressKoa
after(洋葱模型)不支持通过 next() 回调顺序 + 同步栈局部支持await next() 天然支持
res/上下文reqreq/res 双对象ctx 统一封装
路径匹配不支持支持通过中间件可实现
返回 Promise否(部分中间件可自行封装)是(async 语义)
多重 next 防护很少(依赖约定)部分生态插件处理
未处理错误监控静默结束可挂接 error 事件默认抛出,可捕获

代码实现

javascript
export default class Middleware {
  cbHandlers = []
  errHandlers = []

  use(func) {
    if (func.length === 2)
      this.cbHandlers.push(func)
    if (func.length === 3)
      this.errHandlers.push(func)
  }

  start(req) {
    let idx = 0
    let errIdx = 0
    const that = this

    function next(nextErr) {
      const args = [req, next]
      let func = null

      if (nextErr) {
        func = that.errHandlers[errIdx++]
        args.unshift(nextErr)
      }
      else {
        func = that.cbHandlers[idx++]
      }
      try {
        func && Promise.resolve(func(...args)).catch(error => next(error))
      }
      catch (error) {
        next(error)
      }
    }

    next()
  }
}

使用示例

javascript
import Middleware from './middleware'

const app = new Middleware()

// 普通中间件
app.use((req, next) => {
  req.timeline = ['init']
  next()
})

// 异步中间件
app.use(async (req, next) => {
  await new Promise(r => setTimeout(r, 20))
  req.timeline.push('async-done')
  next()
})

// 触发错误
app.use((req, next) => {
  if (!req.ok) {
    return next(new Error('Not OK'))
  }
  next()
})

// 错误处理中间件
app.use((err, req, next) => {
  req.error = err.message
  // 继续执行后续正常中间件
  next()
})

// 收尾中间件
app.use((req, next) => {
  req.timeline.push('end')
  console.log(req)
  next()
})

app.start({ url: '/demo', ok: false })

内容基于 MIT 许可 | 保持节奏 · 持续积累