中间件机制(洋葱模型):它实现中间件级联调用是借助 koa-compose
这个库来实现的。源码只有一个函数。
我们先看看没用compose
时是怎么执行中间件的:
function add(x, y) {
return x + y
}
function double(z) {
return z * 2
}
const res1 = add(1, 2)
const res2 = double(res1)
console.log(res2)
// 或
const res3 = double(add(1,2))
console.log(res3)
double函数要等待add函数的执行结果,可以像上面那样写,但是当中间件一多的时候,代码就不够优雅了。
compose
函数的作用就是将这些中间件整合在一起,然后利用闭包返回一个函数,然后我们直接执行返回的函数即可。
const middlewares = [add, double]
let len = middlewares.length
function compose(midds) {
return (...args) => {
// 初始值
let res = midds[0](...args)
for(let i = 1; i < len; i++){
res = midds[i](res)
}
return res
}
}
const fn = compose(middlewares)
const res = fn(1,2)
console.log(res)
当然,上面的代码为了便于理解,都是同步的。下面看看compose的源码是怎么做的:
'use strict'
/**
* Expose compositor.
*/
module.exports = compose
/**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
*
* @param {Array} middleware
* @return {Function}
* @api public
*/
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
参考了这篇。
首先是对传入的middleware
的一些检查。
然后返回一个闭包,闭包中的dispatch
就是执行中间件的函数。在上面的 dispatch(0)
传入了0,用于获取 middleware[0]
中间件。
let fn = middleware[i]
获得中间件后,怎么使用?
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
上面的代码执行了中间件函数fn(context, dispatch.bind(null, i + 1))
,并传递了 context
和 dispatch.bind(null, i + 1)
函数。
context 就是 koa 中的上下文对象 ctx。dispatch.bind(null, i + 1)
就是我们的next。值得一提的是 i+1 这个参数,传递这个参数就相当于执行了下一个中间件,从而形成递归调用。
这也就是为什么我们在自己写中间件的时候,要先拿到next参数,然后再需要手动执行 next()
:
await next()
即是执行了dispatch.bind(null, i + 1)
只有执行了 next 函数,才能正确得执行下一个中间件。
每个中间件只能执行一次 next,如果在一个中间件内多次执行 next,就会出现问题。
每次执行 dispatch
前,index 必定会小于 i。第一次调用next()后,将i赋值给了index,此时index与i相等。
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
如果再次在这个中间件中调用next,会再次执行 dispatch(i+1)。此时i还是原来的i,而闭包外的index因为第一次的执行,已经是等于i的了。所以将执行:
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
导致报错,现在知道为什么同一个中间件中执行多次next()会报错了吧。
image.png
最后,递归调用完后,就执行next()后面的代码了,一张图很形象的说明:
image.png
至此,这就是我们koa的中间件机制(洋葱模型)。
现在,我们来完善上一篇博客
在application.js
的类中直接加入compose
函数源码。这时候,将use方法改为:
use(callback) {
// this.callback = callback
this.middlewares.push(callback)
}
使用use时先将中间件加入数组。
这个 this.middlewares
在 constructor
中初始化为一个空数组:
this.middlewares = []
将listen方法改为:
listen(...args) {
const server = http.createServer(async (req, res) => {
let ctx = this.createCtx(req, res)
// await this.callback(ctx)
const fn = this.compose(this.middlewares)
await fn(ctx)
ctx.res.end(ctx.body)
})
server.listen(...args)
}
将中间件数组传入compose
中整合。
这样子,在我们的 server.js
中就可以这样用了:
const Coa = require('./coa2')
const app = new Coa()
app.use(async (ctx, next) => {
ctx.body = '1'
await next()
ctx.body += '[1]'
})
app.use(async (ctx, next) => {
ctx.body += '2'
await next()
ctx.body += '[2]'
})
app.use(async (ctx, next) => {
ctx.body += '3'
ctx.body += '[3]'
})
app.listen(8000, () => {
console.log('coa!')
})
网友评论