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.jsresponse.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 中,定义了三个函数 onFulfilledonRejectednext,先看下 next 的定义。

next 的参数实际上就是gen每次 gen.next() 的返回值。如果 gen 已经执行结束,那么 Promise 将返回;否则,将 ret.value promise 化,并再次调用 onFulfilledonRejected 函数。

onFulfilledonRejected 帮助我们推进 gen 的执行。

nextonFulfilledonRejected 的组合,实现了 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 对象,这个对象的 resolvereject 处理函数又分别是上一个 promise 中定义的 onFulfilledonRejected 函数。至此,就完成了 compose 函数返回的 generator 链的推进工作。

最后还有一个问题需要明确一下,那就是 KOA 中的 context 是如何传递的。

通过观察前面的代码不难发现,每次关键节点的函数调用都是使用的 xxxFunc.call(ctx) 的方式,这也正是为什么我们可以在中间件中直接通过 this 访问 context 的原因。

KOA 与 CO 实现浅析的更多相关文章

  1. SQL Server on Linux 理由浅析

    SQL Server on Linux 理由浅析 今天的爆炸性新闻<SQL Server on Linux>基本上在各大科技媒体上刷屏了 大家看到这个新闻都觉得非常震精,而美股,今天微软开 ...

  2. 【深入浅出jQuery】源码浅析--整体架构

    最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...

  3. 高性能IO模型浅析

    高性能IO模型浅析 服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种: (1)同步阻塞IO(Blocking IO):即传统的IO模型. (2)同步非阻塞IO(Non-blocking  ...

  4. netty5 HTTP协议栈浅析与实践

      一.说在前面的话 前段时间,工作上需要做一个针对视频质量的统计分析系统,各端(PC端.移动端和 WEB端)将视频质量数据放在一个 HTTP 请求中上报到服务器,服务器对数据进行解析.分拣后从不同的 ...

  5. Jvm 内存浅析 及 GC个人学习总结

    从诞生至今,20多年过去,Java至今仍是使用最为广泛的语言.这仰赖于Java提供的各种技术和特性,让开发人员能优雅的编写高效的程序.今天我们就来说说Java的一项基本但非常重要的技术内存管理 了解C ...

  6. 从源码浅析MVC的MvcRouteHandler、MvcHandler和MvcHttpHandler

    熟悉WebForm开发的朋友一定都知道,Page类必须实现一个接口,就是IHttpHandler.HttpHandler是一个HTTP请求的真正处理中心,在HttpHandler容器中,ASP.NET ...

  7. 【深入浅出jQuery】源码浅析2--奇技淫巧

    最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...

  8. Node.js实现RESTful api,express or koa?

    文章导读: 一.what's RESTful API 二.Express RESTful API 三.KOA RESTful API 四.express还是koa? 五.参考资料 一.what's R ...

  9. 浅析匿名函数、lambda表达式、闭包(closure)区别与作用

    浅析匿名函数.lambda表达式.闭包(closure)区别与作用 所有的主流编程语言都对函数式编程有支持,比如c++11.python和java中有lambda表达式.lua和JavaScript中 ...

随机推荐

  1. mysql - 查看表字段和字段描述

    1.mysql查看表字段和字段描述 SELECT column_name, column_comment FROM information_schema.columns WHERE table_sch ...

  2. 深入理解java虚拟机(十三) Java 即时编译器JIT机制以及编译优化

    在部分的商用虚拟机中,Java 程序最初是通过解释器( Interpreter )进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会把这些代码认定为“热点代码”.为了提高热点代码的 ...

  3. maven-multiModule

    主pom的定义 packaging:pom modules的指定 dependencyManagement的指定 properties的指定 build或profile的设置 子module的创建 在 ...

  4. [Erlang00]:gen_server:reply/2

    --- gen_server:reply/2 reply(Client, Reply) –> Result      Types:     Client - see below     Repl ...

  5. How to design a product table for many kinds of product where each product has many parameters

    https://stackoverflow.com/questions/695752/how-to-design-a-product-table-for-many-kinds-of-product-w ...

  6. Partition--分区Demo

    --============================================================= --创建分区函数 --创建500分区,分区键按照1000依次递增 CRE ...

  7. Luckily general gradient for spherical harmonics is defined

    http://web4.cs.ucl.ac.uk/staff/j.kautz/publications/gradientSH_RS04.pdf

  8. Math.round(11.5)

    Math.round(-11.5); Math.round(11.5); 经常看到这句代码,特意来总结一下. 查阅资料一直有人说是"四舍六入五成双",四舍六入没错,不过遇到正负数的 ...

  9. cisco和h3c网络设备中一次性打印全部配置信息

    cisco的是全页打印配置信息的命令: #terminal length 0 #show run 华为和h3c的是: >screen-length 0 temporary >display ...

  10. NOIP simulation

    NOIP 模拟赛Day 1题目名称LGTB 玩扫雷LGTB 学分块LGTB 打THD英文代号mine divide thd时限1 秒1 秒1 秒输入文件mine.in divide.in thd.in ...