在使用Express框架设计web应用时,一定会遇到异常处理问题。
因为用户数据或代码自身问题,常常引起请求时,控制器内部异常,res返回不能被执行,前台表现为请求“卡死”。
为了避免这种情况,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
时,会返回“内部错误”,表示控制器内错误被错误处理中间件捕获。
AsyncFunction内部异常时,不会抛出Exception,而是Rejection,不能被传统的try…catch…方式捕获。
上述代码如果改为:
// function => AsyncFunction
router.get('/', async function (req, res) {
JSON.parse('{{'); // 抛出异常
res.send('首页');
});
这时,访问主页,会发现卡死,并且后台打印“全局捕获Rejection…”。这表示异常不能被错误处理中间件捕获,而是在全局被捕获了。没有人处理请求的返回了,于是请求卡死。
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('内部错误');
});
};
};
理论上,我们应该为每一个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增加一个回调函数,用以控制如何处理错误。
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对象。