koa2 中的错误处理以及中间件设计原理

ZelMichalik 9个月前
   <p>其实这不是一个问题,因为就 koa2 而言,他已经帮我做好了统一错误处理入口 app.onerror 方法。</p>    <p>我们只要覆盖这个方法,就可以统一处理包括 中间件,事件,流 等出现的错误。</p>    <p>但我们始终会看到 UnhandledPromiseRejectionWarning: 类型的错误。</p>    <p>当然,这不一定就是 koa 导致,有可能是其他异步未处理错误导致的,但这都不重要。</p>    <p>让我们来看看 koa 是如何处理全局错误的。</p>    <h2>koa2 中间件</h2>    <p>官网例子:</p>    <pre>  <code class="language-javascript">const Koa = require('koa');    const app = new Koa();    app.use(async (ctx, next) => {    const start = Date.now();    await next();    const ms = Date.now() - start;    console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);  });    app.use(ctx => {    ctx.body = 'Hello Koa';  });    app.listen(3000);  </code></pre>    <p>由于 koa2 设计原理,让我们很容易的就实现了一个请求日志中间件。</p>    <p>这里就不上洋葱图了,因为这不是入门教程。</p>    <p>官网上也说了,中间件的 async 可以改写为普通函数。</p>    <pre>  <code class="language-javascript">app.use((ctx, next) => {    const start = Date.now();    return next().then(() => {      const ms = Date.now() - start;      console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);    });  });  </code></pre>    <p>和上面效果一致。</p>    <p>但你知道为什么要加 return ?如果不加 return 会发生什么吗?</p>    <h2>多中间件</h2>    <p>删除 return 测试后会发现,好像没问题,一切正常。</p>    <p>我们来看个例子:</p>    <pre>  <code class="language-javascript">const Koa = require('koa');    const app = new Koa();    app.use((ctx, next) => {    ctx.msg = 'hello';    next();  });    app.use((ctx, next) => {    ctx.msg += ' ';    next();  });    app.use((ctx, next) => {    ctx.msg += 'world';    next();  });    app.use(ctx => {    ctx.body = ctx.msg;  });    app.listen(3000);  </code></pre>    <p>打开页面后,如果你看到 hello world 那恭喜你,一切正常。</p>    <h2>中间件中的异常</h2>    <p>如果我们不小心把 ctx.msg += 'world'; 写成了 cxt.msg += 'world'; 这种手误相信大家都会遇到吧。</p>    <p>或者干脆直接抛出个错误算了,方便测试。</p>    <pre>  <code class="language-javascript">app.use((ctx, next) => {    throw Error('炸了');    ctx.msg += 'world';    next();  });  </code></pre>    <p>恭喜得到 UnhandledPromiseRejectionWarning: Error: 炸了 错误一枚。</p>    <p>让我们加上 app.onerror 来和谐这个错误吧。</p>    <pre>  <code class="language-javascript">const Koa = require('koa');    const app = new Koa();    app.use((ctx, next) => {    ctx.msg = 'hello';    next();  });    app.use((ctx, next) => {    ctx.msg += ' ';    next();  });    app.use((ctx, next) => {    throw Error('炸了');    ctx.msg += 'world';    next();  });    app.use(ctx => {    ctx.body = ctx.msg;  });    app.onerror = (err) => {    console.log('捕获到了!', err.message);  }    app.listen(3000);  </code></pre>    <p>再次运行,遇到哲学问题了,为什么他没捕获到。</p>    <p>再试试官网中记载的错误处理方法 <a href="/misc/goto?guid=4959757862008837380" rel="nofollow,noindex">Error Handling</a> .</p>    <pre>  <code class="language-javascript">const Koa = require('koa');    const app = new Koa();    app.use(async (ctx, next) => {    try {      await next();    } catch (err) {      ctx.status = err.status || 500;      ctx.body = err.message;      ctx.app.emit('error', err, ctx);    }  });    app.on('error', (err, ctx) => {    console.log('捕获到了!', err.message);  });    app.use((ctx, next) => {    ctx.msg = 'hello';    next();  });    app.use((ctx, next) => {    ctx.msg += ' ';    next();  });    app.use((ctx, next) => {    throw Error('炸了');    ctx.msg += 'world';    next();  });    app.use(ctx => {    ctx.body = ctx.msg;  });    app.listen(3000);  </code></pre>    <p>再次运行,,神了,依然也没捕获到,难道官网例子是假的?还是我们下了个假的 koa ?</p>    <h2>中间件关联的纽带</h2>    <p>其实吧,我们违反了 koa 的设计,有两种方法处理这个问题。</p>    <p>如果不想改成 async 函数,那就在所有 next() 前面加上 return 即可。</p>    <p>如果是 async 函数,那所有 next 前面加 await 即可。</p>    <p>先来看看结果:</p>    <pre>  <code class="language-javascript">const Koa = require('koa');    const app = new Koa();    app.use(async (ctx, next) => {    try {      await next();    } catch (err) {      ctx.status = err.status || 500;      ctx.body = err.message;      ctx.app.emit('error', err, ctx);    }  });    app.on('error', (err, ctx) => {    console.log('捕获到了!', err.message);  });    app.use((ctx, next) => {    ctx.msg = 'hello';    return next();  });    app.use((ctx, next) => {    ctx.msg += ' ';    return next();  });    app.use((ctx, next) => {    throw Error('炸了');    ctx.msg += 'world';    return next();  });    app.use(ctx => {    ctx.body = ctx.msg;  });    app.listen(3000);  </code></pre>    <p>再次运行,可以完美的捕获到错误。</p>    <h2>自定义错误处理</h2>    <p>如果是自定义异步操作异常呢。</p>    <pre>  <code class="language-javascript">const Koa = require('koa');    const app = new Koa();    app.use(ctx => {    new Promise(() => {      throw Error('炸了');    });    ctx.body = 'Hello Koa';  });    app.onerror = (err) => {    console.log('捕获到了!', err.message);  }    app.listen(3000);  </code></pre>    <p>由于是用户自定义操作,什么时候发生错误其实是未知的。</p>    <p>但我们只要把错误引导到 koa 层面报错,即可利用 app.onerror 统一处理。</p>    <pre>  <code class="language-javascript">app.use(async ctx => {    await new Promise(() => {      throw Error('炸了');    });    ctx.body = 'Hello Koa';  });  </code></pre>    <p>这样他的错误其实是在 koa 的控制下 throw 的,可以被 koa 统一捕获到。</p>    <h2>中间件原理</h2>    <p>说了这么多错误处理方法,还没说为什么要这处理。</p>    <p>当然如果你对原理不感兴趣,其实上面就够了,下面的原理可以忽略。</p>    <p>koa 的中间件其实就是一个平行函数(函数数组)转为嵌套函数的过程。</p>    <p>用到了 <a href="/misc/goto?guid=4959757862086970143" rel="nofollow,noindex">koa-compose</a> ,除去注释源码就20行左右。</p>    <p>功底扎实的就不需要我多解释了,如果看不懂,那就大致理解为下面这样。</p>    <pre>  <code class="language-javascript">// 我们定义的中间件  fn1(ctx, next);  fn2(ctx, next);  fn3(ctx);  // 组合成  fn1(ctx, () => {    fn2(ctx, () => {      fn3(ctx);    })  });  </code></pre>    <p>是不是看的一脸懵逼,那就对了,因为我也不知道怎么表达。</p>    <p>看个类似的问题的,从本质问题出发。</p>    <pre>  <code class="language-javascript">function fn(ctx) {    return new Promise(resolve => {      setTimeout(() => resolve(ctx), 0);    });  }    const ctx = { a: 1 };  fn(ctx).then((ctx) => {    ctx.b = 1;    fn(ctx).then((ctx) => {      ctx.c = 1;      fn(ctx).then((ctx) => {        ctx.d = 1;        fn(ctx).then((ctx) => {          fn(ctx).then(console.log);        });      });    });  }).catch(console.error);  </code></pre>    <p>执行后输出 { a: 1, b: 1, c: 1, d: 1 }<br> 如果在内层回调中加个错误。</p>    <pre>  <code class="language-javascript">function fn(ctx) {    return new Promise(resolve => {      setTimeout(() => resolve(ctx), 0);    });  }    const ctx = { a: 1 };  fn(ctx).then((ctx) => {    ctx.b = 1;    fn(ctx).then((ctx) => {      ctx.c = 1;      throw Error('err');      fn(ctx).then((ctx) => {        ctx.d = 1;        fn(ctx).then((ctx) => {          fn(ctx).then(console.log);        });      });    });  }).catch(console.error);  </code></pre>    <p>跟 koa 中的情况一样,无法捕获,而且抛出 UnhandledPromiseRejectionWarning: 错误。</p>    <p>我们只需要加上 return 即可。</p>    <pre>  <code class="language-javascript">function fn(ctx) {    return new Promise(resolve => {      setTimeout(() => resolve(ctx), 0);    });  }    const ctx = { a: 1 };  fn(ctx).then((ctx) => {    ctx.b = 1;    return fn(ctx).then((ctx) => {      ctx.c = 1;      throw Error('err');      return fn(ctx).then((ctx) => {        ctx.d = 1;        return fn(ctx).then((ctx) => {          return fn(ctx).then(console.log);        });      });    });  }).catch(console.error);  </code></pre>    <p>这次执行,发现捕获到了。为什么会发生这样的情况呢?</p>    <p>简单说吧,就是 promise 链断掉了。我们只要让他连接起来,不要断掉即可。</p>    <p>所以内层需要 return 否则就相当于 return undefined 导致链断掉了,自然无法被外层 catch 到。</p>    <pre>  <code class="language-javascript">const ctx = { a: 1 };  fn(ctx).then(async () => {    await fn(ctx).then(async () => {      await fn(ctx).then(async () => {        await fn(ctx).then(async () => {          throw Error('123');          await fn(ctx);        });      });    });  }).catch(console.error);  </code></pre>    <p>当然改成 async/await 也可以。</p>    <h2>中间件设计</h2>    <p>官网 issue 中 <a href="/misc/goto?guid=4959757862167980733" rel="nofollow,noindex">I can’t catch the error ~</a> 就有人问了,为什么我捕获不到错误。</p>    <p>回答中说,必须 await 或 return。</p>    <p>但也有人修改了源码,加了个类似 Promise.try 的实现。</p>    <p>然后被人说了,为什么你要违反他本来的设计。</p>    <p>其实没看到这个之前,我也打算自己修改源码的。</p>    <p>很多时候当我们看到代码为什么不那样写的时候,其实人家已经从全局考虑了这个问题。</p>    <p>而我们只是看到了这一个“问题”的解决方法,而没有在更高层面统筹看待问题。</p>    <p> </p>    <p>来自:http://www.52cik.com/2018/05/27/koa-error.html</p>    <p> </p>