此博客不再维护,博客已迁移至 https://github.com/purplebamboo/blog/issues
文章目录
  1. 1. koa简介与使用
  2. 2. 一步一步实现koa
    1. 2.1. 简单例子
    2. 2.2. 简单改良
    3. 2.3. 使用中间件队列
    4. 2.4. 使用generator和co改进
  3. 3. 结语

koa是TJ大神新一代的中间件框架,本系列旨在一步一步实现koa的功能,包括下面这些。

  1. koa源码分析系列(一)generator
  2. koa源码分析系列(二)co的实现
  3. koa源码分析系列(三)koa的中间件机制实现
  4. koa源码分析系列(四)co-4.0新变化

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更是一种前瞻性的东西,但是总要有人去尝试才行。技术日新月异,前端再也不是只会切切页面就行了。

文章目录
  1. 1. koa简介与使用
  2. 2. 一步一步实现koa
    1. 2.1. 简单例子
    2. 2.2. 简单改良
    3. 2.3. 使用中间件队列
    4. 2.4. 使用generator和co改进
  3. 3. 结语