在之前的设计中,我们只是通过try. .. catch机制来捕获异常并简单提示,其他的处理都交给脚手架进行默认处理。在实现其他功能之前,我们对异常处理过程进行改进。具体包括如下的内容:

  • 后端接口返回格式规范
  • 脚手架预定义的全局异常处理
  • 改进的全局异常处理过程
  • 模拟典型出错场景并查看前端反应

13.1 后端接口返回格式规范

在前两章,我们用umi的request函数发起网络请求的时候,在Mock中分别定义了如下的两种返回信息

  1. ///api/member/queryAll返回的数据
  2. const result = {
  3. success: true,
  4. data: dataSource,
  5. total: memberListDataSource.length,
  6. pageSize,
  7. current,
  8. };
  9. const result = {
  10. success: true,
  11. };

两个返回这里遵照了umi-request中对后端接口返回格式的约定(详见@umijs/plugin-request介绍),下面是具体定义

  1. interface ErrorInfoStructure {
  2. success: boolean; // if request is success
  3. data?: any; // response data
  4. errorCode?: string; // code for errorType
  5. errorMessage?: string; // message display to user
  6. showType?: number; // error display type: 0 silent; 1 message.warn; 2 message.error; 4 notification; 9 page
  7. traceId?: string; // Convenient for backend Troubleshooting: unique request ID
  8. host?: string; // Convenient for backend Troubleshooting: host of current access server
  9. }

不管是在Mock模拟的时候还是在服务器实际实现响应处理时,都应该依照此规范进行设计。

当然,我们不必完全严格按照规范从后端(服务器或Mock)返回全部的内容,但至少要通过success属性声明处理结果。

13.2 阅读预定义的全局异常处理

打开src/app.tsx,我们可以看到脚手架预定义的全局异常处理过程(本节的代码都是已经生成的,我们只是阅读和分析)。

首先是和常见HTTP响应状态码有关的出错信息定义

  1. const codeMessage = {
  2. 200: '服务器成功返回请求的数据。',
  3. 201: '新建或修改数据成功。',
  4. 202: '一个请求已经进入后台排队(异步任务)。',
  5. 204: '删除数据成功。',
  6. 400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
  7. 401: '用户没有权限(令牌、用户名、密码错误)。',
  8. 403: '用户得到授权,但是访问是被禁止的。',
  9. 404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
  10. 405: '请求方法不被允许。',
  11. 406: '请求的格式不可得。',
  12. 410: '请求的资源被永久删除,且不会再得到的。',
  13. 422: '当创建一个对象时,发生一个验证错误。',
  14. 500: '服务器发生错误,请检查服务器。',
  15. 502: '网关错误。',
  16. 503: '服务不可用,服务器暂时过载或维护。',
  17. 504: '网关超时。',
  18. };

然后是给request用的异常处理函数

  1. /**
  2. * 异常处理程序
  3. * @see https://beta-pro.ant.design/docs/request-cn
  4. */
  5. export const request: RequestConfig = {
  6. errorHandler: (error: ResponseError) => {
  7. const { response } = error;
  8. if (response && response.status) {
  9. const { status, statusText, url } = response;
  10. const requestErrorMessage = '请求错误';
  11. const errorMessage = `${requestErrorMessage} ${status}: ${url}`;
  12. const errorDescription = codeMessage[status] || statusText;
  13. notification.error({
  14. message: errorMessage,
  15. description: errorDescription,
  16. });
  17. }
  18. if (!response) {
  19. notification.error({
  20. description: '您的网络发生异常,无法连接服务器',
  21. message: '网络异常',
  22. });
  23. }
  24. throw error;
  25. },
  26. };

这里设置了全局网络请求的错误处理函数。在实际的项目中,可以把reque的拦截器interceptors也配置在这里,在发出网络请求前附加上Token或Cookie(详细的说明参见教程安全篇部分)。

异常处理函数的入口参数是一个出错数据对象,下面是他在umi-request中的定义

  1. interface Error {
  2. name: string;
  3. message: string;
  4. stack?: string;
  5. }
  6. export interface ResponseError<D = any> extends Error {
  7. name: string;
  8. data: D;
  9. response: Response;
  10. request: {
  11. url: string;
  12. options: RequestOptionsInit;
  13. };
  14. type: string;
  15. }

在异常处理函数内部首先判断这个出错数据对象中的response属性是否有意义,如果是的话代表目标服务器给出了常规的HTTP状态反馈,此时优先从中解析出错信息。我们首先看一下Response的定义(根据场景的不同,有些属性可能不会被赋值)

  1. /** This Fetch API interface represents the response to a request. */
  2. interface Response extends Body {
  3. headers: Headers;
  4. ok: boolean;
  5. redirected: boolean;
  6. status: number;
  7. statusText: string;
  8. trailer: Promise<Headers>;
  9. type: ResponseType;
  10. url: string;
  11. clone(): Response;
  12. }

