半小时掌握koa

半小时掌握koa

<h3 id="koa分析与实现">koa分析与实现</h3>

<h4 id="example">1.从一个例子开始</h4>

'use strict';

let Application = require('koa');

let app = new Application();

app.use(function*(next) {
  console.log('东方红');
  yield next;
  console.log('太阳升');
  console.log();
});

app.use(function*(next) {
  console.log('-1979年,');
  yield next;
  console.log('-那是一个春天');
});

app.use(function*() {
  let message = '--我爱北京,天安门';
  console.log(message);
  this.body = 'hello world';
});

const PORT = 8888;
app.listen(PORT);

console.log('start at ' + PORT);

上面代码,先后添加了3个middleware。请求的结果如下:
<pre>
东方红
-1979年,
--我爱北京,天安门
-那是一个春天
太阳升
</pre>

<h4 id="abstract">2.koa之流程</h4>

GitHub
GitHub
  • 每一个请求到达服务器后,初始化请求上下文对象
  • 将上下文对象按照顺序,在每个中间件中运行一遍
    • 执行中间件1,next前的逻辑
    • next
      • 执行中间件2的逻辑
    • 执行中间件1,next后的逻辑
  • 将body返回给客户端

上面的过程就是创建http服务器的时候,callback参数应该做的事情。

<h4 id="appObjectAnalyse">3.app对象分析</h4>

  • 属性:由于这里只是简单的山寨一下koa,所以只需要一个middleware数组,用来存储中间件。如果感兴趣,想完善的话可以添加其他的属性,实现相关功能。
  • 公共方法:从开始的例子可以轻松看出有如下2个方法。
    • use
    • listen
  • 私有方法:
    • callback:创建http服务器的时候需要提供的回调函数
    • response:相应函数

<h4 id="callback">4.callback实现</h4>

先从callback下手。因为,解决了这个函数,其他函数的实现就是轻松加愉快。
不过,开始我们就会发现一个问题。回调函数是node底层调用的,我们没有办法把middleware参数传递进。
这个时候,闭包玩的比较熟练的同学也许就会心中窃喜了。
没错解决这个问题的常用手段就是使用高阶函数,利用js的闭包这一特性。

function callback(middleware) {
    return function (req, res) {
    }
}

解决了上边的问题,我们顺着流程写就好了。

首先构造上下文对象

function callback(middleware) {
    return function (req, res) {
        let ctx = {
          req: req,
          res: res
        };
    }
}

之后需要做的事情就是,顺序调用每一个middleware。但是我们手中有的数据结构只是一个generator function array,显然完成不了任务。因此,考虑将它map一下,让它变成一个类似function链表一样的东西。

代码如下:

let next = function*(){};

var i = middleware.length;
while (i--) {
    next = middleware[i].call(ctx, next);
}

ps: 这里有一点不知道大家发现了没有!!在每一个请求到来的时候,都需要进行一次这种map。也就是说,挂载的中间件越多,性能就会越差。我觉得这也是,koa1在性能上比express差的一个原因吧。 性能测试

接着,调用一下链表头的function,就可以实现middleware的调用。

function callback(middleware) {
  return function (req, res) {
    let ctx = {
      req: req,
      res: res
    };
    co(function *() {
      let next = function*(){};

      var i = middleware.length;
      while (i--) {
        next = middleware[i].call(ctx, next);
      }
      return yield next;
    })
  }
}

最后,就是将上下文的body返回给前端。

function callback(middleware) {
  return function (req, res) {
    let ctx = {
      req: req,
      res: res
    };
    co(function *() {
      let next = function*(){};

      var i = middleware.length;
      while (i--) {
        next = middleware[i].call(ctx, next);
      }
      return yield next;
    }).then(() => response.call(ctx))
      .catch(e => console.log(e.stack));
  }
}

以上就是整个callback的代码,不过错误处理比较粗糙。大家可以自己完善一下-

<h4 id="otherFn">5.其他function</h4>

其他function就比较简单了。分别写在下面

app.listen = function () {
  var server = http.createServer(callback(this.middleware));
  return server.listen.apply(server, arguments);
};
app.use = function (fn) {
  this.middleware.push(fn);
};
function response() {
  this.res.writeHead(200, {
    'Content-Type': 'text/plain'
  });
  this.res.end(this.body);
}

