express中间件原理以及大致实现

前言

最近在学习 express 时,使用中间件,感觉函数调用执行顺序和结果非常神奇,于是基于总结出的调用规律尝试实现一下,顺便加深一下印象,话不多说开始进入正题。

expree中间件基本使用

首先中间件运行逻辑按照函数定义顺序 以及运行情况来执行的。

正常运行

js 复制代码
import express from 'express'
const app = express()

// 全局中间件 --> use1
app.use((req, res, next) => {
  console.log('use1---全局中间件1')
  next()
})

// 局部中间件 --> news1
app.get('/news', 
  (req, res, next) => {
    console.log('news1---局部中间件1')
    next() 
  },
  (req, res, next) => {
    console.log('news1---局部中间件2')
    next()
  },
  (err, req, res, next) => {
    console.log('news1---局部错误中间件1')
    next()
  }
)

// 全局中间件 --> use2
app.use(
// 全局错误中间件
(err, req, res, next) => {
  console.log('use2---全局错误中间件1')
  next()
}, 
// 全局中间件
(req, res, next) => {
  console.log('use2---全局中间件1')
  next()
}
)

// 局部中间件 --> news2
app.get('/news',
  (req, res, next) => {
    console.log('new2---局部中间件1')
    next()
  },
  (req, res, next) => {
    console.log('new2---局部中间件2')
    next()
  }
)

app.listen(9090, () => {
  console.log('localhost:9090')
})

上述代码运行结果为:

  • use1---全局中间件1
  • news1---局部中间件1
  • news1---局部中间件2
  • use2---全局中间件1
  • new2---局部中间件1
  • new2---局部中间件2

由此可以推断出,正常运行的情况下会忽略定义的错误中间件

运行出错

局部错误捕获

基于上述 正常运行 代码修改 局部中间件 --> news1 部分

js 复制代码
// 局部中间件 --> news1
app.get('/news', 
  (req, res, next) => {
    console.log('news1---局部中间件1')
    // next(1) 
    throw 1 // 抛出错误 或 next(1)
  },
  (req, res, next) => {
    console.log('news1---局部中间件2')
    next()
  },
  (err, req, res, next) => {
    console.log('news1---局部错误中间件1')
    next()
  }
)

上述代码运行结果为:

  • use1---全局中间件1
  • news1---局部中间件1
  • news1---局部错误中间件1
  • use2---全局中间件1
  • new2---局部中间件1
  • new2---局部中间件2

由此可以推断出,错误运行情况下:

  1. 代码依次正常运行,发生错误后,忽略后续正常的中间件,直到遇到处理错误的中间件
  2. 错误中间件处理后,再次正常运行。

既然说到了错误处理中间件,那错误处理中间件分为全局和局部,它们两个有区别吗?

局部错误捕获 - (例外情况)

js 复制代码
import express from 'express'
const app = express()

// 全局中间件 --> use1
app.use((req, res, next) => {
  console.log('use1---全局中间件1')
  next()
})

// 局部中间件 --> news1
app.get('/news', 
  (req, res, next) => {
    console.log('news1---局部中间件1')
    next(1) // 抛出错误 或 next(1)
  }
)

// 局部中间件 --> news2
app.get('/news',
  (err, req, res, next) => {
    console.log('new2---局部错误中间件1')
    next()
  },
  (req, res, next) => {
    console.log('new2---局部中间件2')
    next()
  }
)

app.listen(9090, () => {
  console.log('localhost:9090')
})

上述代码运行结果为:

  • use1---全局中间件1
  • news1---局部中间件1
  • 1

有没有发现莫名其妙多出来一个1,这正是我们抛出next(1)传递的参数,(throw 同理),同时也表明我们的错误并没有被错误中间件捕获到。

由此可以推断出,错误运行情况下:

  1. 发送错误的中间件后续会寻找 (相同域或全局)的错误中间件
  2. 相同域:在同一个get或其他(除use外)函数调用内传递的参数。

全局错误捕获

js 复制代码
import express from 'express'
const app = express()

// 全局中间件 --> use1
app.use((req, res, next) => {
  console.log('use1---全局中间件1')
  next()
})

// 局部中间件 --> news1
app.get('/news', 
  (req, res, next) => {
    console.log('news1---局部中间件1')
    next(1)
  }
)

// 全局中间件 --> use2
app.use(
// 全局错误中间件
(err, req, res, next) => {
  console.log('use2---全局错误中间件1')
  next()
}, 
// 全局中间件
(req, res, next) => {
  console.log('use2---全局中间件1')
  next()
}
)


// 局部中间件 --> news2
app.get('/news',
  (err, req, res, next) => {
    console.log('new2---局部错误中间件1')
    next()
  },
  (req, res, next) => {
    console.log('new2---局部中间件2')
    next()
  }
)

