最近尝试用了一下Koa,并在此记录一下使用心得。

  注意:本文是以读者已经了解Generator和Promise为前提在写的,因为单单Generator和Promise都能够写一篇博文来讲解介绍了,所以就不在这里赘述。网上资料很多,可以自行查阅。

  Koa是Express原班人马打造的一个更小,基于nodejs平台的下一代web开发框架。Koa的精妙之处就在于其使用generator和promise,实现了一种更为有趣的中间件系统,Koa的中间件是一系列generator函数的对象,执行起来有点类似于栈的结构,依次执行。同时也类似于Python的django框架的中间件系统,以前苏千大神做分享的时候把这种模型称作为洋葱模型。如图:

  

  当一个请求过来的时候,会依次经过各个中间件进行处理,中间件跳转的信号是yield next,当到某个中间件后,该中间件处理完不执行yield next的时候,然后就会逆序执行前面那些中间件剩下的逻辑。直接上个官网的例子:

  1. var koa = require('koa');
  2. var app = koa();
  3.  
  4. // response-time中间件
  5. app.use(function *(next){
  6. var start = new Date;
  7. yield next;
  8. var ms = new Date - start;
  9. this.set('X-Response-Time', ms + 'ms');
  10. });
  11.  
  12. // logger中间件
  13. app.use(function *(next){
  14. var start = new Date;
  15. yield next;
  16. var ms = new Date - start;
  17. console.log('%s %s - %s', this.method, this.url, ms);
  18. });
  19.  
  20. // 响应中间件
  21. app.use(function *(){
  22. this.body = 'Hello World';
  23. });
  24.  
  25. app.listen(3000);

  上面的执行顺序就是:请求 ==> response-time中间件 ==> logger中间件 ==> 响应中间件 ==> logger中间件 ==> response-time中间件 ==> 响应。

  更详细描述就是:请求进来,先进到response-time中间件,执行 var start = new Date; 然后遇到yield next,则暂停response-time中间件的执行,跳转进logger中间件,同理,最后进入响应中间件,响应中间件中没有yield next代码,则开始逆序执行,也就是再先是回到logger中间件,执行yield next之后的代码,执行完后再回到response-time中间件执行yield next之后的代码。

  至此,整个Koa的中间件执行完毕 ,整个中间件执行过程相当有意思。

  而Koa的中间件是运行在 co 函数下的,而tj大神的co函数能够把异步变同步,也就说,编写Koa的中间件的时候可以这样写,就拿上面那个demo最后的响应中间件来说可以改成这样:

  1. app.use(function*(){
  2. var text = yield new Promise(function(resolve){
  3. fs.readFile('./index.html', 'utf-8', function(err, data){
  4. resolve(data);
  5. })
  6. });
  7.  
  8. this.body = text;
  9. });

  通过Promise可以把获取的文件数据data通过resolve函数,传到最外层的text中,而且,整个异步操作变成了同步操作。

  再比如使用mongodb做一个数据库查询功能,就可以写成这样,整个数据的查询原来是异步操作,也可以变成了同步,因为mongodb官方驱动的接口提供了返回Promise的功能,在co函数里只用yield的时候能够直接把异步变成同步,再也不用写那恶心的回调嵌套了。

  1. var MongoClient = require("mongodb").MongoClient;
  2. app.use(function *(){
  3. var db = yield MongoClient.connect('mongodb://127.0.0.1:27017/myblog');
  4.  
  5. var collection = db.collection('document');
  6.  
  7. var result = yield collection.find({}).toArray();
  8.  
  9. db.close()
  10. });

  tj的co函数就如同一个魔法,把所有异步都变成了同步,看起来好像很高大上。但是co函数做的事其实并不复杂。

  整个co函数说白了,就是使用Promise递归调用generator的next方法,并且在后一次调用的时候把前一次返回的数据传入,直到调用完毕。而co函数同时把非Promise对象的function、generator、array等也组装成了Promise对象。所以可以在yield后面不仅仅可以接Promise,还可以接generator对象等。

  自己实现了一个简单的co函数,传入一个generator,获取generator的函数对象,然后定义一个next方法用于递归,在next方法里执行generator.next()并且传入data,执行完generator.next()会获取到{value:XX, done: true|false}的对象,如果done为true,说明generator已经迭代完毕,退出。

  否则,假设当前执行到yield new Promise(),也就是返回的result.value就是Promise对象的,直接执行Promise的then方法,并且在then方法的onFulfilled回调(也就是Promise中的异步执行完毕后,调用resolve的时候会触发该回调函数)中执行next方法进行递归,并且将onFulfilled中传入的数据传入next方法,也就可以在下一次generator.next()中把数据传进去。

  1. // co简易实现
  2. function co(generator){
  3. var gen = generator();
  4.  
  5. var next = function(data){
  6. var result = gen.next(data);
  7.  
  8. if(result.done) return;
  9.  
  10. if (result.value instanceof Promise) {
  11. result.value.then(function (d) {
  12. next(d);
  13. }, function (err) {
  14. next(err);
  15. })
  16. }else {
  17. next();
  18. }
  19. };
  20.  
  21. next();
  22. }

  写个demo测试一下:

  1. // test
  2. co(function*(){
  3. var text1 = yield new Promise(function(resolve){
  4. setTimeout(function(){
  5. resolve("I am text1");
  6. }, 1000);
  7. });
  8.  
  9. console.log(text1);
  10.  
  11. var text2 = yield new Promise(function(resolve){
  12. setTimeout(function(){
  13. resolve("I am text2");
  14. }, 1000);
  15. });
  16.  
  17. console.log(text2);
  18. });

  运行结果:

   运行成功!

  既然了解了co函数的原理,再来说说koa的中间件是怎么实现的。整个实现原理就是把所有generator放到一个数组里保存,然后对所有generator进行相应的链式调用。

  起初是自己按照自己的想法实现了一次,大概原理如下:

  用个数组,在每次执行use方法的时候把generator传入gens数组保存,然后在执行的时候,先定义一个generator的执行索引index、跳转标记ne(也就是yield next里的next)、还有一个是用于保存generator函数对象的数组gs,。然后获取当前中间件generator,并且获取到该generator的函数对象,将函数对象放入gs数组中保存,再执行generator.next()。

  接着根据返回的value,做不同处理,如果是Promise,则跟上面的co函数一样,在其onFulfilled的回调中执行下一次generator.next(),如果是ne,也就是当前执行到了yield next,说明要跳转到下一个中间件,此时对index++,然后从gens数组里获取下一个中间件重复上一个中间件的操作。

  当执行到的中间件里没有yield next时,并且当该generator已经执行完毕,也就是返回的done为true的时候,再逆序执行,从此前用于保存generator的函数对象gs数组获取到上一个generator函数对象,然后执行该generator的next方法。直到全部执行完毕。

   整个过程就像,先是入栈,然后出栈的操作。

  1. //简易实现koa的中间件效果
  2.  
  3. var gens = [];
  4.  
  5. function use(generetor){
  6. gens.push(generetor);
  7. }
  8.  
  9. function trigger(){
  10. var index = 0;
  11. var ne = {};
  12. var gs = [],
  13. g;
  14.  
  15. next();
  16.  
  17. function next(){
  18. //获取当前中间件,传入next标记,即当yield next时处理下一个中间件
  19. var gen = gens[index](ne);
  20.  
  21. //保存实例化的中间件
  22. gs.push(gen);
  23.  
  24. co(gen)
  25. }
  26.  
  27. function co(gen, data){
  28. if(!gen) return;
  29.  
  30. var result = gen.next(data);
  31.  
  32. // 当当前的generator中间件执行完毕,将执行索引减一,获取上一级的中间件并且执行
  33. if(result.done){
  34. index--;
  35.  
  36. if(g = gs[index]){
  37. co(g);
  38. }
  39.  
  40. return;
  41. }
  42.  
  43. // 如果执行到Promise,则当Promise执行完毕再进行递归
  44. if(result.value instanceof Promise){
  45. result.value.then(function(data){
  46. co(gen, data);
  47. })
  48. }else if(result.value === ne){
  49. // 当遇到yield next时,执行下一个中间件
  50. index++;
  51.  
  52. next();
  53. }else {
  54. co(gen);
  55. }
  56. }
  57. }

  然后再写个demo测试一下:

  1. // test
  2.  
  3. use(function*(next){
  4. var d = yield new Promise(function(resolve){
  5. setTimeout(function(){
  6. resolve("step1")
  7. }, 1000)
  8. });
  9.  
  10. console.log(d);
  11.  
  12. yield next;
  13.  
  14. console.log("step2");
  15. });
  16.  
  17. use(function*(next){
  18. console.log("step3");
  19.  
  20. yield next;
  21.  
  22. var d = yield new Promise(function(resolve){
  23. setTimeout(function(){
  24. resolve("step4")
  25. }, 1000)
  26. });
  27.  
  28. console.log(d);
  29. });
  30.  
  31. use(function*(){
  32. var d = yield new Promise(function(resolve){
  33. setTimeout(function(){
  34. resolve("step5")
  35. }, 1000)
  36. });
  37.  
  38. console.log(d);
  39.  
  40. console.log("step6");
  41. });
  42.  
  43. trigger();

  运行结果:

     运行成功!

  上面的只是我自己的觉得的实现原理,但是其实koa自己的实现更精简,在看了koa的源码后,也大概实现了一下,其实就是把上面的那个co函数进行适当改造一下,然后用个while循环,把所有generator链式绑定起来,再放到co函数里进行yield即可。下面贴出源码:

  1. var gens = [];
  2.  
  3. function use(generetor){
  4. gens.push(generetor);
  5. }
  6.  
  7. // 实现co函数
  8. function co(flow, isGenerator){
  9. var gen;
  10.  
  11. if (isGenerator) {
  12. gen = flow;
  13. } else {
  14. gen = flow();
  15. }
  16.  
  17. return new Promise(function(resolve){
  18. var next = function(data){
  19. var result = gen.next(data);
  20. var value = result.value;
  21.  
  22. // 如果调用完毕,调用resolve
  23. if(result.done){
  24. resolve(value);
  25. return;
  26. }
  27.  
  28. // 如果为yield后面接的为generator,传入co进行递归,并且将promise返回
  29. if (typeof value.next === "function" && typeof value.throw === "function") {
  30. value = co(value, true);
  31. }
  32.  
  33. if(value.then){
  34. // 当promise执行完毕,调用next处理下一个yield
  35. value.then(function(data){
  36. next(data);
  37. })
  38. }
  39. };
  40.  
  41. next();
  42. });
  43.  
  44. }
  45.  
  46. function trigger(){
  47. var prev = null;
  48. var m = gens.length;
  49. co(function*(){
  50. while(m--){
  51. // 形成链式generator
  52. prev = gens[m].call(null, prev);
  53. }
  54.  
  55. // 执行最外层generator方法
  56. yield prev;
  57. })
  58. }

  执行结果也是无问题,运行demo和运行结果跟上一个一样,就不贴出来了。

  

  上面写的三个代码放在了github:

  https://github.com/whxaxes/node-test/blob/master/other/myco.js

  https://github.com/whxaxes/node-test/blob/master/other/mykoa.js

  https://github.com/whxaxes/node-test/blob/master/other/mykoa_2.js

  以及能帮助理解的文章:http://www.infoq.com/cn/articles/generator-and-asynchronous-programming/

  