最后整个myKoa的代码

'use strict';

let co = require('co');
let http = require('http');

function Application() {
  this.middleware = [];
}

var app = Application.prototype;


app.listen = function () {
  var server = http.createServer(callback(this.middleware));
  return server.listen.apply(server, arguments);
};

app.use = function (fn) {
  this.middleware.push(fn);
};


function callback(middleware) {
  return function (req, res) {
    let ctx = {
      req: req,
      res: res
    };
    co(function *() {
      let next = function*(){};

      var i = middleware.length;
      while (i--) {
        next = middleware[i].call(ctx, next);
      }
      return yield next;
    }).then(() => response.call(ctx))
      .catch(e => console.log(e.stack));
  }
}

//------private function
function response() {
  this.res.writeHead(200, {
    'Content-Type': 'text/plain'
  });
  this.res.end(this.body);
}

module.exports = Application;
   

<h3 id="koaCompoment">koa与他的小伙伴</h3>

<h4 id="koa-router">koa-router</h4>

<h5 id="router-memory">1.内存模型</h5>

GitHub
GitHub

上图是一个简化的koa-router的内存快照。
1个router最主要的属性就是stack,他是一个layer的数组。每当我们调用一次router.verb或者router.use就会有一个layer被创造出来,放到这个数组中。
如上图所示,每个layer主要有4个属性。
请大家,务必记住这个内存模型,后文的讨论都会围绕这张图进行。

<h5 id="router-flow">2.请求处理流程</h5>

在上一部分曾经说过,koa的每一个中间件都是一个generator function,koa-router也不能免俗。别看他代码比koa要多,但是他就是一个函数而已。
接下来我们就看一下当请求到达服务器后这个函数都干了些什么。

  • 首先便利router的stack数组,通过每一个layer的正则版本的路径检查当前请求的path是否符合这则表达式。
  • 通过了第一步的检查的layer,进入了第二阶段的筛选。这个阶段的标准是layer的methods属性,methods为空数组或者methods数组中有当前请求的verb便通过筛选。
  • 通过筛选的layer会被放入一个叫做pathAndMethod的数组中
  • 我们下载可以看下下我们又有的数据结构-->一个fn*的二维数组。是否有种似曾相识的感觉囊。那么,下一步就是讲数组变成fn链表,然后调用一下开头的fn。

以上就是koa-router的整个流程。

<h5 id="router.verb">3.verb</h5>
相信大家对于这个方法的用法应该很熟悉了吧,我就在这里不多说了。
这里,我想强调的是。每次使用这个方法的时候就会增加一个layer。也就是说筛选合适的layer的操作消耗的时间就会变多。在设计跟优化程序的时候应该注意这个特征。
另外,推荐一种性能更好的使用方法

//method1
router.get('/blabla',fn1,fn2,...)

//method2
router.use('/blabla',fn1)
router.get('/blabla',fn2)

method1和2可以实现相同的功能,但是method1只会创建一个layer,这个layer会有两个middleware。不难发现method1的效率会更高一些。
好了verb就说到这了吧。

<h5 id="router.use">3.use</h5>
关于这个方法,我想说的是,use不但可以use一个自己写的中间件,而且还可以use一个router。这种方法可以让我们实现用文件夹定义url的效果,我个人觉得这是一种有没得方法。

var forums = new Router();
var posts = new Router();

posts.get('/', function *(next) {...});
posts.get('/:pid', function *(next) {...});
forums.use('/forums/:fid/posts', posts.routes(), posts.allowedMethods());

// responds to "/forums/123/posts" and "/forums/123/posts/123"
app.use(forums.routes());

我想,也许有人会好奇这个功能是怎么实现的吧,当时看到api文档的时候我也很好奇。下面我就来说一下吧。
直接上源代码:

middleware = middleware.filter(function (fn) {
    if (fn.router) {
      fn.router.stack.forEach(function (layer) {
        if (path) layer.setPrefix(path);
        if (router.opts.prefix) layer.setPrefix(router.opts.prefix);
        router.stack.push(layer);
      });

      if (router.params) {
        Object.keys(router.params).forEach(function (key) {
          fn.router.param(key, router.params[key]);
        });
      }

      return false;
    }

    return true;
  });

