初识Koa

Published: by Creative Commons Licence

初识Koa

第一部分 Generator

Koa是一个小巧简单的web框架,由Express开发团队开发的新的web框架。

为什么是Koa

Koa的新特性让你开发web应用时变得简单而迅速。它采用了ES6的新特性generators,从而使控制流的管理变得十分简单。Koa本身非常小巧,相比一些当今流行的web框架(比如Express),Koa采用了极致模块化的思想,每个模块做好一件事情。下面是一个使用Koa的例子:

var koa = require('koa');
var app = koa();

app.use(function *() {
  this.body = 'Hello World';
});

app.listen(3000);

为了能够运行上面这段代码,确保你的node版本是0.11.9及以上,运行时加上--harmony选项。注意到上面这段代码比较特殊的就是在function旁边多了个星号*,这就代表该函数是一个generator函数。

Generators

有了generators,你就可以在你的function里面的任何地方暂停执行,然后去干点别的事情,干完后回来继续往下执行。对的,非常棒的特性。

下面就来定义一个generator函数,就是在函数名的左边加上一个星号*就可以了,像下面这样:

function *foo () { }

这样就定义了一个generator函数。当调用该函数时,它会返回一个iterator对象。不像普通的函数,当我们执行一个generator函数时,里面的代码并不会开始执行,我们需要手动来执行里面的代码。

function *foo (arg) { }  // generator function
var bar = foo(123);      // iterator  object

执行完foo(123)之后,返回了一个generator对象bar,我们可以手动来遍历执行generaotr里面的代码。通过调用bar.next()方法就可以让里面的代码真正地开始执行起来,直到遇到暂停点。

bar.next()会返回一个对象,该对象包含了generator的状态信息,其中的value属性代表的是当前迭代对象的值,也就是暂停点返回的值,另一个属性done是一个boolean类型,表明该generator里面的代码是否已经执行完。

function *foo (arg) { return arg; }
var bar = foo(123);
bar.next();          // { value: 123, done: true }

在上面的例子中,foo generator函数中并没有任何暂停点,而是一条renturn语句。所以bar.next()的返回结果中done属性的值为true,表明foo generator已经执行完毕。如果你在generator函数中return一个值,那么这个值也会赋值给最后的迭代对象,所以bar.next()返回的迭代对象的值等于123。那么如何才能在一个generator函数中暂停于某处呢,yield就是为此而生。

yield

yield [[expression]];

通过执行next(),可以让generator里面的代码开始执行,直到遇到yield。然后它会返回一个包含valuedone的对象,value的值等于yield后面的表达式返回的值,yield后面可以是任何表达式(包括函数等等)。

function* foo () {
  var index = 0;
  while (index < 2) {
    yield index++;
  }
}
var bar =  foo();

console.log(bar.next());    // { value: 0, done: false }
console.log(bar.next());    // { value: 1, done: false }
console.log(bar.next());    // { value: undefined, done: true }

当我们一次次执行next()的时候,跟在yield后面表达式的值将会被返回。当然next()也可以接受参数,比如next(val),val值将会被传回,赋值给yield前面的变量。例如下面这段代码:

function* foo () {
  var val = yield 'A';
  console.log(val);           // 'B'
}
var bar =  foo();

console.log(bar.next());    // { value: 'A', done: false }
console.log(bar.next('B')); // { value: undefined, done: true }

Error 处理

如果在执行generator函数的时候遇到错误,尽管调用generator的throw方法,抛出的错误可以在generator函数中通过try catch来捕获处理。例如下面这段代码:

function *foo () {
  try {
    x = yield 'asd B';   // Error will be thrown
  } catch (err) {
    throw err;
  }
}

var bar =  foo();
if (bar.next().value == 'B') {
  bar.throw(new Error("it's B!"));
}

for…of

在ES6中,我们可以使用for..of循环来迭代一个generator。迭代会一直进行,直到done等于ture。例如下面这段代码:

function *foo () {
  yield 1;
  yield 2;
  yield 3;
}

for (v of foo()) {
  console.log(v);
}

上面这段代码会遍历输出1、2、3。

yield *

上面说过yield后面可以是任何类型的表达式,甚至是generator。如果yield后面接的是另一个generator,那么就必须多加一个星号 yield *generator。这种方式称之为授权,在一个generator函数中将执行权利交给了另一个generator。例如下面这段代码:

function *bar () {
  yield 'b';
}

function *foo () {
  yield 'a';
  yield *bar();
  yield 'c';
}

for (v of foo()) {
  console.log(v);
}

将会先打印a,然后进入bar generator,打印b,接着又回到foo generator,打印c。

Thunks

为了理解Koa,理解thunks是必不可少的一步。thunk其实就是包装了一下原函数,然后返回另一个函数。例如如下代码:

var read = function (file) {
  return function (cb) {
    require('fs').readFile(file, cb);
  }
}

read('package.json')(function (err, str) { })

上面的read函数就是一个thunk函数。使用thunk函数的目的是为了将node的callback从函数参数列表中移出来,放在外面给传进去。例如上面的read函数,普通的node风格的函数调用如下:

function read(file, cb) {
    require('fs').readFile(file, cb);
}

通过thunk改造后,我们将cb从参数列表中移了出来,放在外面。thunkify可以帮助我们实现从普通函数到thunk函数的转化。首先将普通的node函数转化为thunk风格的函数,然后我们在generator函数中调用该thunk风格的函数。当我们调用next()的时候,它返回的对象的value属性的值将会是一个函数,它的参数就是thunk函数所需要的callback。在这个callback中我们可以检查错误(适时抛出错误),或者继续调用next(data)。如下所示:

var thunkify = require('thunkify');
var fs = require('fs');
var read = thunkify(fs.readFile);

function *bar () {
  try {
    var x = yield read('input.txt');
  } catch (err) {
    throw err;
  }
  console.log(x);
}
var gen = bar();
gen.next().value(function (err, data) {
  if (err) gen.throw(err);
  gen.next(data.toString());
})

Koa正是基于上面的原理来实现异步流程的控制。通过这种方式可以实现同步代码的书写风格,但背后是异步执行,同时保持良好的错误处理,简直太棒了!