@cherishpeace
2014-05-24T11:52:42.000000Z
字数 7219
阅读 2120
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();//添加中间件1app.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);});//添加中间件2app.use(function *(){console.log("start=======2222");this.body = 'Hello World';console.log("end=======2222");});app.listen(3000);/*start=======1111start=======2222end=======2222end=======1111GET / - 10start=======1111start=======2222end=======2222end=======1111GET /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更是一种前瞻性的东西,但是总要有人去尝试才行。技术日新月异,前端再也不是只会切切页面就行了。