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. BZOJ 2038 小z的袜子(莫队)

    Description 作为一个生活散漫的人,小Z每天早上都要耗费很久从一堆五颜六色的袜子中找出一双来穿.终于有一天,小Z再也无法忍受这恼人的找袜子过程,于是他决定听天由命……具体来说,小Z把这N只袜 ...

  2. linux 进阶2--C++读取lua文件中的变量、一维表、二维表

    lua 语言非常灵活,一般把lua 作为脚本文件,会用C++与之进行交互.最重要的是C++代码能读取到脚本中的变量.一维表.二维表. 这样有些参数就可以在lua文件进行更改,而不用重新更改C++代码. ...

  3. 13、Semantic-UI之表格与表单

    13.1 定义基础样式表格   在HTML中可以通过table进行表格定义,在Semantic-UI中也可以通过class="ui table"定义表格. 示例:定义基础表格 &l ...

  4. IIS应用程序池自动回收作业

    vb: appPoolName = WScript.Arguments() Set oWebAdmin = GetObject("winmgmts:root\WebAdministratio ...

  5. [Erlang09]Erlang gen_server实现定时器(interval)的几种方法及各自的优缺点?

    方法1: %%gen_server:部分call_back function. -define(TIME,1000). init([]) –> erlang:send_after(?TIME,s ...

  6. Python 串口通信 GUI 开发

    在项目中遇到树莓派串口通信问题.由于本人一直从事.net 开发,希望将树莓派系统换成Win10 IOT版.但是在测试过程中出现无法找到串口的问题.最终也没有解决.最终按照领导要求,linux (了解不 ...

  7. Slq怎么样获取首条记录和最后一条记录

    sql如何查询表的第一条记录和最后一条记录 方法一:使用top select TOP 1 * from apple;TOP 1 表示表apple中的第一条数据 select TOP 1 * from ...

  8. .net 抽象类(abstract)和接口(interface)区别

    1.抽象类    (1) 抽象方法只作声明,而不包含实现,可以看成是没有实现体的虚方法    (2) 抽象类不能被实例化    (3) 抽象类可以但不是必须有抽象属性和抽象方法,但是一旦有了抽象方法, ...

  9. python设计模式-适配器

    定义: 将一个接口转换为客户希望的另一个接口,该模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作 适配器模式又叫变压器模式,也叫包装模式(Wrapper),它的核心思想是将一个对象经过包装 ...

  10. maven中archetype(原型)的使用

    原文链接:https://www.cnblogs.com/snowstar123/p/3449349.html 最近项目组做好一套框架,为了推广需要创建一些空白项目给项目组使用,因为所有的空白项目里面 ...