app.listen(9090, () => {
  console.log('localhost:9090')
})

上述代码运行结果为:

  • use1---全局中间件1
  • news1---局部中间件1
  • use2---全局错误中间件1
  • use2---全局中间件1
  • new2---局部中间件2

这个就很简单了,全局错误捕获不会区分是否相同域,(可以理解为全局域)。

初始化运行

js 复制代码
import express from 'express'
const app = express()

// 全局中间件 --> use1
app.use((err, req, res, next) => {
  console.log('use1---全局错误中间件1')
  next()
})

// 局部中间件 --> news1
app.get('/news', 
  (err, req, res, next) => {
    console.log('news1---局部错误中间件1')
    next(1)  // 抛出错误 或 next(1)
  },
  (req, res, next) => {
    console.log('news1---局部中间件1')
    next()
  }
)

// 全局中间件 --> use2
app.use(
(req, res, next) => {
  console.log('use2---全局中间件1')
  next()
}
)

// 局部中间件 --> news2
app.get('/news',
  (err, req, res, next) => {
    console.log('new2---局部错误中间件1')
    next()
  },
  (req, res, next) => {
    console.log('new2---局部中间件2')
    next()
  }
)

app.listen(9090, () => {
  console.log('localhost:9090')
})

上述代码运行结果为:

  • news1---局部中间件1
  • use2---全局中间件1
  • new2---局部中间件2

由此可以推断出:初始化运行时会忽略之前定义的错误中间,会寻找第一个正常的中间件运行

其他

对于一些其他边界情况就不一一示例赘述了,大致说明一下。

  • 中间件函数参数 next决定下一个中间件的调用运行。
  • 函数内next多次调用无效,只会运行第一次。
  • 中间件函数运行出错:
    • throw xx,抛出错误 。
    • next(参数),传递参数。
    • 中间件函数包装为promise,函数包装为promise。
js 复制代码
app.use(async (req, res, next) => {
  await Promise.reject()
})

规律总结

  1. 中间件函数运行时,首先寻找第一个正常中间件函数作为入口。
  2. 函数中需要手动的交给后续中间件处理。
  3. 中间件函数运行过程中发生错误,会寻找后续符合条件的中间件函数运行。
  4. 如果后续没有符合条件的中间函数,则停止运行,并打印出这个错误。

(第4点不太重要)

符合条件:全局或相同域注册的错误中间件函数。

基于上述规律,我们就着手开始尝试实现。

express中间件实现

前言

着重于实现上述效果,与真正的express实现逻辑肯定大相径庭,不喜勿喷。

思考

  1. 函数入口怎么寻找?,怎么区分函数是错误中间件还是正常的中间件?
  • 函数入口:是由正常的中间件开始,那么我们只需要知道第一个正常中间件就行了。
  • 怎么区分:通过函数.length 属性来区分。
  1. 中间件函数忽略过程怎么实现?
  • 换个角度看问题,运行中间件函数本质就是依次运行,而中间件需要忽略函数,本质就是不调用使用者定义的函数,转而内部直接调用next

示例代码

js 复制代码
const run1 = (next) => {
  console.log('run1')
  next()
}
const run2 = (next) => {
  console.log('run2')
  next()
}
const run3 = (next) => {
  console.log('run3')
  next()
}
const list = [
  run1,
  run2,
  run3
]

function run() {
  let i = 0
  function _run() {
    if (i >= list.length) return
    console.log('被调用了')
    const fn = list[i++]
    // 符合条件,交由用户 控制下一次next调用
    if (Math.random() > .5) {
      fn(_run)
    }
    // 不符合条件,内部直接调用, 不经由用户控制
    else {
      _run()
    }
  }
  
  return _run()
}

run()
  1. 中间件函数运行发生错误后续?
  • 错误发生分为3种next(错误)、throw 错误, promise错误
  • 这里我想到了可以定义3种状态,stop:停止,running:运行中,error:发生错误。
  • try catch 包裹中间件运行函数,发生错误手动调用next(错误),并修改运行状态。
  • promise错误 可以利用 Promise.resolve(next()).catch(error => {}) 捕获,然后再次通过next传递。

示例代码

js 复制代码
const run1 = (next) => {
  console.log('run1')
  next()
}
const run2 = (next) => {
  console.log('run2')
  next(1) // 抛出错误 或者 next(参数)
  // throw 1 抛出错误 或者 next(参数)
}
const run3 = (next) => {
  console.log('run3')
  next()
}
const list = [
  run1,
  run2,
  run3
]

// 0: 停止 1: 运行 -1:出错
let flag = 1