Koa框架实践与中间件原理剖析的更多相关文章

  1. express框架安装及中间件原理

    本文主要介绍express中间件的原理,来应对面试. 1.安装express及初始化: npm install express-generator -g   =>   express expre ...

  2. 从头实现一个koa框架

    koajs是最流行的nodejs后端框架之一,有很多网站都使用koa进行开发,同时社区也涌现出了一大批基于koa封装的企业级框架.然而,在这些亮眼的成绩背后,作为核心引擎的koa代码库本身,却非常的精 ...

  3. koa框架异步返回值的操作(co,koa-compose)

    最近在做demo的时候使用了koa框架,自己做了一个静态服务器,首先判断访问文件是否存在,在回调函数中设置了this.body,run之后,各种404,花了N长的时间把koa-compose和co模块 ...

  4. koa中间件系统原理及koa+orm2实践。

    koa是由 Express 原班人马打造的新的web框架.套用其官方的说法:Koa 应用是一个包含一系列中间件 generator 函数的对象. 这些中间件函数基于 request 请求以一个类似于栈 ...

  5. ASP.NET Core 运行原理剖析2:Startup 和 Middleware(中间件)

    ASP.NET Core 运行原理剖析2:Startup 和 Middleware(中间件) Startup Class 1.Startup Constructor(构造函数) 2.Configure ...

  6. 深入浅出深度学习:原理剖析与python实践_黄安埠(著) pdf

    深入浅出深度学习:原理剖析与python实践 目录: 第1 部分 概要 1 1 绪论 2 1.1 人工智能.机器学习与深度学习的关系 3 1.1.1 人工智能——机器推理 4 1.1.2 机器学习—— ...

  7. 推荐《深入浅出深度学习原理剖析与python实践》PDF+代码

    <深入浅出深度学习原理剖析与Python实践>介绍了深度学习相关的原理与应用,全书共分为三大部分,第一部分主要回顾了深度学习的发展历史,以及Theano的使用:第二部分详细讲解了与深度学习 ...

  8. ASP.NET Core 运行原理剖析1:初始化WebApp模版并运行

    ASP.NET Core 运行原理剖析1:初始化WebApp模版并运行 核心框架 ASP.NET Core APP 创建与运行 总结 之前两篇文章简析.NET Core 以及与 .NET Framew ...

  9. MapReduce/Hbase进阶提升(原理剖析、实战演练)

    什么是MapReduce? MapReduce是一种编程模型,用于大规模数据集(大于1TB)的并行运算.概念"Map(映射)"和"Reduce(归约)",和他们 ...

