在使用Express框架設計web應用時,一定會遇到異常處理問題。
因爲用戶數據或代碼自身問題,常常引起請求時,控制器內部異常,res返回不能被執行,前臺表現爲請求“卡死”。
1 處理 Exception
1.1 Express 錯誤處理中間件
爲了避免這種情況,Express有“錯誤處理中間件”的機制。
const express = require('express');
process.on('unhandledRejection', rej => console.warn('全局捕獲Rejection', rej));
const app = express();
const router = express.Router();
router.get('/', function (req, res) {
JSON.parse('{{'); // 拋出異常
res.send('首頁');
});
app.use(router);
// 錯誤處理中間件
app.use(function (err, req, res, next) {
console.warn('錯誤處理中間捕獲Exception', err);
res.send('內部錯誤');
});
app.listen(5000, () => console.log('Server Start ...'));
上述代碼運行後,訪問http://localhost:5000
時,會返回“內部錯誤”,表示控制器內錯誤被錯誤處理中間件捕獲。
2 處理 Rejection
2.1 存在的問題
AsyncFunction內部異常時,不會拋出Exception,而是Rejection,不能被傳統的try…catch…方式捕獲。
上述代碼如果改爲:
// function => AsyncFunction
router.get('/', async function (req, res) {
JSON.parse('{{'); // 拋出異常
res.send('首頁');
});
這時,訪問主頁,會發現卡死,並且後臺打印“全局捕獲Rejection…”。這表示異常不能被錯誤處理中間件捕獲,而是在全局被捕獲了。沒有人處理請求的返回了,於是請求卡死。
2.2 異常處理裝飾器
ctrlRejHandler.js :
module.exports = (ctrl) => {
if ({}.toString.call(ctrl) !== '[object AsyncFunction]') throw Error('Try to use ctrlErrHandler to decorate an Object not a AsyncFunction');
return function (req, res, next) {
return ctrl(req, res, next).catch(rej => {
console.warn('錯誤處理裝飾器 捕獲錯誤', rej);
res.send('內部錯誤');
});
};
};
2.3 異常處理裝飾器的位置問題
理論上,我們應該爲每一個AsyncFunction類型的裝飾器都使用異常處理。也就是說,我們希望儘量在全局安裝裝飾器,而不是每次使用控制器時都添加。
一種更簡約的寫法是,在使用module.exports導出控制器時,包裝整個函數參數對象。但這會帶來一個問題:無法通過IDE的定位功能定位ctrl了。
容易想到,控制器就是用在路由中的,至少路由使用控制器時,默認進行包裝就好了。
可以定義一個函數來包裝router,實現這樣的功能。更改router.get/post/…等使用控制器的函數,對它們的最後一個回調函數參數(即控制器)進行錯誤處理。
routerRejHandler.js :
module.exports = routerRejHandler;
// 對ctrlRejHandler稍作修改,使其支持一般的function
function ctrlRejHandler(ctrl) {
if (typeof ctrl !== 'function') throw Error('Try to use ctrlRejHandler to decorate an Object not a function');
if ({}.toString.call(ctrl) !== '[object AsyncFunction]') {
return ctrl;
}
return function (req, res, next) {
return ctrl(req, res, next).catch(rej => {
console.warn('錯誤處理裝飾器 捕獲錯誤', rej);
res.send('內部錯誤');
});
};
};
// router裝飾器,自動對控制器進行錯誤捕獲
function routerRejHandler(router) {
const methods = require('methods');
[...methods, 'all'].forEach(method => {
if (router[method]) {
router[method] = function (path, ...fns) {
const route = this.route(path);
const ctrlIndex = fns.length - 1;
// 只認爲最後一個回調函數參數爲控制器,之前的爲中間件
fns[ctrlIndex] = ctrlRejHandler(fns[ctrlIndex]);
route[method].apply(route, fns);
return this;
};
}
});
return router;
}
使用之:
const express = require('express');
const router = routerRejHandler(express.Router());
這時,對router.get('/path', 中間件1, 中間件2, 控制器)
這樣的情況,無需做任何其他處理,控制器拋出的Rejection將會被自動捕獲和處理。
進一步的,可以爲routerRejHandler增加一個回調函數,用以控制如何處理錯誤。
2.4 最佳實踐
rejHandler.js :
module.exports = {
ctrlDecorator,
routerDecorator
};
function ctrlDecorator(ctrl, dealer) {
if ({}.toString.call(ctrl) !== '[object AsyncFunction]') {
return ctrl;
}
return function (req, res, next) {
return ctrl(req, res, next).catch(rej => {
if (dealer) return dealer(req, res, next, rej);
res.send('Internal Error');
});
};
}
function routerDecorator(router, dealer) {
const methods = require('methods');
[...methods, 'all'].forEach(method => {
if (router[method]) {
router[method] = function (path, ...fns) {
if (fns.length === 0) return;
const route = this.route(path);
const ctrlIndex = fns.length - 1;
if (typeof fns[ctrlIndex] !== 'function') throw Error('The last param should be a controller, but not a function');
fns[ctrlIndex] = ctrlDecorator(fns[ctrlIndex], dealer);
route[method].apply(route, fns);
return this;
};
}
});
return router;
}
使用:
const routerRejHandler = require('./rejHandler').routerDecorator;
const router = routerRejHandler(express.Router(), function (req, res, next, rej) {
console.warn('捕捉到控制器內Rejection', rej);
res.status(500);
res.send('內部錯誤');
});
另外,若不使用router,裝飾器同樣可以用於app對象。