下面是response属性有意义时在异常处理函数中的做法

  1. if (response && response.status) {
  2. // 取出response中的HTTP状态码、状态信息和原始请求的地址
  3. const { status, statusText, url } = response;
  4. const requestErrorMessage = '请求错误';
  5. const errorMessage = `${requestErrorMessage} ${status}: ${url}`;
  6. // response.status存的是HTTP状态码,如果已经定义了对应的说明就使用该说明,
  7. // 否则就使用response..statusText的内容
  8. const errorDescription = codeMessage[status] || statusText;
  9. //把出错信息、HTTP状态码和原始请求地址一起告知用户
  10. notification.error({
  11. message: errorMessage,
  12. description: errorDescription,
  13. });
  14. }

当没有获得服务器的响应(如地址错误、服务器超时等)或服务器给出正常响应,但在返回数据中告知发生了业务方面的错误(如不存在指定的数据、业务逻辑错误等)时,error.response将是空值,预定义的异常处理程序按照如下的方式进行处理:

  1. if (!response) {
  2. notification.error({
  3. description: '您的网络发生异常,无法连接服务器',
  4. message: '网络异常',
  5. });
  6. }

显然这里的处理比较粗糙,我们需要更详细、更友好的处理过程。

13.3 改进的异常处理过程

本节我们修改预定义的全局异常处理,让机制更加合理、友好。

13.3.1 response为空时的异常处理

当没有获得服务器的响应(如地址错误、服务器超时等)或服务器给出正常响应,但在返回数据中告知发生了业务方面的错误(如不存在指定的数据、业务逻辑错误等)时,error.response将是空值,这部分是我们优化的重点。首先在函数体外定义我们自己的提示信息

  1. const appExceptionMessage = {
  2. 0: '未知的错误。',
  3. 1: '服务器一直没有反应,可能是比较忙,请等一会儿再试一下。',
  4. 2: '哎呀,没有找到你要处理的数据,刷新一下页面再试试看吧。',
  5. };

这里我们只定义了3个值。在实际的应用开发中,根据后端业务的实际情况,可以增加更多的内容。
把预定义的如下代码

  1. if (!response) {
  2. notification.error({
  3. description: '您的网络发生异常,无法连接服务器',
  4. message: '网络异常',
  5. });
  6. }

用下面优化后的代码代替

  1. if (!response) {
  2. let option = {
  3. description : '您的网络发生异常,无法连接服务器',
  4. message : '网络异常',
  5. }
  6. //处于开发状态的时候,将error的内容解析后输出在控制台上
  7. if(isDev)
  8. console.log('error', JSON.stringify(error,null,2));
  9. switch(error.name) {
  10. case 'RequestError':
  11. //请求的时候就发生了错误,目前只针对超时进行单独的处理
  12. if(error.type === "Timeout") {
  13. option.description = appExceptionMessage[1];
  14. option.message = '系统忙';
  15. }
  16. break;
  17. case 'BizError':
  18. //error.name是'BizError'意味着错误是由于返回的数据里面success为false而被抛出了异常
  19. //这是umi-request的定义的机制
  20. const {errorCode = 0, errorMessage = undefined} = error.data;
  21. option.description = (errorMessage != undefined)? errorMessage : (appExceptionMessage[errorCode] || appExceptionMessage[0]);
  22. option.message = '操作失败';
  23. break;
  24. }
  25. notification.error(option)
  26. }

13.3.2 更友好地显示HTTP异常状态信息

在反馈HTTP异常状态时,预订的信息内容和展示方式是纯粹从技术开发的角度来设计的,对于普通的软件用户并不友好,因此我们需要对此进行改进。

首先修改codeMessage的定义(这里只修改了期中的一部分,其他的内容可以视实际情况修改)

  1. const codeMessage = {
  2. ...
  3. 404: '请求了不存在的资源,如果反复出现此现象,请联系软件服务商。',
  4. ...
  5. 500: '服务器遇到了一些麻烦,请稍后尝试或联系软件服务商。',
  6. 502: '中间服务器和远端服务器之间遇到了一些麻烦,请稍后尝试或联系软件服务商。',
  7. 503: '服务器临时有点儿忙,请稍后尝试或联系软件服务商。',
  8. 504: '中间服务器和没有联系到远端服务器,请稍后尝试或联系软件服务商。',
  9. };

