[关闭]
@cherishpeace 2014-05-24T11:52:42.000000Z 字数 7219 阅读 1757

koa源码分析系列(三)koa的实现

koa是TJ大神新一代的中间件框架,本系列旨在一步一步实现koa的功能,包括下面这些。
1. koa源码分析系列(一)generator
2. koa源码分析系列(二)co的实现
3. koa源码分析系列(三)koa的中间件机制实现

环境准备

koa基于co实现,co又是使用了es6的generator特性,所以,没错这个特性支持很一般。
有下面几种办法体验generator:

koa简介与使用

koa是基于generator与co之上的新一代的中间件框架。虽然受限于generator的实现程度。。但是它的优势却不容小觑。

使用方式:

  1. var koa = require('koa');
  2. var app = koa();
  3. //添加中间件1
  4. app.use(function *(next){
  5. var start = new Date;
  6. console.log("start=======1111");
  7. yield next;
  8. console.log("end=======1111");
  9. var ms = new Date - start;
  10. console.log('%s %s - %s', this.method, this.url, ms);
  11. });
  12. //添加中间件2
  13. app.use(function *(){
  14. console.log("start=======2222");
  15. this.body = 'Hello World';
  16. console.log("end=======2222");
  17. });
  18. app.listen(3000);
  19. /*
  20. start=======1111
  21. start=======2222
  22. end=======2222
  23. end=======1111
  24. GET / - 10
  25. start=======1111
  26. start=======2222
  27. end=======2222
  28. end=======1111
  29. GET /favicon.ico - 5
  30. */

这就是官方的例子,运行后访问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. var start = new Date;
  2. console.log("start=======1111");

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

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

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

  1. console.log("end=======1111");
  2. var ms = new Date - start;
  3. console.log('%s %s - %s', this.method, this.url, ms);

说白了yield next;的作用就是我们之前提到过的delegating yield的功能,只不过这边是通过co支持的,而不是使用的原生的。

通过这种中间件机制,我们可以对一个请求的之前与之后做出处理。这种思想其实在java里面已经很出名了。java框架Spring的 Filter过滤器就是这个概念。这种编程方式叫做面向切面编程。

面向切面编程的知识这边就不详细介绍了,可以参考这篇文章,英文看不懂可以看翻译的文章。还有篇腾讯团队分享的文章也不错。

有了这种next的机制 我们只需要关心写各种中间件,就可以很容易的把应用搭建起来了。

一步一步实现koa

简单例子

首先我们写一个最简单的hello word网页。

  1. var http = require('http');
  2. http.createServer(function (req, res) {
  3. res.writeHead(200, {'Content-Type': 'text/plain'});
  4. res.end('Hello World\n');
  5. }).listen(1337, '127.0.0.1');
  6. console.log('Server running at http://127.0.0.1:1337/');

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

简单改良

我们进行下改良:

  1. var http = require('http');
  2. function Application (){
  3. this.context = {};
  4. this.context['res'] = null;
  5. }
  6. var app = Application.prototype;
  7. function respond(){
  8. this.res.writeHead(200, {'Content-Type': 'text/plain'});
  9. this.res.end(this.body);
  10. }
  11. app.use = function(fn){
  12. this.do = fn;
  13. }
  14. app.callback = function(){
  15. var fn = this.do;
  16. var that = this;
  17. return function(req,res){
  18. that.context.res = res;
  19. fn.call(that.context);
  20. respond.call(that.context);
  21. }
  22. }
  23. app.listen = function(){
  24. var server = http.createServer(this.callback());
  25. return server.listen.apply(server, arguments);
  26. };
  27. //调用
  28. var appObj = new Application();
  29. appObj.use(function(){
  30. this.body = "hello world!";
  31. })
  32. appObj.listen(3000);

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

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

我们称之为中间件。

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

使用中间件队列

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

  1. var http = require('http');
  2. function Application (){
  3. this.context = {};
  4. this.context['res'] = null;
  5. this.middleware = [];
  6. }
  7. var app = Application.prototype;
  8. var respond = function(next){
  9. console.log("start app....");
  10. next();
  11. this.res.writeHead(200, {'Content-Type': 'text/plain'});
  12. this.res.end(this.body);
  13. }
  14. var compose = function(){
  15. var that = this;
  16. var handlelist = Array.prototype.slice.call(arguments,0);
  17. var _next = function(){
  18. if((handle = handlelist.shift()) != undefined){
  19. handle.call(that.context,_next);
  20. }
  21. }
  22. return function(){
  23. _next();
  24. }
  25. }
  26. app.use = function(fn){
  27. //this.do = fn;
  28. this.middleware.push(fn)
  29. }
  30. app.callback = function(){
  31. var mds = [respond].concat(this.middleware);
  32. var fn = compose.apply(this,mds);
  33. var that = this;
  34. return function(req,res){
  35. that.context.res = res;
  36. fn.call(that.context);
  37. //respond.call(that.context);
  38. }
  39. }
  40. app.listen = function(){
  41. var server = http.createServer(this.callback());
  42. return server.listen.apply(server, arguments);
  43. };
  44. //调用
  45. var appObj = new Application();
  46. appObj.use(function(next){
  47. this.body = "hello world!";
  48. next();
  49. })
  50. appObj.use(function(){
  51. this.body += "by me!!";
  52. })
  53. appObj.listen(3000);

