上一篇文章从0到1 讲解了express中间件的加载以及运行原理,本文再将koa从0至1根据源码来解析koa的中间件加载机制。
上一篇请参考express中间件加载原理
闲言少讲,放码过来,本次同样是以koa-generator 生成的极简项目为例,来看看koa的中间件是如何定义以及使用的。
首先不得不夸一下,koa良心了,在初始化项目时安装了nodemon,直接执行npm run dev就会使用nodemon启动项目,妈妈再也不用担心我更新文件之后忘记重启了。
"scripts": {
"start": "node bin/www",
"dev": "./node_modules/.bin/nodemon bin/www",
"prd": "pm2 start bin/www",
"test": "echo \"Error: no test specified\" && exit 1"
},
首先来看最常用的use方法
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
what? 竟然这么简单?什么都没有处理?就简简单单的把传入的function传入到了middleware对象中,相比较起来,比express确实简单了很多。
那好吧,我们也不必纠结,毕竟koa是express之后的一代,相比于前一代肯定要进步很多。那么让我们来看看它在使用的时候是怎么处理的吧。
通过源码我们可知,创建server的时候,koa传入了this.callback(),那么请求来了之后,就会执行callback方法返回的方法了。我们先来看看callback方法。
callback() {
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
callback 方法中我们终于见到了著名的洋葱模型,compose函数。可以看到,callback函数的执行结果是返回handleRequest函数,而这个函数则是通过传入的req和res,生成ctx对象,在调用application的hanldeRequest方法,去处理。而handleRequest方法也很简单,就是执行我们通过compose函数生成的函数,
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);
}
那么接下来就简单了,我们来看看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)
}
}
}
}
koa-compose方法很简单,只有短短的三十行代码,通过前文可知,middleware就是用户通过use方法传入的一个一个的function。而compose方法只是简单的作了一下处理,然后就返回了一个匿名函数,在fnMiddleware(ctx)调用的时候,这个函数才真正的被调用到。
dispatch函数返回的时候使用promise.resolve处理了一下,是防止传入的函数是非promise函数,就无法进行promise的链式调用了。然后执行了取到的第0个函数,以及下一个next函数,即dispatch.bind(null, i+1)。通过bind函数特性可知,我们在调用next函数时,将会直接bind中传入的i+1作为参数。那么就不用担心用户会传入错误的参数。而且下一步调用的位置也可以有fn中的函数自己来决定,即可以先执行next或者后执行next,或者再执行的中间执行next,从而达到了洋葱模型的效果。
使用时方法如下:
app.use(function middleware1(ctx, next) {
console.log(1);
next();
console.log(2);
});
app.use(function middleware2(ctx, next) {
console.log(3);
next();
console.log(4);
});
在执行时的输出顺序为:1,3,4,2 。
看到源码时我突然有个疑问❓就是app。use方法,是取的入参的第一项作为function的,那么如果你传入多项还有意义么?使用koa2-generator生成的app.js,默认路由的如参数是这样的
app.use(index.routes(), index.allowedMethods())
app.use(users.routes(), users.allowedMethods())
所以其实第二项入参是无用的。这应该算是一个bug吧。
midway的compse函数做了什么处理?
midway在官方文档中讲midway对洋葱模型做了一些微调,那么微调了哪些内容呢?
const composeFn = (context: T, next?) => {
const supportBody = isIncludeProperty(context, 'body');
// last called middleware #
let index = -1;
return dispatch(0);
function dispatch(i) {
if (i <= index)
return Promise.reject(
new MidwayCommonError('next() called multiple times')
);
index = i;
let fn = (newMiddlewareArr as Array<FunctionMiddleware<T, R, N>>)[i];
if (i === newMiddlewareArr.length) fn = next;
if (!fn) return Promise.resolve();
try {
if (supportBody) {
return Promise.resolve(
fn(context, dispatch.bind(null, i + 1), {
index,
} as any)
).then(result => {
/**
* 1、return 和 ctx.body,return 的优先级更高
* 2、如果 result 有值(非 undefined),则不管什么情况,都会覆盖当前 body,注意,这里有可能赋值 null,导致 status 为 204,会在中间件处进行修正
* 3、如果 result 没值,且 ctx.body 已经赋值,则向 result 赋值
*/
if (result !== undefined) {
context['body'] = result;
} else if (context['body'] !== undefined) {
result = context['body'];
}
return result;
});
} else {
return Promise.resolve(
fn(context, dispatch.bind(null, i + 1), {
index,
} as any)
);
}
} catch (err) {
return Promise.reject(err);
}
}
};
其实就是重点改造的就是添加了中文注释的那一部分。在midway中,可以通过return方法对中间件进行传参,在实际的处理过程中,是如果result存在,那么就会将result赋值给context.body,反之如果不存在,但是context.body存在,则会将其赋值给result。这么做很大程度上方便了开发者,可以对上一个函数的处理结果进行修改。可是同样的也会存在一些问题,比如说某一个中间件返回时返回了一个全新的东西,将上一个中间件的结果遗弃了,那么后续的中间件都会以这个中间件返回的结果作为结果。这样的处理方式仁者见仁。在开发时需要多加注意。
其实洋葱模型的理解主要还是需要开发者debug,一步一步断点的方式去调试,这样才能理解的更加透彻。使用vscode的 JavaScript Debug Terminal 可以很方便的进行调试, 使用cmd+shift+p,试一下吧~。