koa2 中的錯誤處理以及中間件設計原理

ZelMichalik 6年前發布 | 31K 次閱讀 Koa.js 中間件 JavaScript開發

其實這不是一個問題,因為就 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

 

 本文由用戶 ZelMichalik 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!