koa源码分析系列(三)koa的实现
更新日期:
koa是TJ大神新一代的中间件框架,本系列旨在一步一步实现koa的功能,包括下面这些。
koa基于co实现,co又是使用了es6的generator特性,所以,没错这个特性支持很一般。
有下面几种办法体验generator:
- node v0.11 可以使用 (node —harmony)
- 使用gnode来使用,不过据说性能一般
- 使用chrome体验,打开chrome://flags/, 搜索harmony, 启用,重启chrome即可。
koa简介与使用
koa是基于generator与co之上的新一代的中间件框架。虽然受限于generator的实现程度。。但是它的优势却不容小觑。
- 有了koa,我们可以很好的解决回调的问题。只要yield就行,还可以直接用try来捕获异常
- koa会自动帮你改造node的req,res对象,省去你很多工作。再也不需要每个res.end都要写一大堆返回状态了,也不需要各种检测错误了,也不需要每次都用finish来确保程序正常关闭了。
- 内置了很多以前express的第三方基础库,更加方便。这样你写中间件的时候没必要到处安装依赖库。
使用方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | var koa = require('koa'); var app = koa(); //添加中间件1 app.use(function *(next){ var start = new Date; console.log("start=======1111"); yield next; console.log("end=======1111"); var ms = new Date - start; console.log('%s %s - %s', this.method, this.url, ms); }); //添加中间件2 app.use(function *(){ console.log("start=======2222"); this.body = 'Hello World'; console.log("end=======2222"); }); app.listen(3000); /* start=======1111 start=======2222 end=======2222 end=======1111 GET / - 10 start=======1111 start=======2222 end=======2222 end=======1111 GET /favicon.ico - 5 */ |
这就是官方的例子,运行后访问localhost:3000
,控制台会打印这些东西。
访问首页会有两个请求,一个是网站小图标favicon.ico
,一个是首页。我们只需要看第一个请求。
首先我们使用var app = koa();
获得一个koa对象。
之后我们可以使用app.use()
来添加中间件。use函数接受一个generatorFunction。这个generatorFunction就是一个中间件。generatorFunction有一个参数next。这个next是下一个中间件generatorFunction的对应generator对象。
比如上面的代码第7行next就是下面添加第二个中间件的generatorFunction的对应generator。
yield next;
代表调用下一个中间件的代码。
对于上面的例子。
一个请求会先执行第一个中间件的:
1 2 | var start = new Date; console.log("start=======1111"); |
遇到yield next;
的时候会转过去执行后来的中间件的代码也就是:
1 2 3 | console.log("start=======2222"); this.body = 'Hello World'; console.log("end=======2222"); |
等下一级中间件执行完毕后才会继续执行接下来的:
1 2 3 | console.log("end=======1111"); var ms = new Date - start; console.log('%s %s - %s', this.method, this.url, ms); |
说白了yield next;
的作用就是我们之前提到过的delegating yield的功能,只不过这边是通过co支持的,而不是使用的原生的。
通过这种中间件机制,我们可以对一个请求的之前与之后做出处理。这种思想其实在java里面已经很出名了。java框架Spring的 Filter过滤器就是这个概念。这种编程方式叫做面向切面编程。
有了这种next的机制 我们只需要关心写各种中间件,就可以很容易的把应用搭建起来了。
一步一步实现koa
简单例子
首先我们写一个最简单的hello word网页。
1 2 3 4 5 6 | var http = require('http'); http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('Hello World\n'); }).listen(1337, '127.0.0.1'); console.log('Server running at http://127.0.0.1:1337/'); |
官方标准例子,相当简单。不过毫无扩展性。
简单改良
我们进行下改良:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | var http = require('http'); function Application (){ this.context = {}; this.context['res'] = null; } var app = Application.prototype; function respond(){ this.res.writeHead(200, {'Content-Type': 'text/plain'}); this.res.end(this.body); } app.use = function(fn){ this.do = fn; } app.callback = function(){ var fn = this.do; var that = this; return function(req,res){ that.context.res = res; fn.call(that.context); respond.call(that.context); } } app.listen = function(){ var server = http.createServer(this.callback()); return server.listen.apply(server, arguments); }; //调用 var appObj = new Application(); appObj.use(function(){ this.body = "hello world!"; }) appObj.listen(3000); |
咋看一下,这么多代码,感觉好复杂,但是应该注意到的是我们实际使用时只要写:
1 2 3 | function(){ this.body = "hello world!"; } |
我们称之为中间件。
解释下上面这段代码,appObj.listen
的时候调用http.createServer
创建一个server实例。通过this.callback()
得到一个标准回调函数。callback是一个高阶函数,返回一个新的执行函数。在执行函数里,我们首先将http请求的res对象保存下来。之后调用存储的this.do
函数。this.do
函数就是我们之前使用appObj.use
添加的,也就是我们的中间件函数。最后调用respond
。在respond
里我们完成通用的处理代码。
使用中间件队列
当然 我们这个还不完善,作为中间件应该可以添加多个,并且顺序执行。
我们需要一种机制,实现上面说的面向切面编程的效果。我们做一些改进:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | var http = require('http'); function Application (){ this.context = {}; this.context['res'] = null; this.middleware = []; } var app = Application.prototype; var respond = function(next){ console.log("start app...."); next(); this.res.writeHead(200, {'Content-Type': 'text/plain'}); this.res.end(this.body); } var compose = function(){ var that = this; var handlelist = Array.prototype.slice.call(arguments,0); var _next = function(){ if((handle = handlelist.shift()) != undefined){ handle.call(that.context,_next); } } return function(){ _next(); } } app.use = function(fn){ //this.do = fn; this.middleware.push(fn) } app.callback = function(){ var mds = [respond].concat(this.middleware); var fn = compose.apply(this,mds); var that = this; return function(req,res){ that.context.res = res; fn.call(that.context); //respond.call(that.context); } } app.listen = function(){ var server = http.createServer(this.callback()); return server.listen.apply(server, arguments); }; //调用 var appObj = new Application(); appObj.use(function(next){ this.body = "hello world!"; next(); }) appObj.use(function(){ this.body += "by me!!"; }) appObj.listen(3000); |
这样实现了可以使用use添加多个中间件的功能,并且respond我们也作为一个中间件放在了最前。为什么放在最前面在下面再分析。
use的时候我们将所有的中间件存起来。在app.callback里面通过compose
对所有的中间件进行一次“编译”,返回一个启动函数fn。
我们看下compose的实现:
1 2 3 4 5 6 7 8 9 10 11 12 | function compose(handlelist){ var that = this; var handle = null; var _next = function(){ if((handle = handlelist.shift()) != undefined){ handle.call(that.context,_next); } } return function(){ _next(); } } |
compose也是一个高阶函数,它内部定义了一个_next函数,用于不停的从队列中拿中间件函数执行,并且传入_next的引用,这样每个中间件函数都可以在自己内部调用下一个中间件。compose会返回一个启动函数,就是初始调用_next()。这样一个由中间件组成的,一层层的操作就开始了。注意这边的调用顺序,一个中间件的代码,”next”关键字之前的会先执行,之后会跳入下一个中间件执行”next”关键字之前的代码,一直跳下去,一直到最后一个,开始返回执行”next”关键字下面的代码,然后又一层层的传递回来。实现了一种先进入各种操作,之后再出来再各种操作,相当于每个中间件都有个前置代码区和后置代码区。这就是面向切面编程的概念。
执行过程如下图:
所以我们才把respond放在了中间件最前面。
这其实是之前connect的大致实现方式,通过这种尾触发的机制,实现这种顺序流机制。
使用generator和co改进
我们的主要目的是探讨koa的实现。我们需要做的是使用generator和co对上面做些改进。
我们希望这样,每个中间件都是一个generatorFunction。有了co的支持后,在中间件里面我们可以直接使用yield,操作各种异步任务,可以直接yield下一个中间件generatorFunction的generator对象。实现顺序流机制。
如果实现了,我们以respond为例改造:
1 2 3 4 5 6 | function *respond(next){ console.log("start app...."); yield next; this.res.writeHead(200, {'Content-Type': 'text/plain'}); this.res.end(this.context.body); } |
respond本身变为一个generatorFunction,我们只需要通过yield next去调用下一个中间件。在这个中间件里面,我们可以随意使用co提供的异步操作机制。
要实现这个,我们只需要对compose做一个改造:
1 2 3 4 5 6 7 8 9 10 11 12 | require "co" function compose(handlelist,ctx) { return co(function * () { var prev = null; var i = handlelist.length; while (i--) { prev = handlelist[i].call(ctx, prev); } yield prev; }) } |
compose仍然用来返回一个启动函数。
我们首先对中间件队列从后遍历,挨个的获取对应的generator对象,同时将后面的generator对象传递给前面中间件的generatorFunction。这样就形成了一个从前往后的调用链,每个中间件都保存着下一个中间件的generator的引用。
最后我们使用co生成一个启动函数。
1 2 3 | co(function *(){ yield gen; }) |
通过前面的co的源码分析,我们知道co接收一个generatorFunction,生成一个回调函数,执行这个回调函数就会开始执行里面的yield。这个回调函数显然就是个启动函数。当co引擎遇到yield gen;
的时候,又会开始执行这个gen的代码,一个个的执行下去。实现切面编程。
在koa的源码里,其实不是
yield gen;
而是yield *gen;
其实功能是一样的,差别在于前者是co引擎支持的,后者是es6的generator规范原生支持的。原生的在某些情况下性能更好,koa官方是不推荐在中间件里面直接使用yield *next;
的,直接使用yield next;
,co会为你完成一切。
全部代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | var co = require('co'); var http = require('http'); function Application() { this.context = {}; this.context['res'] = null; this.middleware = []; } var app = Application.prototype; function compose(handlelist,ctx) { return co(function * () { var prev = null; var i = handlelist.length; while (i--) { prev = handlelist[i].call(ctx, prev); } yield prev; }) } function *respond(next) { console.log("start app...."); yield next; this.res.writeHead(200, { 'Content-Type': 'text/plain' }); this.res.end(this.body); } app.use = function(fn) { //this.do = fn; this.middleware.push(fn) } app.callback = function() { var fn = compose.call(this, [respond].concat(this.middleware),this.context); var that = this; return function(req, res) { that.context.res = res; fn.call(that.context); //respond.call(that.context); } } app.listen = function() { var server = http.createServer(this.callback()); return server.listen.apply(server, arguments); }; //调用 var appObj = new Application(); appObj.use(function *(next) { this.body = "hello world!"; yield next; }) appObj.use(function *(next) { this.body += "by me!!"; }) appObj.listen(3000); |
结语
整个koa分析系列到这就完了,koa必将成为未来流行的框架之一,目前我们部门已经尝试着在一些地方使用了。node还不成熟,koa更是一种前瞻性的东西,但是总要有人去尝试才行。技术日新月异,前端再也不是只会切切页面就行了。