koa源码分析系列(三)koa的实现
发布在js从零单排2015年1月28日view:2705buy_cialisJavaScript
在文章任何区域双击击即可给文章添加【评注】!浮到评注点上可以查看详情。

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的第三方基础库,更加方便。这样你写中间件的时候没必要到处安装依赖库。

使用方式:

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;代表调用下一个中间件的代码。

对于上面的例子。 一个请求会先执行第一个中间件的:

 var start = new Date;
 console.log("start=======1111");

遇到yield next;的时候会转过去执行后来的中间件的代码也就是:

console.log("start=======2222");
this.body = 'Hello World';
console.log("end=======2222");

等下一级中间件执行完毕后才会继续执行接下来的:

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网页。

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/');

官方标准例子,相当简单。不过毫无扩展性。

简单改良

我们进行下改良:

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);

咋看一下,这么多代码,感觉好复杂,但是应该注意到的是我们实际使用时只要写:

function(){
    this.body = "hello world!";
}

我们称之为中间件。

解释下上面这段代码,appObj.listen的时候调用http.createServer创建一个server实例。通过this.callback()得到一个标准回调函数。callback是一个高阶函数,返回一个新的执行函数。在执行函数里,我们首先将http请求的res对象保存下来。之后调用存储的this.do函数。this.do函数就是我们之前使用appObj.use添加的,也就是我们的中间件函数。最后调用respond。在respond里我们完成通用的处理代码。

使用中间件队列

当然 我们这个还不完善,作为中间件应该可以添加多个,并且顺序执行。 我们需要一种机制,实现上面说的面向切面编程的效果。我们做一些改进:


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的实现:

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为例改造:

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做一个改造:

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生成一个启动函数。

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会为你完成一切。

全部代码如下:

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

评论
发表评论
2年前

作者思路清晰,知识渊博,对于很多技术细节有很深入的理解,赞一个!

2年前
赞了此文章!
WRITTEN BY
竹隐
前端。。。
TA的新浪微博
PUBLISHED IN
js从零单排

js从零单排

我的收藏