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. APUE(4)---文件和目录 (3)

    十三.函数rename和renameat #include <stdio.h> int rename(const char *oldname, const char *newname); ...

  2. 个人写spark小测试

    写脚本生成类似文件 java 代码 封装类 package day0327; import java.util.UUID; public class data { private String ip; ...

  3. Windows装python

    pycharm常用快捷键ctr+alt+shift+l可以快速格式化python安装下载地址https://www.python.org/downloads/release/python-365/ 一 ...

  4. Mysql自动设置时间(自动获取时间,填充时间)

    应用场景: 1.在数据表中,要记录每条数据是什么时候创建的,不需要应用程序去特意记录,而由数据数据库获取当前时间自动记录创建时间: 2.在数据库中,要记录每条数据是什么时候修改的,不需要应用程序去特意 ...

  5. jira项目管理平台搭建

    参考文档:http://www.cnblogs.com/ilanni/p/6200875.html   一.环境准备 jira7.2的运行是需要依赖java环境的,也就是说需要安装jdk并且要是1.8 ...

  6. Raspberry Pi 3 安装 Lazarus 1.6.2(2017-02-09更新)

    Raspberry Pi3 Lazarus 1.6.2 安装步骤如下: 安装环境:Raspbian Jessie, RPi3 1.安装subversion和unzip Sudo Apt-get upd ...

  7. C# 二维码生成——QRCode

    C#二维码生成,这里使用开源的ThoughtWorks.QRCode.dll库. 步骤: 1.下载ThoughtWorks.QRCode.dll库文件,并引用到项目中. 2.创建QRCodeHandl ...

  8. C++11左值引用和右值引用

    转载:https://www.cnblogs.com/golaxy/p/9212897.html C++11的左值引用与右值引用总结 概念 1.&与&&  对于在C++中,大家 ...

  9. 百万数据测试 Entity Framework 到底有多慢

    测试环境 硬件:阿里云乞丐配置 操作系统:Centos 7 CPU: 1核 内存:1 GB (I/O优化) 网络:1Mbps(峰值) 软件 .net core 2.0 ZKEACMS For .net ...

  10. Spring Boot - 记录日志

    比自己写文本日志的好处 默认定义好了一些日志级别,会记录当前使用的级别以上的日志,通常线上环境设置的级别较高记得较少 有一些自动split之类的功能 Commons-logging 日志级别:TRAC ...