概述
接口报错的统一处理本质上是为了防止意外的代码逻辑导致系统崩溃停止服务的问题,帮助后台开发快速定位问题所在。
就经验而谈,实际开发过程中,处理接口错误通常分为两个层次:
其一是处理常见的请求错误,比如接口404、400、500等等,这类错误没有完成完整的请求-响应过程,后台可以通过中间件捕获异常返回给前端。
其二则是正确请求了接口,服务端也接受了响应,但却没有返回给前端期望的响应,比如用户访问某个接口时没有权限操作,这时后台就需要规定自己的状态码来标识,将结果返回给前端。
所以统一处理接口错误,需要从这两方面来做。
实例
以获取菜单列表的接口controller为例:
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将其包裹起来,若代码逻辑出现错误,就将错误信息和状态码交由如下的错误中间件处理:
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中引入。
如此,对于未正常完成请求-响应过程的错误,就会暴露给前端。
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);
前端获取到错误以后,根据响应状态码做响应拦截。
后台则在controller层,通过try catch包括可能出现问题的代码来抛出错误,使用express自带的http-error来创建http错误,并交给前端处理:
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一个没有的变量,那么这段代码运行时将会报错:
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:
查看对应的错误信息:
有时候后台写代码难免会遇到一些逻辑上的问题,没有注意到从代码逻辑上去排查问题,所以这个错误信息前端会第一时间拿到,和后台沟通交流解决问题。
第二层拦截
第一层的错误拦截就完成了,接下来是处理第二层的逻辑控制。
其实第二层由于完成了请求-响应流程,后台服务的代码逻辑是没有问题的,主要是配合前端完成规范的响应,让前端能够更合理地获取预期数据,并按预期的方式进行处理。
在app文件夹下新建个文件common,新增文件:eslintResponce.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的请求错误,我觉得不合理,实际上这不是这个处理业务逻辑的中间件内部能够发送给前端的错误信息了,因为这个code是写在res的status中的,而且前端的响应拦截也不好做,所以代码细节上的code有所改动。
之后在controller中使用:
const EslientResult = require('../../common/eslintResponce')
返回结果:
return res.send( EslientResult.success(result[0]))
res.send({
msg:'成功',
code:10000,
data:result[0]
})
结果和预期一样:
相关知识点
中间件
开发node服务应用的框架express,本质上是一系列中间件函数的调用,中间件函数一般接受三个参数:请求对象、响应对象和下一个中间件函数。
中间件函数的主要作用:
1、更改请求和响应对象
2、结束请求-响应
3、调用堆栈中的下一个中间件函数
在Express中,中间件包括5种类型:
1、应用级中间件
2、路由级中间件
3、错误处理中间件
4、内置中间件
5、第三方中间件
应用级中间件
应用级中间件通过app.use和app.method挂载到express实例上。
app.use((req, res, next) => {
console.log('Time:', Date.now())
next()
})
该实例没有挂载路径,每次收到请求都会执行该函数。
app.use('/user/editCircle',middleware)
此实例挂载到/user/editCircle路径上,匹配到该路径的任何HTTP请求都会执行。
app.get('/user/editCircle', middleware)
该实例挂载了中间件函数的实例,值处理该路径的get请求。
app.get('/user/editCircle', middleware1, middleware2)
该实例加载了一系列中间件函数,在挂载点创建中间件系统的子堆栈,并按照堆栈顺序进行执行。
我们在开发过程中会经常使用到这样的场景,比如鉴别用户权限时,往往需要多个中间一起协作,比如:
router.post('/user/editCircle',[authMiddleware.auth,articleMiddleware.canYouEditArticle],controller.editCircle)
这两个中间件auth和canyouEditArticle在执行过程中若是捕获到错误,往往需要跳出中间件子堆栈,将控制权移交给最后的错误中间件进行处理,而非下一个中间件,这时可以传递一个其他内容给next函数,如next(err),此时express会将当前请求认为是错误,并跳过任何剩余的剩余的非错误处理路由和中间件函数。
而next('route')则是将控制权传递给下一个路由,需要注意的是,next("route")可以且仅可以在app.method或 router中的中间件中工作。
路由级中间件
路由级中间件和应用级中间件的工作方式相同,区别在于路由级中间件绑定在router中:
const router = express.Router()
同样可以使用use和method来挂载。
错误处理中间件
错误处理中间件和其他中间件不同,该中间件使用4个参数而不是3个:
app.use((err, req, res, next) => {
console.error(err.stack)
res.status(500).send('Something broke!')
})
这里不得不提下关于Express的错误处理机制,express自带错误处理程序,开发者只需要确保能够捕获到程序即可。 捕获错误有两种办法,一是通过try catch,二是通过promise。
对于同步代码错误,express能够捕获它,并将其传递给错误中间件,然而对于异步的错误,则需要手动调用next(err)捕获错误。 可以使用try catch来捕获同步和异步的代码错误,并将错误传递给错误中间件进行处理。
promise可以避免去增加try catch块,实际上大差不差,图方便我这里就使用try catch了,其余想多了解的可以去看官方文档:
express.nodejs.cn/en/guide/er…
内置中间件
express具有如下内置中间件函数:
express.static,提供静态资源,例如html文件、图片等
express.json,使用JSON有效负载去解析传入请求
express.urlencoded,使用URL编码的负载解析传入的请求
第三方中间件
使用第三方中间件向express添加功能,需要进行下载安装,然后再应用级或路由级引入。