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/