在之前的设计中,我们只是通过try. .. catch
机制来捕获异常并简单提示,其他的处理都交给脚手架进行默认处理。在实现其他功能之前,我们对异常处理过程进行改进。具体包括如下的内容:
- 后端接口返回格式规范
- 脚手架预定义的全局异常处理
- 改进的全局异常处理过程
- 模拟典型出错场景并查看前端反应
13.1 后端接口返回格式规范
在前两章,我们用umi的request
函数发起网络请求的时候,在Mock中分别定义了如下的两种返回信息
///api/member/queryAll返回的数据
const result = {
success: true,
data: dataSource,
total: memberListDataSource.length,
pageSize,
current,
};
const result = {
success: true,
};
两个返回这里遵照了umi-request中对后端接口返回格式的约定(详见@umijs/plugin-request介绍),下面是具体定义
interface ErrorInfoStructure {
success: boolean; // if request is success
data?: any; // response data
errorCode?: string; // code for errorType
errorMessage?: string; // message display to user
showType?: number; // error display type: 0 silent; 1 message.warn; 2 message.error; 4 notification; 9 page
traceId?: string; // Convenient for backend Troubleshooting: unique request ID
host?: string; // Convenient for backend Troubleshooting: host of current access server
}
不管是在Mock模拟的时候还是在服务器实际实现响应处理时,都应该依照此规范进行设计。
当然,我们不必完全严格按照规范从后端(服务器或Mock)返回全部的内容,但至少要通过
success
属性声明处理结果。
13.2 阅读预定义的全局异常处理
打开src/app.tsx
,我们可以看到脚手架预定义的全局异常处理过程(本节的代码都是已经生成的,我们只是阅读和分析)。
首先是和常见HTTP响应状态码有关的出错信息定义
const codeMessage = {
200: '服务器成功返回请求的数据。',
201: '新建或修改数据成功。',
202: '一个请求已经进入后台排队(异步任务)。',
204: '删除数据成功。',
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
401: '用户没有权限(令牌、用户名、密码错误)。',
403: '用户得到授权,但是访问是被禁止的。',
404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
405: '请求方法不被允许。',
406: '请求的格式不可得。',
410: '请求的资源被永久删除,且不会再得到的。',
422: '当创建一个对象时,发生一个验证错误。',
500: '服务器发生错误,请检查服务器。',
502: '网关错误。',
503: '服务不可用,服务器暂时过载或维护。',
504: '网关超时。',
};
然后是给request
用的异常处理函数
/**
* 异常处理程序
* @see https://beta-pro.ant.design/docs/request-cn
*/
export const request: RequestConfig = {
errorHandler: (error: ResponseError) => {
const { response } = error;
if (response && response.status) {
const { status, statusText, url } = response;
const requestErrorMessage = '请求错误';
const errorMessage = `${requestErrorMessage} ${status}: ${url}`;
const errorDescription = codeMessage[status] || statusText;
notification.error({
message: errorMessage,
description: errorDescription,
});
}
if (!response) {
notification.error({
description: '您的网络发生异常,无法连接服务器',
message: '网络异常',
});
}
throw error;
},
};
这里设置了全局网络请求的错误处理函数。在实际的项目中,可以把reque
的拦截器interceptors
也配置在这里,在发出网络请求前附加上Token或Cookie(详细的说明参见教程安全篇部分)。
异常处理函数的入口参数是一个出错数据对象,下面是他在umi-request中的定义
interface Error {
name: string;
message: string;
stack?: string;
}
export interface ResponseError<D = any> extends Error {
name: string;
data: D;
response: Response;
request: {
url: string;
options: RequestOptionsInit;
};
type: string;
}
在异常处理函数内部首先判断这个出错数据对象中的response
属性是否有意义,如果是的话代表目标服务器给出了常规的HTTP状态反馈,此时优先从中解析出错信息。我们首先看一下Response的定义(根据场景的不同,有些属性可能不会被赋值)
/** This Fetch API interface represents the response to a request. */
interface Response extends Body {
headers: Headers;
ok: boolean;
redirected: boolean;
status: number;
statusText: string;
trailer: Promise<Headers>;
type: ResponseType;
url: string;
clone(): Response;
}
下面是response
属性有意义时在异常处理函数中的做法
if (response && response.status) {
// 取出response中的HTTP状态码、状态信息和原始请求的地址
const { status, statusText, url } = response;
const requestErrorMessage = '请求错误';
const errorMessage = `${requestErrorMessage} ${status}: ${url}`;
// response.status存的是HTTP状态码,如果已经定义了对应的说明就使用该说明,
// 否则就使用response..statusText的内容
const errorDescription = codeMessage[status] || statusText;
//把出错信息、HTTP状态码和原始请求地址一起告知用户
notification.error({
message: errorMessage,
description: errorDescription,
});
}
当没有获得服务器的响应(如地址错误、服务器超时等)或服务器给出正常响应,但在返回数据中告知发生了业务方面的错误(如不存在指定的数据、业务逻辑错误等)时,error.response
将是空值,预定义的异常处理程序按照如下的方式进行处理:
if (!response) {
notification.error({
description: '您的网络发生异常,无法连接服务器',
message: '网络异常',
});
}
显然这里的处理比较粗糙,我们需要更详细、更友好的处理过程。
13.3 改进的异常处理过程
13.3.1 response为空时的异常处理
当没有获得服务器的响应(如地址错误、服务器超时等)或服务器给出正常响应,但在返回数据中告知发生了业务方面的错误(如不存在指定的数据、业务逻辑错误等)时,error.response
将是空值,这部分是我们优化的重点。首先在函数体外定义我们自己的提示信息
const appExceptionMessage = {
0: '未知的错误。',
1: '服务器一直没有反应,可能是比较忙,请等一会儿再试一下。',
2: '哎呀,没有找到你要处理的数据,刷新一下页面再试试看吧。',
};
这里我们只定义了3个值。在实际的应用开发中,根据后端业务的实际情况,可以增加更多的内容。
把预定义的如下代码
if (!response) {
notification.error({
description: '您的网络发生异常,无法连接服务器',
message: '网络异常',
});
}
用下面优化后的代码代替
if (!response) {
let option = {
description : '您的网络发生异常,无法连接服务器',
message : '网络异常',
}
//处于开发状态的时候,将error的内容解析后输出在控制台上
if(isDev)
console.log('error', JSON.stringify(error,null,2));
switch(error.name) {
case 'RequestError':
//请求的时候就发生了错误,目前只针对超时进行单独的处理
if(error.type === "Timeout") {
option.description = appExceptionMessage[1];
option.message = '系统忙';
}
break;
case 'BizError':
//error.name是'BizError'意味着错误是由于返回的数据里面success为false而被抛出了异常
//这是umi-request的定义的机制
const {errorCode = 0, errorMessage = undefined} = error.data;
option.description = (errorMessage != undefined)? errorMessage : (appExceptionMessage[errorCode] || appExceptionMessage[0]);
option.message = '操作失败';
break;
}
notification.error(option)
}
13.3.2 更友好地显示HTTP异常状态信息
在反馈HTTP异常状态时,预订的信息内容和展示方式是纯粹从技术开发的角度来设计的,对于普通的软件用户并不友好,因此我们需要对此进行改进。
首先修改codeMessage的定义(这里只修改了期中的一部分,其他的内容可以视实际情况修改)
const codeMessage = {
...
404: '请求了不存在的资源,如果反复出现此现象,请联系软件服务商。',
...
500: '服务器遇到了一些麻烦,请稍后尝试或联系软件服务商。',
502: '中间服务器和远端服务器之间遇到了一些麻烦,请稍后尝试或联系软件服务商。',
503: '服务器临时有点儿忙,请稍后尝试或联系软件服务商。',
504: '中间服务器和没有联系到远端服务器,请稍后尝试或联系软件服务商。',
};
然后修改显示状态信息的逻辑和格式,不提示401( Unauthorized)状态(软件会直接跳转到登录页面),并且只有在开发状态才向软件用户提示当前访问的网络地址
- const errorMessage = `${requestErrorMessage} ${status}: ${url}`;
+ const errorMessage = `${requestErrorMessage} ${status}: ${isDev? url: ''}`;
+ if(status != 401) {
notification.error({
message: errorMessage,
description: errorDescription,
});
+ }
13.3.4 捕获请求异常后不再主动提示
改进全局异常处理之后,通常我们用try ... catch
捕获请求异常以后,无须再主动提示信息(除非业务逻辑有全局处理无法覆盖的情况)。现在我们删除pages/MemberList/index.tsx中的handleDeleteCurrent
和handleDeleteList
函数的出错提示:
hide();
- message.error('删除失败,请重试');
return false;
13.4 模拟典型出错场景
13.4.1 请求访问不存在的服务器
把src/services/api/member.ts
中的请求地址改成一个不存在的服务器
export async function deleteMember(params: {}) {
...
return request('http://na.51gh.com.cn/api/member/delete', {
13.4.2 请求访问服务器上不存在的资源
把src/services/api/member.ts
中的请求地址改成一个不存在的资源
export async function deleteMember(params: {}) {
...
return request('/api/member/delete1234', {
执行删除一条数据的操作,根据版本的不同,将得到下面两种提示信息中的一种
13.4.3 服务器反馈响应超时
在执行request
的时候,可以在配置参数值指定服务器请求超时的时间。因为如果设置的不合导致不应有的异常(详见下章的有关内容)所以在全局设置里面没有指定该参数的默认值,为模拟超时的场景,我们在src/services/api/member.ts
中给request
设置一个较短的超时时间(500毫秒):
export async function deleteMember(params: {}) {
...
return request('/api/member/delete', {
method: 'GET',
+ timeout: 500,
然后在mock/member.ts
中定义一个用于代码延迟的函数
//让异步函数暂停指定时间的函数,参数为毫秒
const waitTime = (microsecond: number = 100) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, microsecond);
});
};
把请求处理函数改成异步函数并且中间等待较长时间
-function deleteRow(req: Request, res: Response, u: string) {
+async function deleteRow(req: Request, res: Response, u: string) {
const { id } = req.query;
+ await waitTime(1000);
memberListDataSource = memberListDataSource.filter( data => data.id != id);
const result = {
success: true,
}
return res.json(result);
}
13.4.4 后端应用反馈出错信息
当后端返回数据对象的success
属性值为false
时,全局异常处理过程认为后端出现了异常(出错)状况,会自动在前端抛出异常,把后端传过来的数据打包到ResponseError
对象的data
属性并激发errorHandler
过程(此时ResponseError
对象的name值固定为BizError。
根据我们优化后的全局异常处理过程,后端传递过来的errorMessage的优先级比errorCode,也就是如果errorMessage不为空的的情况下,errorCode将被忽略。下面我们先模拟这一场景。
在mock/member.ts
的deleteRow
中,返回如下的数据
const result = {
success: false,
errorCode: 2,
errorMessage: '亲,我没有找到你要删除的东西',
}
执行删除一条数据的操作将得到下面的提示信息(内容时后端传递过来的)
如果后端没有设置errorMessage,那么前端的全局异常处理过程将尝试根据errorCode反馈信息
在mock/member.ts
的deleteRow
中,返回如下的数据
const result = {
success: false,
errorCode: 2,
}
执行删除一条数据的操作将得到下面的提示信息(内容时在前端预定义的)
提示:
- 具体开发的时候,应该尽可能以前后端配合的使用
**errorCode**
为主,这样有助于维护全局的异常提示信息。只有后端业务逻辑过于复杂、庞杂的场景才在后端设置**errorMessage**
。- 在复杂场景中,可以由后端在异常信息中设置
**traceId**
来协助前端进行分析。版权说明:本文由北京朗思云网科技股份有限公司原创,向互联网开放全部内容但保留所有权力。