这样实现了可以使用use添加多个中间件的功能,并且respond我们也作为一个中间件放在了最前。为什么放在最前面在下面再分析。

use的时候我们将所有的中间件存起来。在app.callback里面通过compose对所有的中间件进行一次“编译”,返回一个启动函数fn。

我们看下compose的实现:

  1. function compose(handlelist){
  2. var that = this;
  3. var handle = null;
  4. var _next = function(){
  5. if((handle = handlelist.shift()) != undefined){
  6. handle.call(that.context,_next);
  7. }
  8. }
  9. return function(){
  10. _next();
  11. }
  12. }

compose也是一个高阶函数,它内部定义了一个_next函数,用于不停的从队列中拿中间件函数执行,并且传入_next的引用,这样每个中间件函数都可以在自己内部调用下一个中间件。compose会返回一个启动函数,就是初始调用_next()。这样一个由中间件组成的,一层层的操作就开始了。注意这边的调用顺序,一个中间件的代码,"next"关键字之前的会先执行,之后会跳入下一个中间件执行"next"关键字之前的代码,一直跳下去,一直到最后一个,开始返回执行"next"关键字下面的代码,然后又一层层的传递回来。实现了一种先进入各种操作,之后再出来再各种操作,相当于每个中间件都有个前置代码区和后置代码区。这就是面向切面编程的概念。
执行过程如下图:

泳道图

所以我们才把respond放在了中间件最前面。

这其实是之前connect的大致实现方式,通过这种尾触发的机制,实现这种顺序流机制。

使用generator和co改进

我们的主要目的是探讨koa的实现。我们需要做的是使用generator和co对上面做些改进。
我们希望这样,每个中间件都是一个generatorFunction。有了co的支持后,在中间件里面我们可以直接使用yield,操作各种异步任务,可以直接yield下一个中间件generatorFunction的generator对象。实现顺序流机制。

如果实现了,我们以respond为例改造:

  1. function *respond(next){
  2. console.log("start app....");
  3. yield next;
  4. this.res.writeHead(200, {'Content-Type': 'text/plain'});
  5. this.res.end(this.context.body);
  6. }

respond本身变为一个generatorFunction,我们只需要通过yield next去调用下一个中间件。在这个中间件里面,我们可以随意使用co提供的异步操作机制。

要实现这个,我们只需要对compose做一个改造:

  1. require "co"
  2. function compose(handlelist,ctx) {
  3. return co(function * () {
  4. var prev = null;
  5. var i = handlelist.length;
  6. while (i--) {
  7. prev = handlelist[i].call(ctx, prev);
  8. }
  9. yield prev;
  10. })
  11. }

compose仍然用来返回一个启动函数。

我们首先对中间件队列从后遍历,挨个的获取对应的generator对象,同时将后面的generator对象传递给前面中间件的generatorFunction。这样就形成了一个从前往后的调用链,每个中间件都保存着下一个中间件的generator的引用。

最后我们使用co生成一个启动函数。

  1. co(function *(){
  2. yield gen;
  3. })

通过前面的co的源码分析,我们知道co接收一个generatorFunction,生成一个回调函数,执行这个回调函数就会开始执行里面的yield。这个回调函数显然就是个启动函数。当co引擎遇到yield gen;的时候,又会开始执行这个gen的代码,一个个的执行下去。实现切面编程。

在koa的源码里,其实不是yield gen; 而是 yield *gen;其实功能是一样的,差别在于前者是co引擎支持的,后者是es6的generator规范原生支持的。原生的在某些情况下性能更好,koa官方是不推荐在中间件里面直接使用yield *next;的,直接使用yield next;,co会为你完成一切。

全部代码如下:

  1. var co = require('co');
  2. var http = require('http');
  3. function Application() {
  4. this.context = {};
  5. this.context['res'] = null;
  6. this.middleware = [];
  7. }
  8. var app = Application.prototype;
  9. function compose(handlelist,ctx) {
  10. return co(function * () {
  11. var prev = null;
  12. var i = handlelist.length;
  13. while (i--) {
  14. prev = handlelist[i].call(ctx, prev);
  15. }
  16. yield prev;
  17. })
  18. }
  19. function *respond(next) {
  20. console.log("start app....");
  21. yield next;
  22. this.res.writeHead(200, {
  23. 'Content-Type': 'text/plain'
  24. });
  25. this.res.end(this.body);
  26. }
  27. app.use = function(fn) {
  28. //this.do = fn;
  29. this.middleware.push(fn)
  30. }
  31. app.callback = function() {
  32. var fn = compose.call(this, [respond].concat(this.middleware),this.context);
  33. var that = this;
  34. return function(req, res) {
  35. that.context.res = res;
  36. fn.call(that.context);
  37. //respond.call(that.context);
  38. }
  39. }
  40. app.listen = function() {
  41. var server = http.createServer(this.callback());
  42. return server.listen.apply(server, arguments);
  43. };
  44. //调用
  45. var appObj = new Application();
  46. appObj.use(function *(next) {
  47. this.body = "hello world!";
  48. yield next;
  49. })
  50. appObj.use(function *(next) {
  51. this.body += "by me!!";
  52. })
  53. appObj.listen(3000);

结语

整个koa分析系列到这就完了,koa必将成为未来流行的框架之一,目前我们部门已经尝试着在一些地方使用了。node还不成熟,koa更是一种前瞻性的东西,但是总要有人去尝试才行。技术日新月异,前端再也不是只会切切页面就行了。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注