KOA 与 CO 的实现都非常的短小精悍,只需要花费很短的时间就可以将源代码通读一遍。以下是一些浅要的分析。
如何用 node 实现一个 web 服务器
既然 KOA 实现了 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 的实现
简单的说,KOA 就是对上面这段代码的封装。
首先看下 KOA 的大概目录结构:
lib
目录下只有四个文件,其中 request.js
和 response.js
是对 node 原生的 request(req)
和 response(res)
的增强,提供了很多便利的方法,context.js
就是著名的上下文。我们暂时抛开这三个文件的细节,先看下主文件 application.js
的实现。
先关注两个函数:
// 构造函数
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 服务器的建立过程:
const server = new KOA(); // new Application()
server.listen(8001);
而先前 http.createServer()
的那个回调函数则被替换成了 app.callback
的返回值。
我们细看下 app.callback
的具体实现:
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 包装
- 返回处理request的回调函数
每当服务器接收到请求时,做如下处理:
- 初始化上下文
- 调用之前
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 的原理分析
还是先看下 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
的原因。