koa中间件机制和错误处理解决方案

由 Express 原班人马打造的 koa,致力于成为一个更小、更健壮、更富有表现力的 Web 框架。使用 koa 编写 web 应用,通过组合不同的 generator,可以免除重复繁琐的回调函数嵌套,并极大地提升常用错误处理效率。Koa 不在内核方法中绑定任何中间件,它仅仅提供了一个轻量优雅的函数库,使得编写 Web 应用变得得心应手。

我觉得其实koa主要就是避免了繁琐的回调函数嵌套,代码看着更和谐。还有就是它的错误处理机制。

其实这几天一直在纠结为什么generator是一种异步方案。觉得真的好奇怪。因为你用generator要一直调用next方法才能继续,我想这就是阻塞的啊,怎么能是异步的呢。后来发现有些书上这样写是错误的。generator不是一种异步方案,它是和其他的一些配合才能实现异步。比如promise。由于yield以前也没用过,yield后面yieldable的内容又是有限的。自然就没有max(我nice的同事)说用的那么顺手。


解决我的疑问

后来我在一篇文章experiments-with-koa-and-javascript-generators找到了答案。

也就是说promise+generator或者generator和其他异步方案一起合作才是正解。直接说generator就是异解决方案是不准确的。


koa的中间机制原理

那么koa 中间件机制实现原理是什么呢?为什么中间我们一定要传入一个generator当作函数呢?一定需要用yield吗?yield的作用是什么呢?next又是什么?yield next为什么可以就顺序的执行下一个中间件?
为什么中间件从上到下执行完后,可以从下到上执行 yield next 后的逻辑?这是我用这个框架的时候的疑问。

先来看一种实现koa中间机制的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var co = require('co');

class koa {
constructor (){
this.middlewares = [];
}

use(middleware){
this.middlewares.push(middleware);
}

listen(){
this._run();
}

_run(){
var ctx = this;
var middlewares = this.middlewares;

return co(function *(){
var prev = null;

while (i--) {
prev = middlewares[i].call(ctx, prev);
}
//执行第一个中间件
yield prev;
})();
}
}

然后我们以原始跟原始koa项目一样的代码来运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var app = new koa();

app.use(function *(next){
this.body = '1';
yield next;
this.body += '5';
console.log(this.body);
});
app.use(function *(next){
this.body += '2';
yield next;
this.body += '4';
});
app.use(function *(){
this.body += '3';
});
app.listen();

可以很清楚的发现是一样的结果。1,2,3,4,5。koa这种思想中yield相当于一个断点。到这个地方就停了。然后接着执行下一个next。很类似我们学过的递归的思想。它的思想就是把一系列中间件放入一个数组中,然后从第一个开始执行。


koa的错误处理机制

一般我们在js的错误处理机制中有一下几种方法:

  1. 和其他同步语言类似的 throw / try / catch 方法
  2. callback(err, data) 回调形式
  3. 通过 EventEmitter 触发一个 error 事件

koa 通过可以通过它的特性让我们可以使用 catch 来捕获异步代码中的错误。

比如:

1
2
3
4
5
6
7
8
9
10
const fs = require('fs');
const Promise = require('bluebird');

let filename = '/nonexists';
let statAsync = Promise.promisify(fs.stat);
try {
yield statAsync(filename);
} catch(e) {
// error here
}

koa主要是通过co来实现。co是tj写的用来自动执行generator的小工具。如果我们不利用co,调用generator的时候,需要一直next,co帮助自动执行。

一般我们把中间件可以称作MVC里面的M,也就是逻辑业务处理层。最好的方式,是我们使用中间件来处理koa的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
app.use(function* (next) {
try {
yield* next;
} catch(e) {
let status = e.status || 500;
let message = e.message || '服务器错误';

if (e instanceof JsonError) { // 错误是 json 错误
this.body = {
'status': status,
'message': message
};
if (status == 500) {
// 触发 koa 统一错误事件,可以打印出详细的错误堆栈 log
this.app.emit('error', e, this);
}
return;
}

this.status = status;
// 根据 status 渲染不同的页面
if (status == 403) {
this.body = yield this.render('403.html', {'err': e});
}
if (status == 404) {
this.body = yield this.render('404.html', {'err': e});
}
if (status == 500) {
this.body = yield this.render('500.html', {'err': e});
// 触发 koa 统一错误事件,可以打印出详细的错误堆栈 log
this.app.emit('error', e, this);
}
}
});
`

当我们只在try块里面执行yield的时候。我们的错误都放在catch里面,然后再对catch到的内容做处理,就是一种很有效的方法。

当我们触发错误的时候:

1
2
3
4
5
6
7
8
9
10
11
const router = new (require('koa-router'));

router.get('/some_page', function* () {
// 直接抛出错误,被中间件捕获后当成 500 错误
throw new PageError('发生了一个致命错误');
throw new JsonError('发送了一个致命错误');

// 带 status 的错误,被中间件捕获后特殊处理
this.throw(403, new PageError('没有权限访问'));
this.throw(403, new JsonError('没有权限访问'));
});

这里的JsoneError和PageError是定义的错误构造器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const util = require('util');

exports.JsonError = JsonError;
exports.PageError = PageError;

function JsonError(message) {
Error.call(this, message);
}
util.inherits(JsonError, Error);

function PageError(message) {
Error.call(this, message);
}
util.inherits(PageError, Error);

通过将代码细分,就可以得到更清晰的结果。这一部分学习自:淘宝fedkoa错误处理


总结

关于co中间的原理,比如返回promise,比如利用到了trunk。这些我还没有完全理解清楚,等理解清楚了再来记录下。推荐几篇文章:

  1. 阮一峰es6入门:http://es6.ruanyifeng.com/#docs/async#co模块
  2. promise实现:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
  3. koa中文文档: https://github.com/guo-yu/koa-guide
  4. koa异步讲解:http://blog.stevensanderson.com/2013/12/21/experiments-with-koa-and-javascript-generators/
  5. 淘宝fedkoa错误处理:http://taobaofed.org/blog/2016/03/18/error-handling-in-koa/