使用 Koa 从零打造 TODO 应用

原文地址:Koa: Zero to Todo List

###注意:你需要使用node 0.11.x外加 -harmony 来执行代码

Express 团队利用新的 ECMAScript 6 的生成器语法创建了新的框架,Koa 框架是一个全新的 node web 框架,包含了很多有意思的东西。 ##之前的方式

在 node 标准库里,http 模块被用来创建服务。

var server = http.createServer(function(req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  //这里写服务逻辑
  res.end('');
});

server.listen(3000, '127.0.0.1');
console.log('listening on port 3000');

Express 暴露一个方法使我们可以将 http.createServer 作为回调。Express 中间件是一个函数集合,每个函数包含了三个参数 req,res,next。中间件执行一些操作,修改请求或者返回对象然后通过调用 next() 来传递到堆栈里的下一个中间件。它类似一个瀑布模型,在中间件栈的底部结束响应。

##进入 Koa:建立在生成器机制上的框架

就像 Express,Koa 也是生成一个可以被传递到 http.createServer() 的回调。与 Express 不同的是,它使用生成器提供一个更加细粒度的控制流程。

下面是一个最基本的 Koa 应用,用来读取一个文件的内容

var koa      = require('koa');
var Promise  = require('bluebird');

//创建 promise 版本的 fs
var fs = Promise.promisifyAll(require('fs'));
//创建 koa 实例
var app = koa();

app.use(function *(next) {
  //这是一个示例中间件,在控制台记录一些东西
  console.log('timestamp: before request => ', time.now());
  yield next;
  console.log('timestamp: after request => ', time.now());
});

app.use(function *() {
  this.body = yield fs.readFileAsync('./app.js', 'utf8');
});

app.listen(3000);
console.log('now listening on port 3000');

不像 Express,Koa 中中间件使用生成器来编写。在 Koa 流中下游的中间件在返回时向上流动(回形针调用方式,具体参见:koajs.cn)。通过显式的调用 yield next 来执行下游中间件。当下游中间件返回时,控制流回溯到上游中间件。

Express 通过不同的函数来传递 node 原生的 req 和 res,Koa 则是通过讲它们装入一个借口来管理上下文。不过它们仍然可以通过 this 关键字获取到,像这样:this.req, this.res。然而,在文档中直接使用原生对象是不被推荐的。可以预测到当在控制流中调用 this.res.end('') 时会抛出一个 monkey wrench(猴子扳手?此处不会翻译欢迎指正)。所以建议你使用 this.requestthis.response 来代替直接调用原生对象。很多方法都起了别名指向直接用 this 调用,比如:this.body 就是 this.response.body 的别名。

目前似乎还没有出现可以直接得到请求体的办法。co-body 分析器可以直接的解析请求体,不过文档说别这么做,Koa 是一个年轻的框架,所以别让你的手闲下来。

##使用 Koa 做一个 TODO 应用

刚才我们已经简单的进行了介绍,现在来试着做一个复杂点的。一个 TODO 应用貌似不错,为了简化,我们把 todos 存放在内存里。

Koa 是一个极简的框架,它核心里并没有提供 body 解析,session 和 routing。不幸的是 Koa 太嫩了以至于还没有很多 npm 的模块是为它来写的。浏览了一下 Koa 介绍页面发现有一些必要的模块可以供给我们的基本 TODO 应用来使用。

  1. koa-route: 用作路由
  2. co-body: 用作解析 post 请求体
  3. koa-static: 用于处理静态文件

下面是基本的服务端 api

var koa          = require('koa');
var staticServer = require('koa-staitc');

//这个允许我们解析原生请求对象来获取请求内容
var parse        = require('co-body');

var router       = require('koa-route');
var _            = require('underscore');

var Promise      = require('bluebird');
var path         = require('path');

var fs           = Promise.promisifyAll(require('fs'));
var app          = koa();

//我们的最简单的存储方式
var todos = [];

//获取唯一的 id 值
var counter = (function() {
  var count = 0;
  return function() {
    count++;
    return count;
  }
})();

//处理静态资源文件夹
app.use(staticServer(path.join(__dirname, 'public')));

app.use(router.post('/todos', function *() {
  /*
    yield使我们可以传递异步函数,然后返回内容或者是 promises
    它会冻结当前中间件直到函数被执行完成,然后返回当前中间件继续解冻执行
  */
  var todo = (yield parse.json(this));
  
  todo.id = counter();
  todos.push(todo);
  this.body = JSON.stringify(todos);
}));

app.use(router.get('/todos', function *() {
  this.body = JSON.stringify(todos);
}));

app.use(router.delete('/todos/:id', function *(id) {
  todos = _(todos).reject(function(todo) {
    console.log('what? ', todo, id);
    return todo.id === parseInt(id, 10);
  }, this);
  this.body = JSON.stringify(todos.sort(function(a, b) {
    return a - b;
  }));
}));

app.listen(3000);
console.log('listening on port 3000');

github 上下载完整代码,github 上的版本包含了前端代码。

###一些需要注意的:

yield 关键字可以做一些有意思的事情。如果我们向当前中间件传递一个一步函数,这个函数返回数据块或者 promise,那么它会停止执行当前中间件直到函数完成。等它返回数据块或者 promise 后,会恢复生成器执行。这样更容易阅读。

###一些警告:

yield 关键字使我们可以写出一些安全的代码块,但它也不总是理想的解决办法。

举个栗子,如果我们执行三个相互不依赖的异步操作,像下面这样…

app.use(function *() {
  var a = yield async1();
  var b = yield async2();
  var c = yield async3();
});

这会使 node 的并发失效。当我们调用 async1,我们必须等待 async1 完成才能执行 async2。不过我们可以用 promise 来优化这3个函数,然后生成一个合并的 promise。

app.use(function *() {
  var a = async1();
  var b = async2();
  var c = async3();
  var result = yield Promise.all([a, b, c]);
});

注意:tjholowaychuk 大神在原文留言指出了一些问题,见下面图

当 Koa 框架成熟时,它将会允许更加细粒度的控制以便于我们写出下一代的 web 应用。

TJ