KOA 与 CO 的实现都很是的短小精悍,只须要花费很短的时间就能够将源代码通读一遍。如下是一些浅要的分析。node
既然 KOA 实现了 web 服务器,那咱们就先从最原始的 web 服务器的实现方式着手。
下面的代码中咱们建立了一个始终返回请求路径的 web 服务器。web
const http = require('http'); const server = http.createServer((req, res) => { res.end(req.url); }); server.listen(8001);
当你请求 http://localhost:8001/some/url
的时候,获得的响应就是 /some/url
。数组
简单的说,KOA 就是对上面这段代码的封装。promise
首先看下 KOA 的大概目录结构:服务器
lib
目录下只有四个文件,其中 request.js
和 response.js
是对 node 原生的 request(req)
和 response(res)
的加强,提供了不少便利的方法,context.js
就是著名的上下文。咱们暂时抛开这三个文件的细节,先看下主文件 application.js
的实现。app
先关注两个函数:dom
// 构造函数 function Application() { if (!(this instanceof Application)) return new Application; this.env = process.env.NODE_ENV || 'development'; this.subdomainOffset = 2; this.middleware = []; this.proxy = false; this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); } // listen 方法 app.listen = function(){ debug('listen'); var server = http.createServer(this.callback()); return server.listen.apply(server, arguments); };
上面的这两个函数,正是完成了一个 web 服务器的创建过程:koa
const server = new KOA(); // new Application() server.listen(8001);
而先前 http.createServer()
的那个回调函数则被替换成了 app.callback
的返回值。函数
咱们细看下 app.callback
的具体实现:oop
app.callback = function(){ if (this.experimental) { console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.') } var fn = this.experimental ? compose_es7(this.middleware) : co.wrap(compose(this.middleware)); var self = this; if (!this.listeners('error').length) this.on('error', this.onerror); return function handleRequest(req, res){ res.statusCode = 404; var ctx = self.createContext(req, res); onFinished(res, ctx.onerror); fn.call(ctx).then(function handleResponse() { respond.call(ctx); }).catch(ctx.onerror); } };
先跳过 ES7 的实验功能以及错误处理,app.callback
中主要作了以下几件事情:
每当服务器接收到请求时,作以下处理:
co.wrap
返回的函数,并作必要的错误处理如今咱们把目光集中到这三行代码中:
// 中间件重组与 co 包装 var fn = co.wrap(compose(this.middleware)); // ------------------------------------------ // 在处理 request 的回调函数中 // 建立每次请求的上下文 var ctx = self.createContext(req, res); // 调用 co 包装的函数,执行中间件 fn.call(ctx).then(function handleResponse() { respond.call(ctx); }).catch(ctx.onerror);
先看第一行代码,compose
实际上就是 koa-compose
,实现以下:
function compose(middleware){ return function *(next){ if (!next) next = noop(); var i = middleware.length; while (i--) { next = middleware[i].call(this, next); } return yield *next; } } function *noop(){}
compose
返回一个 generator函数
,这个 generator函数
中倒序依次以 next
为参数调用每一个中间件,并将返回的generator实例
从新赋值给 next
,最终将 next
返回。
这里比较有趣也比较关键的一点是:
next = middleware[i].call(this, next);
咱们知道,调用 generator函数
返回 generator实例
,当 generator函数
中调用其余的 generator函数
的时候,须要经过 yield *genFunc()
显式调用另外一个 generator函数
。
举个例子:
const genFunc1 = function* () { yield 1; yield *genFunc2(); yield 4; } const genFunc2 = function* () { yield 2; yield 3; } for (let d of genFunc1()) { console.log(d); }
执行的结果是在控制台依次打印 1,2,3,4。
回到上面的 compose
函数,其实它就是完成上面例子中的 genFunc1
调用 genFunc2
的事情。而 next
的做用就是保存并传递下一个中间件函数返回的 generator实例
。
参考一下 KOA 中间件的写法以帮助理解:
function* (next) { // do sth. yield next; // do sth. }
经过 compose
函数,KOA 把中间件所有级联了起来,造成了一个 generator
链。下一步就是完成上面例子中的 for-of
循环的事情了,而这正是 co 的工做。
仍是先看下 co.wrap
co.wrap = function (fn) { createPromise.__generatorFunction__ = fn; return createPromise; function createPromise() { return co.call(this, fn.apply(this, arguments)); } };
该函数返回一个函数 createPromise
,也就是 KOA 源码里面的 fn
。
当调用这个函数的时候,实际上调用的是 co
,只是将上下文 ctx
做为 this
传递了进来。
如今分析下 co
的代码:
function co(gen) { var ctx = this; var args = slice.call(arguments, 1) // 返回一个 promise return new Promise(function(resolve, reject) { if (typeof gen === 'function') gen = gen.apply(ctx, args); if (!gen || typeof gen.next !== 'function') return resolve(gen); onFulfilled(); function onFulfilled(res) { var ret; try { ret = gen.next(res); } catch (e) { return reject(e); } next(ret); } function onRejected(err) { var ret; try { ret = gen.throw(err); } catch (e) { return reject(e); } next(ret); } function next(ret) { if (ret.done) return resolve(ret.value); var value = toPromise.call(ctx, ret.value); if (value && isPromise(value)) return value.then(onFulfilled, onRejected); return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, ' + 'but the following object was passed: "' + String(ret.value) + '"')); } }); }
co
函数的参数是 gen
,就是以前 compose
函数返回的 generator实例
。
在 co
返回的 Promise 中,定义了三个函数 onFulfilled
、 onRejected
和 next
,先看下 next
的定义。
next
的参数实际上就是gen
每次 gen.next()
的返回值。若是 gen
已经执行结束,那么 Promise 将返回;不然,将 ret.value
promise 化,并再次调用 onFulfilled
和 onRejected
函数。
onFulfilled
和 onRejected
帮助咱们推动 gen
的执行。
next
和 onFulfilled
、onRejected
的组合,实现了 generator
的递归调用。那么到底是如何实现的呢?关键还要看 toPromise
的实现。
function toPromise(obj) { if (!obj) return obj; if (isPromise(obj)) return obj; if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj); if ('function' == typeof obj) return thunkToPromise.call(this, obj); if (Array.isArray(obj)) return arrayToPromise.call(this, obj); if (isObject(obj)) return objectToPromise.call(this, obj); return obj; }
在 toPromise
函数中,后三个分支处理分别对 thunk 函数、数组和对象进行了处理,此处略去细节,只须要知道最终都调回了 toPromise
的前三个分支处理中。这个函数最终返回一个 promise 对象,这个对象的 resolve
和 reject
处理函数又分别是上一个 promise 中定义的 onFulfilled
和 onRejected
函数。至此,就完成了 compose
函数返回的 generator
链的推动工做。
最后还有一个问题须要明确一下,那就是 KOA 中的 context
是如何传递的。
经过观察前面的代码不难发现,每次关键节点的函数调用都是使用的 xxxFunc.call(ctx)
的方式,这也正是为何咱们能够在中间件中直接经过 this
访问 context
的缘由。