前言
最近在学习 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---全局中间件1news1---局部中间件1news1---局部中间件2use2---全局中间件1new2---局部中间件1new2---局部中间件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---全局中间件1news1---局部中间件1news1---局部错误中间件1use2---全局中间件1new2---局部中间件1new2---局部中间件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---全局中间件1news1---局部中间件11
有没有发现莫名其妙多出来一个1,这正是我们抛出next(1)传递的参数,(throw 同理),同时也表明我们的错误并没有被错误中间件捕获到。
由此可以推断出,错误运行情况下:
- 发送错误的中间件后续会寻找 (相同域或全局)的错误中间件。
- 相同域:在同一个
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---全局中间件1news1---局部中间件1use2---全局错误中间件1use2---全局中间件1new2---局部中间件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---局部中间件1use2---全局中间件1new2---局部中间件2
由此可以推断出:初始化运行时会忽略之前定义的错误中间,会寻找第一个正常的中间件运行。
其他
对于一些其他边界情况就不一一示例赘述了,大致说明一下。
- 中间件函数参数
next决定下一个中间件的调用运行。 - 函数内
next多次调用无效,只会运行第一次。 - 中间件函数运行出错:
throw xx,抛出错误 。next(参数),传递参数。中间件函数包装为promise,函数包装为promise。
js
app.use(async (req, res, next) => {
await Promise.reject()
})
规律总结
- 中间件函数运行时,首先寻找第一个正常中间件函数作为入口。
- 函数中需要手动的交给后续中间件处理。
- 中间件函数运行过程中发生错误,会寻找后续符合条件的中间件函数运行。
- 如果后续没有符合条件的中间函数,则停止运行,并打印出这个错误。
(第4点不太重要) 
符合条件:全局或相同域注册的错误中间件函数。
基于上述规律,我们就着手开始尝试实现。
express中间件实现
前言
着重于实现上述效果,与真正的express实现逻辑肯定大相径庭,不喜勿喷。
思考
- 函数入口怎么寻找?,怎么区分函数是错误中间件还是正常的中间件?
- 函数入口:是由正常的中间件开始,那么我们只需要知道第一个正常中间件就行了。
- 怎么区分:通过
函数.length属性来区分。
- 中间件函数忽略过程怎么实现?
- 换个角度看问题,运行中间件函数本质就是依次运行,而中间件需要忽略函数,本质就是不调用使用者定义的函数,转而内部直接调用
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()
- 中间件函数运行发生错误后续?
- 错误发生分为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()
- 函数多次调用怎么阻止?
- 两种实现。
- 每个函数内部可以使用
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
}
}
}
实现思路与上述思考过程大差不差,不过我这里使用的双向链表结构,通过
函数.next访问下一个中间件,函数.prev访问上一个中间件。通过
函数.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('运行')
})
创作不易,如有疑问或者错误描述的地方,可评论或者私信,我会及时修改和回复,谢谢。