然后修改显示状态信息的逻辑和格式,不提示401( Unauthorized)状态(软件会直接跳转到登录页面),并且只有在开发状态才向软件用户提示当前访问的网络地址

  1. - const errorMessage = `${requestErrorMessage} ${status}: ${url}`;
  2. + const errorMessage = `${requestErrorMessage} ${status}: ${isDev? url: ''}`;
  1. + if(status != 401) {
  2. notification.error({
  3. message: errorMessage,
  4. description: errorDescription,
  5. });
  6. + }

13.3.4 捕获请求异常后不再主动提示

改进全局异常处理之后,通常我们用try ... catch捕获请求异常以后,无须再主动提示信息(除非业务逻辑有全局处理无法覆盖的情况)。现在我们删除pages/MemberList/index.tsx中的handleDeleteCurrenthandleDeleteList函数的出错提示:

  1. hide();
  2. - message.error('删除失败,请重试');
  3. return false;

13.4 模拟典型出错场景

现在我们模拟集中典型的出错场景并查看前端的反应情况。

13.4.1 请求访问不存在的服务器

src/services/api/member.ts中的请求地址改成一个不存在的服务器

  1. export async function deleteMember(params: {}) {
  2. ...
  3. return request('http://na.51gh.com.cn/api/member/delete', {

执行删除一条数据的操作将得到下面的提示信息
image.png

13.4.2 请求访问服务器上不存在的资源

src/services/api/member.ts中的请求地址改成一个不存在的资源

  1. export async function deleteMember(params: {}) {
  2. ...
  3. return request('/api/member/delete1234', {

执行删除一条数据的操作,根据版本的不同,将得到下面两种提示信息中的一种
image.png
image.png

13.4.3 服务器反馈响应超时

在执行request的时候,可以在配置参数值指定服务器请求超时的时间。因为如果设置的不合导致不应有的异常(详见下章的有关内容)所以在全局设置里面没有指定该参数的默认值,为模拟超时的场景,我们在src/services/api/member.ts中给request设置一个较短的超时时间(500毫秒):

  1. export async function deleteMember(params: {}) {
  2. ...
  3. return request('/api/member/delete', {
  4. method: 'GET',
  5. + timeout: 500,

然后在mock/member.ts中定义一个用于代码延迟的函数

  1. //让异步函数暂停指定时间的函数,参数为毫秒
  2. const waitTime = (microsecond: number = 100) => {
  3. return new Promise((resolve) => {
  4. setTimeout(() => {
  5. resolve(true);
  6. }, microsecond);
  7. });
  8. };

把请求处理函数改成异步函数并且中间等待较长时间

  1. -function deleteRow(req: Request, res: Response, u: string) {
  2. +async function deleteRow(req: Request, res: Response, u: string) {
  3. const { id } = req.query;
  4. + await waitTime(1000);
  5. memberListDataSource = memberListDataSource.filter( data => data.id != id);
  6. const result = {
  7. success: true,
  8. }
  9. return res.json(result);
  10. }

执行删除一条数据的操作将得到下面的提示信息
image.png

13.4.4 后端应用反馈出错信息

当后端返回数据对象的success属性值为false时,全局异常处理过程认为后端出现了异常(出错)状况,会自动在前端抛出异常,把后端传过来的数据打包到ResponseError对象的data属性并激发errorHandler过程(此时ResponseError对象的name值固定为BizError。

根据我们优化后的全局异常处理过程,后端传递过来的errorMessage的优先级比errorCode,也就是如果errorMessage不为空的的情况下,errorCode将被忽略。下面我们先模拟这一场景。

mock/member.tsdeleteRow中,返回如下的数据

  1. const result = {
  2. success: false,
  3. errorCode: 2,
  4. errorMessage: '亲,我没有找到你要删除的东西',
  5. }

执行删除一条数据的操作将得到下面的提示信息(内容时后端传递过来的)
image.png

如果后端没有设置errorMessage,那么前端的全局异常处理过程将尝试根据errorCode反馈信息

mock/member.tsdeleteRow中,返回如下的数据

  1. const result = {
  2. success: false,
  3. errorCode: 2,
  4. }

执行删除一条数据的操作将得到下面的提示信息(内容时在前端预定义的)
image.png

提示:

  1. 具体开发的时候,应该尽可能以前后端配合的使用**errorCode**为主,这样有助于维护全局的异常提示信息。只有后端业务逻辑过于复杂、庞杂的场景才在后端设置**errorMessage**
  2. 在复杂场景中,可以由后端在异常信息中设置**traceId**来协助前端进行分析。

版权说明:本文由北京朗思云网科技股份有限公司原创,向互联网开放全部内容但保留所有权力。