koa2 中的错误处理以及中间件设计原理
ZelMichalik
6年前
<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>