function run() {
  let i = 0
  function _run(err) {
    if (i >= list.length) return

    // 设置当前运行状态为错误, 后续运行根据错误状态来判断,是否 由用户控制 还是 直接内部next()
    if (err) {
      flag = -1
    }

    console.log('被调用了')
    const fn = list[i++]

    try {
      // 符合条件,交由用户 控制下一次next调用
      if (Math.random() > .5) {
        
        // promise 错误捕获
        Promise.resolve(fn(_run)).catch((error) => {
          next(error)
        })
      }
      // 不符合条件,内部直接调用, 不经由用户控制
      else {
        _run()
      }
    }
    // 发送错误手动调用 next 并传递错误消息 
    catch (error) {
      next(error)
    }
  }
  
  return _run()
}

run()
  1. 函数多次调用怎么阻止?
  • 两种实现。
  • 每个函数内部可以使用isInvoke,当运行函数时通过isInvoke判断是否被调用过。
  • 实现2:koa-componse实现。

示例代码

js 复制代码
// 实现1
function fun(fn) {
  let isInvoke = false
  return () => {
    if (isInvoke) return
    isInvoke = true
    return fn()
  }
}
fun(() => {
  console.log('运行')
})

// --------------------------------------------------------

// 实现2
const run1 = (next) => {
  console.log('run1')
  next()
  next()
}
const run2 = (next) => {
  console.log('run2')
  next(1) // 抛出错误 或者 next(参数)
  // throw 1 抛出错误 或者 next(参数)
  next(1)
}
const run3 = (next) => {
  console.log('run3')
  next()
}
const list = [
  run1,
  run2,
  run3
]

// 0: 停止 1: 运行 -1:出错
let flag = 1

function run() {
  let i = 0
  function _run(index, err) {
    if (i >= list.length) return

    // 这里阻止了 多次调用
    if (index < i) return

    // 设置当前运行状态为错误, 后续运行根据错误状态来判断,是否 由用户控制 还是 直接内部next()
    if (err) {
      flag = -1
    }

    console.log('被调用了')
    const fn = list[i++]

    try {
      // 符合条件,交由用户 控制下一次next调用
      if (Math.random() > -1) {

        // promise 错误捕获
        Promise.resolve(fn(_run.bind(null, i))).catch((error) => {
          next(error)
        })
      }
      // 不符合条件,内部直接调用, 不经由用户控制
      else {
        _run.bind(null, i)
      }
    }
    // 发送错误手动调用 next 并传递错误消息 
    catch (error) {
      next(error)
    }
  }
  
  return _run(0)
}

run()

至此我们可以编写实现代码了。

具体实现

代码

js 复制代码
const isFuction = (v) => typeof v === 'function' 
const taskStatus = {
  STOP: 0, // 停止
  RUNNING: 1, // 运行中
  ERROR: -1  // 运行出错
}

class Task {
  #taskStatus = taskStatus['STOP'] // 任务状态
  #lastTask // 最后一个任务
  #initalRunTask // 第一个初始化运行函数
  #scope = 0 // 作用域
  #runTimeErrTask = null // 运行出错的任务
  request = {} // 请求对象
  response = {} // 响应对象
  static errTaskQueryLength = 4 // 函数参数判断
  static globalScope = 0 // 全局作用域

  add(...args) {
    this.#scope++
    this.#pushTasks(args, this.#scope)
  }

  use(...args) {
    this.#pushTasks(args, Task.globalScope)
  }

  run() {
    if (!this.#initalRunTask) return
    this.#taskStatus = taskStatus['RUNNING']
    this.#initalRunTask() 
  }

  #pushTasks(tasks, scope) {
    for (const t of tasks) {
      if (!isFuction(t)) continue
      this.#handleTask(t, scope)
    }
  }

  #handleTask(task, scope) {
    let isInvoke = false
    const t = (err) => {
      if (isInvoke) return
      isInvoke = true

      // 运行错误判断
      if (err !== void 0) {
        this.#taskStatus = taskStatus['ERROR']
        this.#runTimeErrTask = (t.prev.error = err, t.prev)
      }

      const next = t.next || (() => {
        this.#taskStatus = taskStatus['STOP']
        this.#runTimeErrTask && (console.log(this.#runTimeErrTask.error))
        this.#cleanRunTimeErrTask()
      })

      // 函数参数
      const query = [this.#runTimeErrTask?.error, this.request, this.response, next]
      !t.isErrTask && query.shift()

      /**
       * 判断当前队列状态
       * 1. 如果为 RUNNING, 说明后续需要运行正常中间件函数
       * 2. 如果为 ERROR, 说明后续需要运行符合条件的错误中间件函数
       */
      try {
        switch (this.#taskStatus) {
          case taskStatus['RUNNING']:
            t.isErrTask ? next() :  Promise.resolve(task.apply(null, query)).catch((error) => {  
              next(error || null)
            })
            break
          case taskStatus['ERROR']:

            if (t.isErrTask) {
              const errTaskCcope = this.#runTimeErrTask?.scope || Task.globalScope
              // 如果 错误处理 为 use注册 或 同一个add函数添加的函数
              if (!t.scope || t.scope === errTaskCcope) {
                this.#taskStatus = taskStatus['RUNNING']
                // 清空错误任务函数
                this.#cleanRunTimeErrTask()
                Promise.resolve(task.apply(null, query)).catch((error) => {
                  next(error || null)
                })
              }
              else {
                next()
              }
            } 
            else {
              next()
            }
            break
          default:
            break
        }
      } catch (error) {
        next(error)
      }
    }
    
    const isErrTask = this.isErrTask(task)
    t.scope = scope
    t.isErrTask = isErrTask
    t.error = null

    // 定义初始化运行函数
    if (!this.#initalRunTask && !isErrTask) {
      this.#initalRunTask = t
    }
    
    // 双向链表结构, next: 指向下一个t, prev: 指向上一个t 
    if (this.#lastTask) {
      this.#lastTask.next = t
      t.prev = this.#lastTask
    }

    this.#lastTask = t
    return t
  }

