经常在使用 koa 的时候,通过 .use
的形式来注册各种中间件,例如下面一段代码
js
app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(2);
});
app.use(async (ctx, next) => {
console.log(3);
await next();
console.log(4);
});
这里会输出 1,3,4,2,下面就来翻看一下源码看看这个中间件实现的具体原理。
在看具体代码之前,先温习一下,使用 koa 的最小运行代码是什么样的
js
const Koa = require("koa");
const app = new Koa();
// response
app.use((ctx) => {
ctx.body = "Hello Koa";
});
app.listen(3000);
可以看到,最后通过 listen 方法来启动服务,那我们重点先看下 use 和 listen 做了什么事情。
use
js
use (fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
debug('use %s', fn._name || fn.name || '-')
this.middleware.push(fn)
return this
}
代码比较少,这里直接贴上去了,use 的主要作用就是给 middleware 添加相对应的 fn。
listen
js
listen (...args) {
debug('listen')
const server = http.createServer(this.callback())
return server.listen(...args)
}
这里 server 是 http 的库的方法,我们先不管,主要看一下 this.callback 做了什么事情。
js
callback () {
const fn = this.compose(this.middleware)
if (!this.listenerCount('error')) this.on('error', this.onerror)
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res)
if (!this.ctxStorage) {
return this.handleRequest(ctx, fn)
}
return this.ctxStorage.run(ctx, async () => {
return await this.handleRequest(ctx, fn)
})
}
return handleRequest
}
handleRequest 这个函数的实现如下
js
handleRequest (ctx, fnMiddleware) {
const res = ctx.res
res.statusCode = 404
const onerror = err => ctx.onerror(err)
const handleResponse = () => respond(ctx)
onFinished(res, onerror)
return fnMiddleware(ctx).then(handleResponse).catch(onerror)
}
可以看到,最终是把 this.compose 返回的 fn 进行了调用,那么由此可以知道 this.compose 就是具体中间件调度的具体实现。
compose
this.compose 可以在 constructor 中看到,默认情况下就是 koa-compose,这个库也非常精简只有 50 行代码,下面会通过注释的形式来对源码进行一个说明。
js
// 省略部分注释和部分代码
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!");
}
// 这里默认返回一个函数,handleRequest函数会调用这个返回的函数,并且传递 context
return function (context, next) {
let index = -1;
// 默认情况下执行一次 dispatch ,dispatch因为是函数声明所以会提升到做作用域顶部
return dispatch(0);
function dispatch(i) {
// 通常情况下不会遇到,但是如果执行两次就会抛出异常,例如第一次调用index为-1,i为0,第二次执行则变成index为0,i也为0则抛出错误
if (i <= index)
return Promise.reject(new Error("next() called multiple times"));
index = i;
let fn = middleware[i];
// 这里从koa的实现可以看到,是没有传递next的,所以这行代码可以跳过
if (i === middleware.length) fn = next;
// 执行到最后一项的时候直接返回不再继续递归下去
if (!fn) return Promise.resolve();
try {
// 这里实现很巧妙,利用了bind的原理,bind的第一个参数为this,之后的参数为函数的预设值,最后返回一个函数
// 然后根据Promise.resolve的实现规范,如果传递的Promise.resolve是一个Promise要等待新的Promise执行完成之后决定状态
// 这里推荐看下PromiseA+规范实现
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
};
}
ok,这里基本上就讲完了,还是对照最初的示例来看
js
app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(2);
});
app.use(async (ctx, next) => {
console.log(3);
await next();
console.log(4);
});
这里传递给 compose([fn1, fn2]),之后 i 为 0,返回
js
const fn = async (ctx, next) => {
console.log(1);
await next();
console.log(2);
};
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
之后 i 为 1 的时候
js
const fn = async (ctx, next) => {
console.log(3);
await next();
console.log(4);
};
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
此时 i 为 2,发现数组取不到值了,执行
js
if (!fn) return Promise.resolve();
回到最最后一步,还没有解释为什么会洋葱结构这样来执行代码
js
app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(2);
});
app.use(async (ctx, next) => {
console.log(3);
await next();
console.log(4);
});
这里我们知道先执行第一个函数
- 输出 console.log(1);
- 执行的过程中遇到
await next()
会执行 next,而从源码dispatch.bind(null, i + 1)
可以知道下一项就是第二个函数 - 执行第二个函数
- 输出 console.log(3)
- 继续执行
await next()
从源码知道 i 为 2 的时候,返回的是if (!fn) return Promise.resolve();
- 执行 conosle.log(4),返回 Promise 状态为已完成,结果为 undefined
- 执行 console.log(2)
上面可能有点绕,但是其实 koa-compose 利用了事件循环的机制,对于微任务每次执行都会放到微任务队列,等待主线程执行栈调用,而栈的特点就是先进后出,所以这也是为什么会输出 1,3,4,2 的原因了。
最后
如果文章有说的不对地方欢迎指出,最后本人正在找工作,有相关 hc 岗位欢迎滴滴。