KOA 与 CO 实现浅析
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
的原因。
KOA 与 CO 实现浅析的更多相关文章
- SQL Server on Linux 理由浅析
SQL Server on Linux 理由浅析 今天的爆炸性新闻<SQL Server on Linux>基本上在各大科技媒体上刷屏了 大家看到这个新闻都觉得非常震精,而美股,今天微软开 ...
- 【深入浅出jQuery】源码浅析--整体架构
最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...
- 高性能IO模型浅析
高性能IO模型浅析 服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种: (1)同步阻塞IO(Blocking IO):即传统的IO模型. (2)同步非阻塞IO(Non-blocking ...
- netty5 HTTP协议栈浅析与实践
一.说在前面的话 前段时间,工作上需要做一个针对视频质量的统计分析系统,各端(PC端.移动端和 WEB端)将视频质量数据放在一个 HTTP 请求中上报到服务器,服务器对数据进行解析.分拣后从不同的 ...
- Jvm 内存浅析 及 GC个人学习总结
从诞生至今,20多年过去,Java至今仍是使用最为广泛的语言.这仰赖于Java提供的各种技术和特性,让开发人员能优雅的编写高效的程序.今天我们就来说说Java的一项基本但非常重要的技术内存管理 了解C ...
- 从源码浅析MVC的MvcRouteHandler、MvcHandler和MvcHttpHandler
熟悉WebForm开发的朋友一定都知道,Page类必须实现一个接口,就是IHttpHandler.HttpHandler是一个HTTP请求的真正处理中心,在HttpHandler容器中,ASP.NET ...
- 【深入浅出jQuery】源码浅析2--奇技淫巧
最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...
- Node.js实现RESTful api,express or koa?
文章导读: 一.what's RESTful API 二.Express RESTful API 三.KOA RESTful API 四.express还是koa? 五.参考资料 一.what's R ...
- 浅析匿名函数、lambda表达式、闭包(closure)区别与作用
浅析匿名函数.lambda表达式.闭包(closure)区别与作用 所有的主流编程语言都对函数式编程有支持,比如c++11.python和java中有lambda表达式.lua和JavaScript中 ...
随机推荐
- APUE(4)---文件和目录 (3)
十三.函数rename和renameat #include <stdio.h> int rename(const char *oldname, const char *newname); ...
- 个人写spark小测试
写脚本生成类似文件 java 代码 封装类 package day0327; import java.util.UUID; public class data { private String ip; ...
- Windows装python
pycharm常用快捷键ctr+alt+shift+l可以快速格式化python安装下载地址https://www.python.org/downloads/release/python-365/ 一 ...
- Mysql自动设置时间(自动获取时间,填充时间)
应用场景: 1.在数据表中,要记录每条数据是什么时候创建的,不需要应用程序去特意记录,而由数据数据库获取当前时间自动记录创建时间: 2.在数据库中,要记录每条数据是什么时候修改的,不需要应用程序去特意 ...
- jira项目管理平台搭建
参考文档:http://www.cnblogs.com/ilanni/p/6200875.html 一.环境准备 jira7.2的运行是需要依赖java环境的,也就是说需要安装jdk并且要是1.8 ...
- 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 ...
- C# 二维码生成——QRCode
C#二维码生成,这里使用开源的ThoughtWorks.QRCode.dll库. 步骤: 1.下载ThoughtWorks.QRCode.dll库文件,并引用到项目中. 2.创建QRCodeHandl ...
- C++11左值引用和右值引用
转载:https://www.cnblogs.com/golaxy/p/9212897.html C++11的左值引用与右值引用总结 概念 1.&与&& 对于在C++中,大家 ...
- 百万数据测试 Entity Framework 到底有多慢
测试环境 硬件:阿里云乞丐配置 操作系统:Centos 7 CPU: 1核 内存:1 GB (I/O优化) 网络:1Mbps(峰值) 软件 .net core 2.0 ZKEACMS For .net ...
- Spring Boot - 记录日志
比自己写文本日志的好处 默认定义好了一些日志级别,会记录当前使用的级别以上的日志,通常线上环境设置的级别较高记得较少 有一些自动split之类的功能 Commons-logging 日志级别:TRAC ...