  // 判断是否为错误中间函数
  isErrTask(task) {
    return task.length === Task.errTaskQueryLength
  }

  // 清除运行出错的任务
  #cleanRunTimeErrTask() {
    if (this.#runTimeErrTask) {
      this.#runTimeErrTask.error = null
      this.#runTimeErrTask = null
    }
  }
}
  1. 实现思路与上述思考过程大差不差,不过我这里使用的双向链表结构,通过函数.next 访问下一个中间件,函数.prev 访问上一个中间件。

  2. 通过函数.scope区分相同域与全局域。

测试

js 复制代码
const task = new Task()

task.add(
  async (a, b, next) => {
    console.log('运行1')
    await Promise.reject()
    // next(1)
    // throw 1
    console.log('运行1结束')
  }, 
  (a, b, next) => {
    console.log('运行1-1')
    next()
  }
)

task.add((err, a, b, c) => {
  console.log('运行99')
  console.log()
})

task.use((a, b, next) => {
  console.log('运行2')
  next()
  console.log('运行2结束')
})

console.log(task)
task.run()

最后

掘金上刷到koa中间件实现,就还原了一下。

js 复制代码
class Compose {
  constructor(middleware) {
    if (!Array.isArray(middleware)) {
      throw new TypeError('middleware 必须是个数组')
    }
    for (const ware of middleware) {
      if (typeof ware !== 'function') throw new TypeError('middleware的每个组成部件必须是函数')
    }
    this.middleware = middleware
    return this.compose.bind(this)
  }

  compose(context, next) {
    let index = -1
    const excuteFn = (i) => {
      if (i <= index) return Promise.reject('next多次调用')
      index = i
      let fn = this.middleware[i]
      if (i >= this.middleware.length) {
        fn = next 
      }    
      if (typeof fn !== 'function') {
        return Promise.resolve()
      }
      // 同步错误处理
      try {        
        // 异步错误处理
        return Promise.resolve(fn.call(null, context, excuteFn.bind(null, ++i)))
      } catch (error) {
        return Promise.reject(error)
      }
    }
    return excuteFn(0)
  }
}

测试

js 复制代码
const middleware = [
  (a, next) => {
      return next().then((res) => {
        console.log('运行', res)
          return res + '1'
      })
  },
  (a, next) => {
      return next().then((res) => {
        console.log('运行2')
        
          return res + '2'
      })
  },
  (a, next) => {
      return next().then((res) => {
        console.log('运行3')
          return res + '3'
      })
  }
]

const compose = new Compose(middleware)
compose({a: 1}).then(res => {
  console.log('运行')
})

创作不易,如有疑问或者错误描述的地方,可评论或者私信,我会及时修改和回复,谢谢。

相关推荐
CoderWeen1 小时前
从零实现一个 Vue3 流程图编辑器:节点拖拽、贝塞尔连线与框选
前端·javascript
光影少年1 小时前
HashRouter 和 BrowserRouter 区别、底层原理、部署差异
前端·react.js·nestjs
柯克七七1 小时前
我把祖传项目的构建时间砍了90%,领导以为我只是在"优化了一下",结果隔壁组的CI都崩了来问我配置
前端·webpack
风骏时光牛马1 小时前
JSP页面直接输出实体对象空属性引发页面500报错实战案例
前端
IT_陈寒2 小时前
Python里这个赋值坑,连老司机都能翻车
前端·人工智能·后端
Hyyy3 小时前
什么是bun?和pnpm有什么区别
前端·面试·bun
IT_陈寒16 小时前
Vue这个坑我跳了两次,原来问题出在这
前端·人工智能·后端
kyriewen16 小时前
我用 50 行代码重写了 React Router 核心,终于搞懂了前端路由原理
前端·javascript·react.js
WebInfra17 小时前
Rspack 2.1 发布:React Compiler 提速 10 倍!
前端