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

Koa.js 中间件   2018-05-28 08:54:35 发布
您的评价:
     
0.0
收藏     0收藏
文件夹
标签
(多个标签用逗号分隔)

其实这不是一个问题,因为就 koa2 而言,他已经帮我做好了统一错误处理入口 app.onerror 方法。

我们只要覆盖这个方法,就可以统一处理包括 中间件,事件,流 等出现的错误。

但我们始终会看到 UnhandledPromiseRejectionWarning: 类型的错误。

当然,这不一定就是 koa 导致,有可能是其他异步未处理错误导致的,但这都不重要。

让我们来看看 koa 是如何处理全局错误的。

koa2 中间件

官网例子:

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);

由于 koa2 设计原理,让我们很容易的就实现了一个请求日志中间件。

这里就不上洋葱图了,因为这不是入门教程。

官网上也说了,中间件的 async 可以改写为普通函数。

app.use((ctx, next) => {
  const start = Date.now();
  return next().then(() => {
    const ms = Date.now() - start;
    console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
  });
});

和上面效果一致。

但你知道为什么要加 return ?如果不加 return 会发生什么吗?

多中间件

删除 return 测试后会发现,好像没问题,一切正常。

我们来看个例子:

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);

打开页面后,如果你看到 hello world 那恭喜你,一切正常。

中间件中的异常

如果我们不小心把 ctx.msg += 'world'; 写成了 cxt.msg += 'world'; 这种手误相信大家都会遇到吧。

或者干脆直接抛出个错误算了,方便测试。

app.use((ctx, next) => {
  throw Error('炸了');
  ctx.msg += 'world';
  next();
});

恭喜得到 UnhandledPromiseRejectionWarning: Error: 炸了 错误一枚。

让我们加上 app.onerror 来和谐这个错误吧。

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);

再次运行,遇到哲学问题了,为什么他没捕获到。

再试试官网中记载的错误处理方法 Error Handling .

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);

再次运行,,神了,依然也没捕获到,难道官网例子是假的?还是我们下了个假的 koa ?

中间件关联的纽带

其实吧,我们违反了 koa 的设计,有两种方法处理这个问题。

如果不想改成 async 函数,那就在所有 next() 前面加上 return 即可。

如果是 async 函数,那所有 next 前面加 await 即可。

先来看看结果:

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);

再次运行,可以完美的捕获到错误。

自定义错误处理

如果是自定义异步操作异常呢。

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);

由于是用户自定义操作,什么时候发生错误其实是未知的。

但我们只要把错误引导到 koa 层面报错,即可利用 app.onerror 统一处理。

app.use(async ctx => {
  await new Promise(() => {
    throw Error('炸了');
  });
  ctx.body = 'Hello Koa';
});

这样他的错误其实是在 koa 的控制下 throw 的,可以被 koa 统一捕获到。

中间件原理

说了这么多错误处理方法,还没说为什么要这处理。

当然如果你对原理不感兴趣,其实上面就够了,下面的原理可以忽略。

koa 的中间件其实就是一个平行函数(函数数组)转为嵌套函数的过程。

用到了 koa-compose ,除去注释源码就20行左右。

功底扎实的就不需要我多解释了,如果看不懂,那就大致理解为下面这样。

// 我们定义的中间件
fn1(ctx, next);
fn2(ctx, next);
fn3(ctx);
// 组合成
fn1(ctx, () => {
  fn2(ctx, () => {
    fn3(ctx);
  })
});

是不是看的一脸懵逼,那就对了,因为我也不知道怎么表达。

看个类似的问题的,从本质问题出发。

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);

执行后输出 { a: 1, b: 1, c: 1, d: 1 }
如果在内层回调中加个错误。

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);

跟 koa 中的情况一样,无法捕获,而且抛出 UnhandledPromiseRejectionWarning: 错误。

我们只需要加上 return 即可。

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);

这次执行,发现捕获到了。为什么会发生这样的情况呢?

简单说吧,就是 promise 链断掉了。我们只要让他连接起来,不要断掉即可。

所以内层需要 return 否则就相当于 return undefined 导致链断掉了,自然无法被外层 catch 到。

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);

当然改成 async/await 也可以。

中间件设计

官网 issue 中 I can’t catch the error ~ 就有人问了,为什么我捕获不到错误。

回答中说,必须 await 或 return。

但也有人修改了源码,加了个类似 Promise.try 的实现。

然后被人说了,为什么你要违反他本来的设计。

其实没看到这个之前,我也打算自己修改源码的。

很多时候当我们看到代码为什么不那样写的时候,其实人家已经从全局考虑了这个问题。

而我们只是看到了这一个“问题”的解决方法,而没有在更高层面统筹看待问题。

 

来自:http://www.52cik.com/2018/05/27/koa-error.html

 

扩展阅读

用 Koa 写服务体验
给 JavaScript 初心者的 ES2015 实战
企业级 Node.js Web 应用解决方案设计的零零总总
koa-grace:一个基于koa的node多应用MVC框架
基于webpack搭建前端工程解决方案探索

为您推荐

一个基于 Koa2 构建的类似于 Rails 的 nodejs 开源项目
paloma - 一个类似于angluar的MVC框架
是时候闭环Java应用了
jQuery常用方法
jQuery常用方法一览

更多

Koa.js
中间件
JavaScript开发
相关文档  — 更多
相关经验  — 更多
相关讨论  — 更多