Koa的合并思路并不复杂,就是让compose再返回一个函数,返回的这个函数会开始这个数组的遍历工作:
function compose(middleware) { // 参数检查,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!"); } // 返回一个方法,这个方法就是compose的结果 // 外部可以通过调用这个方法来开起中间件数组的遍历 // 参数形式和普通中间件一样,都是context和next return function (context, next) { return dispatch(0); // 开始中间件执行,从数组第一个开始 // 执行中间件的方法 function dispatch(i) { let fn = middleware[i]; // 取出需要执行的中间件 // 如果i等于数组长度,说明数组已经执行完了 if (i === middleware.length) { fn = next; // 这里让fn等于外部传进来的next,其实是进行收尾工作,比如返回404 } // 如果外部没有传收尾的next,直接就resolve if (!fn) { return Promise.resolve(); } // 执行中间件,注意传给中间件接收的参数应该是context和next // 传给中间件的next是dispatch.bind(null, i + 1) // 所以中间件里面调用next的时候其实调用的是dispatch(i + 1),也就是执行下一个中间件 try { return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err); } } }; }上面代码主要的逻辑就是这行:
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));这里的fn就是我们自己写的中间件,比如文章开始那个logger,我们稍微改下看得更清楚:
const logger = async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); }; app.use(logger);那我们compose里面执行的其实是:
logger(context, dispatch.bind(null, i + 1));也就是说logger接收到的next其实是dispatch.bind(null, i + 1),你调用next()的时候,其实调用的是dispatch(i + 1),这样就达到了执行数组下一个中间件的效果。
另外由于中间件在返回前还包裹了一层Promise.resolve,所以我们所有自己写的中间件,无论你是否用了Promise,next调用后返回的都是一个Promise,所以你可以使用await next()。
koa-compose的源码看这里:https://github.com/koajs/compose/blob/master/index.js
app.createContext上面用到的this.createContext也是一个实例方法。这个方法根据http.createServer传入的req和res来构建ctx这个上下文,官方源码长这样:
这段代码里面context,ctx,response,res,request,req,app这几个变量相互赋值,头都看晕了。其实完全没必要陷入这堆面条里面去,我们只需要将他的思路和骨架拎清楚就行,那怎么来拎呢?
首先搞清楚他这么赋值的目的,他的目的其实很简单,就是为了使用方便。通过一个变量可以很方便的拿到其他变量,比如我现在只有request,但是我想要的是req,怎么办呢?通过这种赋值后,直接用request.req就行。其他的类似,这种面条式的赋值我很难说好还是不好,但是使用时确实很方便,缺点就是看源码时容易陷进去。
那request和req有啥区别?这两个变量长得这么像,到底是干啥的?这就要说到Koa对于原生req的扩展,我们知道http.createServer的回调里面会传入req作为请求对象的描述,里面可以拿到请求的header啊,method啊这些变量。但是Koa觉得这个req提供的API不好用,所以他在这个基础上扩展了一些API,其实就是一些语法糖,扩展后的req就变成了request。之所以扩展后还保留的原始的req,应该也是想为用户提供更多选择吧。所以这两个变量的区别就是request是Koa包装过的req,req是原生的请求对象。response和res也是类似的。
既然request和response都只是包装过的语法糖,那其实Koa没有这两个变量也能跑起来。所以我们拎骨架的时候完全可以将这两个变量踢出去,这下骨架就清晰了。
那我们踢出response和request后再来写下createContext这个方法:
// 创建上下文ctx对象的函数 createContext(req, res) { const context = Object.create(this.context); context.app = this; context.req = req; context.res = res; return context; }这下整个世界感觉都清爽了,context上的东西也一目了然了。但是我们的context最初是来自this.context的,这个变量还必须看下。
app.createContext对应的官方源码看这里:
context.js