@cherishpeace
2014-05-24T19:52:42.000000Z
字数 7219
阅读 1888
koa是TJ大神新一代的中间件框架,本系列旨在一步一步实现koa的功能,包括下面这些。
1. koa源码分析系列(一)generator
2. koa源码分析系列(二)co的实现
3. koa源码分析系列(三)koa的中间件机制实现
koa基于co实现,co又是使用了es6的generator特性,所以,没错这个特性支持很一般。
有下面几种办法体验generator:
koa是基于generator与co之上的新一代的中间件框架。虽然受限于generator的实现程度。。但是它的优势却不容小觑。
使用方式:
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的机制 我们只需要关心写各种中间件,就可以很容易的把应用搭建起来了。
首先我们写一个最简单的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的大致实现方式,通过这种尾触发的机制,实现这种顺序流机制。
我们的主要目的是探讨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更是一种前瞻性的东西,但是总要有人去尝试才行。技术日新月异,前端再也不是只会切切页面就行了。