koa2 中的錯誤處理以及中間件設計原理
其實這不是一個問題,因為就 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