接口报错的统一处理这个功能是极为必要,说到好处确实挺多的,比如执行效率高、更安全、避免代码冗余等等,我觉得这些话大概率是精通面试题的人会侃侃而谈的,确实有这样的道理,但我认为最切实际同时也是最重要的说法是,接口报错统一处理在本质上还是为了防止代码逻辑出现错误时导致系统崩溃停止服务的问题(当然,有很多插件会在系统崩溃后自动重启,这是另一回事,这里不谈)
就经验而谈,在我们实际开发过程中,处理接口错误通常分为两个层次。
第一层处理常见的请求错误,比如接口404,服务器错误500,或者验证参数时不合规的400(实际上考虑这个问题,个人理解还是应该200的成功状态,至少在node搭建后台应该这样做,毕竟node后台主要是靠中间件对请求进行拦截的,它是中间件一个一个走的)等等,这类错误可以通过中间件异常捕获返回给前端。
第二层则是正确请求了接口,也就是说参数验证通过,http状态值为200,但存在可能处理请求成功却返回数据不符合预期的情况,比如权限不足,这层权限可以通过自定义的错误码来标识,并返回给前端。
这样能够更好地统一处理接口错误,防止服务器出现意外死机,提高代码的可维护性。
我们以这个获取菜单列表的接口controller为例:
js
async getMenuList(req,res){
let {error,value} = Joi.object({
pagenum:Joi.number().required(),
pagesize:Joi.number().required(),
search:Joi.string()
}).unknown().validate(req.query)
if(error) return res.send(error)
let result = await adminDao.getMenuList(value)
return res.send({
code:10000,
msg:'ok',
data:result
})
},
最没有经验的开发人员会这样写的,这种sync await是非常容易出现问题的,很多未知的错误都是从异步函数中产生的,比如可能数据库连接错误、mysql语法错误或者其他代码运行时产生的错误,一旦代码运行出错,服务将会停止。
所以我们要对controller进行异常捕获,即通过try catch将其包裹起来,如果出现错误,就将错误信息和状态码交由下个中间件处理,也就是处理错误的中间件。
那么先完成这个拦截的中间件:
js
async errorMiddlewarefunction (err, req, res, next) {
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
res.status(err.status || 500);
res.render('error');
}
这个没啥好说的,就是拿的express初始化的那个处理错误的中间件函数。
然后把这个中间件参数写在middleware下,在app.js中引入:
js
for(let i in indexRouter){
// console.log(i)
app.use('/',indexRouter[i])
}
app.use(function (req, res, next) {
next(createError(404));
});
// 集中处理第一层错误
app.use(middleware.errorMiddleware);
第一个中间件,是自己写的接口。
第二个中间件,集中处理没有匹配到任何的路由,并抛出一个错误传递给错误中间件。
第三个中间件,则是集中处理错误的中间件。
这个错误会让前端去控制,比如axios通过响应拦截去处理不同的响应状态。
后台在controller层需要包裹try catch来抛出错误,使用express自带的http-error来创建http错误,并交给前端处理:
js
var createError = require('http-errors');
async getUserInfo(req,res,next){
// let token = req.headers['authorization']
try {
let params = req.query
let result = await loginDao.getUserInfo(params)
result[0].roleList = []
result[0].roleids&&result[0].roleids.split(',').forEach((item,index)=>{
let rolenames = result[0].rolenames.split(',')
result[0].roleList.push({
id:item,
rolename:rolenames[index]
})
})
res.send({
msg:'成功',
code:10000,
data:result[0]
})
}catch(err){
const error = createError(500, err.message);
next(error)
}
},
现在我在dao层中去console一个没有的变量,那么这段代码运行时将会报错:
js
static async getUserInfo(params){
console.log(song)
let baseSql = `select *,
(SELECT GROUP_CONCAT(user_role.roleid) from user_role WHERE user_role.userid = user.id) roleids,
(SELECT GROUP_CONCAT(role.rolename) from user_role LEFT JOIN role on role.id = user_role.roleid
WHERE user_role.userid = user.id) rolenames
from user where 1=1 and id = :id`
let [results,meta] = await Model.sequelize.query(baseSql,{
replacements:{
id:params.userid
}
})
return results
}
前端请求这个接口,捕获到错误,code为500:
查看对应的错误信息:
有时候后台写代码难免会遇到一些逻辑上的问题,没有注意到从代码逻辑上去排查问题,所以这个错误信息前端会第一时间拿到,和后台沟通交流解决问题。
那么到这里,第一层的错误拦截就完成了,其实第一层express都给做好了,我只是把对应的逻辑搬到它该去的地方,接下来就是第二层的逻辑控制。
其实第二层严格来说不会影响后台服务发生问题,因为请求响应都是正常,主要是配合前端完成规范的响应问题,让前端能够更合理地获取预期数据,并按预期的方式进行处理。
在app文件夹下新建个文件common,新增文件:eslintResponce.js:
js
module.exports = class EslientResult {
code;
msg;
data;
time;
constructor(code, msg, data) {
this.code = code;
this.msg = msg;
this.data = data;
this.time = Date.now();
}
static success(data) {
return new EslientResult(
EslientResultCode.SUCCESS.code,
EslientResultCode.SUCCESS.desc,
data
);
}
static fail(errData) {
return new EslientResult(
EslientResultCode.FAILED.code,
EslientResultCode.FAILED.desc,
errData
);
}
static validateFailed(param) {
return new EslientResult(
EslientResultCode.VALIDATE_FAILED.code,
EslientResultCode.VALIDATE_FAILED.desc,
param
);
}
/**
* 拦截到的业务异常
* @param bizException {BizException} 业务异常
*/
static bizFail(bizException) {
return new EslientResult(bizException.code, bizException.msg, null);
}
};
class EslientResultCode {
code;
desc;
constructor(code, desc) {
this.code = code;
this.desc = desc;
}
static SUCCESS = new EslientResultCode(10000, "成功");
static FAILED = new EslientResultCode(10004, "失败");
static VALIDATE_FAILED = new EslientResultCode(10005, "参数校验失败");
// static API_NOT_FOUNT = new EslientResultCode(404, "接口不存在");
static API_BUSY = new EslientResultCode(10006, "操作过于频繁");
}
这里借鉴的是@热爱学习的欧尼酱的写法,就没有想那么多了,原文:
code笔记:nodeJS框架 express 接口统一返回结果设计 - 掘金 (juejin.cn)
这个思路跟我的差得不多,也就没有再去手写了,原文中他是截获了原生http的请求错误,我觉得不合理,就比如404或500,实际上这不是这个处理业务逻辑的中间件内部能够发送给前端的错误信息了,因为这个code是写在res的status中的,而且前端的响应拦截也不好做,所以代码细节上的code有所改动。
之后在controller中使用:
js
const EslientResult = require('../../common/eslintResponce')
返回结果(下面那个send就没用了,这里只是对照下,其实返回结果是一样的):
js
return res.send( EslientResult.success(result[0]))
res.send({
msg:'成功',
code:10000,
data:result[0]
})
结果和预期一样:
错误拦截和响应的统一处理,基本就到此结束了