随机推荐

  1. MySQL客户端工具 SQLyog

    我最喜欢它的History功能,把执行过的操作对应语句都输出出来,一些麻烦的语句用GUI操作后,还能保存对应的sql语句.不错! 官网:http://www.webyog.com 下面是官方的介绍:S ...

  2. 在Linux下使用gradle自动打包

    一.下载软件包 1.下载地址 wget https://dl.google.com/android/android-sdk_r24.4.1-linux.tgz wget http://services ...

  3. my_strlen()

    int my_strlen(const char* S){ int i=0; while('\0'!=*(S+i)){ i++; } return i; }

  4. proteus怎么仿真?

    加入hex 文件 双击 单片机芯片 AT89C51 弹出对话框,选择好 用keil 编译好的 hex 文件,晶振 12  即可. 运行

  5. 学习OpenStack之 (3):Devstack Screen 使用技巧

    Devstack环境中,openstack运行在一个screen中,每个service运行在一个window中.我总结的几个tips: 0. 注意需要使用screen启动用户来进行一下操作 1. 查看 ...

  6. C++11 之 override

    1  公有继承 派生类公有继承自 (public inheritance) 基类,继承包含两部分:一是函数的 "接口" (interface),二是函数的 "实现&quo ...

  7. NOIP2012借教室[线段树|离线 差分 二分答案]

    题目描述 在大学期间,经常需要租借教室.大到院系举办活动,小到学习小组自习讨论,都需要 向学校申请借教室.教室的大小功能不同,借教室人的身份不同,借教室的手续也不一样. 面对海量租借教室的信息,我们自 ...

  8. 第16章 调色板管理器_16.4 一个DIB位图库的实现(1)

    16.4.1自定义的 DIBSTRUCT结构体 字段 含义 PBYTE *ppRow ①指向位图视觉上最上面的一行像素.(不管是自下而上,还是自上而下) ②放在第一个字段,为的是后面定义宏时可方便访问 ...

  9. inverse理解

    首先术语inverse 被翻译为反转的意思.inverse 制定了关联关系中的方向. 当set的inverse属性默认情况下,hibernate会按照持久化对象的属性变化来同步更新数据库. 得到两条s ...

  10. 关于log4j的讨论

    1.LoggersLoggers组件在此系统中被分为五个级别:DEBUG.INFO.WARN.ERROR和FATAL.这五个级别是有顺序的,DEBUG < INFO < WARN < ...