从上面可以很轻易的看出,作者是先检测了一下这个fn,发现如果是router,那么读取这个router的所有layer,把每一个layer设置一下前缀,然后直接放到父router的stack中。机智吧-.

<h5 id="router.param">3.param </h5>
最后要说的是param这个方法。

router
  .param('user', function *(id, next) {
    this.user = users[id];
    if (!this.user) return this.status = 404;
    yield next;
  })
  .get('/users/:user', function *(next) {
    this.body = this.user;
  })
  .get('/users/:user/friends', function *(next) {
    this.body = yield this.user.getFriends();
  })
  // /users/3 => {"id": 3, "name": "Alex"}
  // /users/3/friends => [{"id": 4, "name": "TJ"}]

当时,看到这段代码的时候,确实激动了一把。如作者说的,这个功能可以很方便的实现auto-loading作者是validation。
但是,但是,但是!!!!!!!同志们一定要注意,这里面有一个小小的坑。
闲言碎语不要说,我们直接上代码

var app = require('koa')();
var router = require('koa-router')();
       
router
  .get('/test/:id', function *() {
    console.log('get', this.test);
    this.body = 'hello world';
  })
  .use('/test/:id', function*(next) {
    console.log('use', this.test)
    yield next;
  })
  .param('id', function * (id, next) {
    this.test = this.test || 0;
    this.test = this.test + id;
    yield next;
  });

app.use(router.routes())
  .use(router.allowedMethods());

app.listen(8888);

/**
输出结果
use 01
get 011
**/

相信眼尖的通知们已经知道发生了什么!没错,param对应的fn执行了两遍,这是因为param会把中间件放到layer的middleware数组中。
好了,虽然有点小小的瑕疵,但是,用的时候注意就好了。这个功能还是很好用的,吼吼吼。

以上就是koa-router我想说的全部内容,至于怎么用的话,看一下api文档就好了,我只能说作者设计的很简洁,一切说明都是废话。

希望我也能早日设计出这么有没得api。

<h4 id="koa-body">koa-body</h4>

这个组件使用起来也是非常方便的,在这里说一下的目的是,由于前一阵子公司使用的body parse用起来难用的不要不要的。所以在这里推荐一下吧。

下面的内容基本就是readme的翻译,没兴趣的可以直接不看。

<h5>1.简介</h5>
koa-body支持multipart, urlencoded 和 json 请求体,提供跟express的multer一样的功能。他是对co-body和formidable的封装。

<h5>2.使用方法</h5>
koa-body可以向multer一样使用,非常简单,因为你可以从ctx.request.body 或者 ctx.req.body 中获得fields和files

var app      = require('koa')(),
    koaBody   = require('koa-body');

app.use(koaBody({formidable:{uploadDir: __dirname}}));
app.use(function *(next) {
  if (this.request.method == 'POST') {
    console.log(this.request.body);
    // => POST body
    this.body = JSON.stringify(this.request.body);
  }
  yield next;
});
app.listen(3131)

koa-body还可以跟koa-router一起使用

var app     = require('koa')(),
    router  = require('koa-router')(),
    koaBody = require('koa-body')();

router.post('/users', koaBody,
  function *(next) {
    console.log(this.request.body);
    // => POST body
    this.body = JSON.stringify(this.request.body);
  }
);

app.use(router.routes());

<h5>3.参数</h5>

  • patchNode bool 把request的body给node的ctx.req,默认为false。
  • patchKoa bool 把request的body给koa的ctx.req,默认为false。
  • jsonLimit String|Integer json body的最大byte数,默认为1mb
  • formLimit String|Integer form body的最大byte数,默认为56kb
  • textLimit String|Integer text body的最大byte数,默认为56kb
  • encoding String 设置field的编码,默认为utf-8
  • multipart Boolean 是否解析multipart body,默认为false
  • formidable Object 床底给formidable的设置项
  • strict bool 如果激活,koa-body不会解析GET,HEAD,DELETE请求,默认为true
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容