在之前的设计中,我们只是通过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 successdata?: any; // response dataerrorCode?: string; // code for errorTypeerrorMessage?: string; // message display to usershowType?: number; // error display type: 0 silent; 1 message.warn; 2 message.error; 4 notification; 9 pagetraceId?: string; // Convenient for backend Troubleshooting: unique request IDhost?: 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**来协助前端进行分析。版权说明:本文由北京朗思云网科技股份有限公司原创,向互联网开放全部内容但保留所有权力。
