如何封装一个好用的网络请求(二)-- 错误统一处理


theme: orange highlight: an-old-hope

引言

这一系列文章记录了我多年工作中积累的心得体会,旨在与大家分享经验,欢迎各位大佬指导,共同提升技术水平。

承接上篇

前文《后台经验系列(一)--网络封装(上)》已经介绍了网络请求封装的基本框架,接下来我们将深入探讨请求的生命周期及错误处理。

代码仓库在 案例一:网络封装 · Duck/EmpiricalCase - 码云 - 开源中国 (gitee.com),可以下载下来看完整代码

一、请求生命周期

在完善功能之前,让我们先来探讨请求的生命周期。

graph LR
发起请求 --> 请求拦截器 --> 实际请求 --> 响应拦截器 --> 请求成功 --> 业务代码
  1. 发起请求:业务代码发起请求。
  2. 请求拦截器:在实际发出请求之前,对请求进行校验、参数处理、请求头处理等操作,如果存在不符合逻辑的请求,可以直接取消。
  3. 实际请求:发出实际的网络请求。
  4. 响应拦截器:处理请求成功或失败后返回的数据,包括错误处理、数据校验等。
  5. 业务代码:处理请求并获得响应数据,继续业务逻辑。

针对以上生命周期,我们可以封装如下函数:

const baseUrl = 'http://localhost:8080';
function ajaxFunc({ url, type = 'get', data, headers } = config) {
  const realUrl = baseUrl + url;
  return new Promise((resolve, reject) => {
    // 请求拦截器
    if (requestInterceptor(config) === false) {
      return;
    }
    $.ajax({
      url: realUrl,
      type,
      data,
      success: function (responseData) {
        // 响应拦截器:请求成功
        const result = responseInterceptorSuccess(responseData);
        resolve(result);
      },
      error: function (response) {
        //  响应拦截器:请求失败
        const result = responseInterceptorError(response);
        reject(result);
      },
    });
  });
}

/**

  • 请求拦截器
  • @param {Object} config
  • @return false 表示取消请求
    */
    function requestInterceptor(config) {}

/**

  • 响应拦截器:请求成功,返回2xx的code
  • @param {Object} response
    */
    function responseInterceptorSuccess(responseData) {}

/**

  • 响应拦截器:请求失败,即返回除了2xx外的code
  • @param {Object} response
    */
    function responseInterceptorError(response) {}

二、错误处理-响应拦截器

封装网络请求的一个重要作用是统一处理错误。我将错误分为两类:网络状态码错误和业务逻辑错误。

  1. 网络状态码错误:如404,403,500等
  2. 业务逻辑错误:如1000代表权限错误、10001代表关键词错误等,但是statusCode是2xx的,代表网络是成功的

1. 响应拦截器:请求错误拦截处理

在处理网络请求时,我们需要对网络状态码进行拦截处理,例如处理离线、403等情况。 我们先在request文件夹下新建文件error.js,用来存储各种网络请求错误的处理代码

// src/utils/request/error.js
/**
 * 没网的处理逻辑
 */
function handleOffline(error) {
  Object.assign(error, {
    code: 0,
    message: '网络错误',
  });
  // 消息提醒,比如element的 message.error()
  console.log(error);
}

/**

  • 403的处理逻辑
    */
    function handle403(error) {
    Object.assign(error, {
    code: 403,
    message: ‘权限出错,请返回登录页面重新登录’,
    });
    // 消息提醒,比如element的 message.error()
    console.log(error);
    // 清除缓存,跳转登录页
    }

然后返回网络请求的index.js文件内,在 响应拦截器:请求错误拦截处理的函数内,添加一些代码

/**
 * 执行AJAX请求
 * @param {string} url 请求的URL
 * @param {string} type 请求类型(如GET、POST等)
 * @param {Object|string} data 要发送的数据
 * @returns {Promise} 返回一个Promise对象,成功时resolve响应结果,失败时reject错误对象
 */
const baseUrl = 'http://localhost:8080';
function ajaxFunc(config) {
  const { url, type = 'get', data, headers } = config;
  const realUrl = baseUrl + url;
  return new Promise((resolve, reject) => {
    // 请求拦截器
    if (requestInterceptor(config) === false) {
      return;
    }
    $.ajax({
      url: realUrl,
      type,
      data,
      success: function (res) {
        // 响应拦截器:请求成功
        const result = responseInterceptorSuccess(res);
        resolve(result);
      },
      error: function (err) {
        //  响应拦截器:请求失败
        responseInterceptorError(err);
        // 把响应数据返回出去,页面如果要处理,可以获取
        reject(err);
      },
    });
  });
}

/**

  • 请求拦截器
  • @param {Object} config
  • @return false 表示取消请求
    */
    function requestInterceptor(config) {
    return config;
    }

/**

  • 响应拦截器:请求成功,返回2xx的code
  • @param {Object} response
    */
    function responseInterceptorSuccess(responseData) {
    return responseData;
    }

/**

  • 响应拦截器:请求失败,即返回除了2xx外的code
  • @param {Object} response
    */
    function responseInterceptorError(response) {
    const httpCode = response.status;
    const error = {
    type: ‘http’,
    code: httpCode,
    message: ‘请求出错’,
    };
    if (!window.navigator.onLine) {
    // 网路错误
    handleOffline(error);
    return;
    }
    if (httpCode === 403) {
    handle403(error);
    return;
    }
    // 后面可以接上其他的处理逻辑

// 最后统一处理
// 消息提醒,比如element的 message.error()
console.log(error);
}

我们看看效果,我自己用express写了一个服务器,写了一个接口固定返回403

2. 响应拦截器:请求成功拦截处理

除了处理网络状态码错误外,我们还需要处理业务逻辑错误,如权限错误、关键词错误等。

我们在 error.js 文件里面新增逻辑code错误的处理代码

/**
 * 敏感词的处理逻辑
 */
function handle1002(error) {
  Object.assign(error, {
    code: 1002,
    message: error.message,
  });
  // 消息提醒,比如element的 message.error()
  console.log(error);
  //   清除缓存,跳转登录页
}

然后返回网络请求的index.js文件内,在 响应拦截器:请求成功拦截处理的函数内,添加一些代码

// ajax的回调success里面,修改一下代码
success: async function (responseData) {
        // 响应拦截器:请求成功
        try {
          const result = await responseInterceptorSuccess(responseData);
          resolve(result);
        } catch (error) {
          reject(error);
        }
      },

/**

  • 响应拦截器:请求成功,返回2xx的code
  • @param {Object} response
    */
    function responseInterceptorSuccess(responseData) {
    if (responseData.code === 0) {
    return Promise.resolve(responseData);
    }
    if (responseData.code === 1001) {
    handle1002(responseData);
    return Promise.reject(responseData);
    }
    // 最后统一处理
    // 消息提醒,比如element的 message.error()
    console.log(responseData);
    return Promise.reject(responseData);
    }

在业务代码没变的情况下,我们便完成了这次错误的处理,如图

那么业务代码完成可以去除错误的处理,那么代码便会特别精简

      async function a() {
        const res = await ajaxFunc({
          url: '/codeError',
          type: 'get',
          data: {
            name: 'zhangsan',
            age: 18,
          },
        });
        console.log('业务代码--请求成功返回:', res);
      }

3. 兼容文件流

当然了,接口不一定都是返回json格式,还有可能会有文件流,比如导出接口、验证码接口等,我们需要对这些接口进行兼容,只需要很简单的两行代码即可 在 responseInterceptorSuccess 函数里面,新增

/**
 * 响应拦截器:请求成功,返回2xx的code
 * @param {Object} response
 */
function responseInterceptorSuccess(responseData) {
  // 判断请求是否一个文件流
  if (typeof responseData !== 'object') {
    return Promise.resolve(responseData);
  }
  // 成功
  if (responseData.code === 0) {
    return Promise.resolve(responseData);
  }
   // 逻辑报错
  if (responseData.code === 1001) {
    handle1002(responseData);
    return Promise.reject(responseData);
  }
  //   最后统一处理
  // 消息提醒,比如element的 message.error()
  console.log(responseData);
  return Promise.reject(responseData);
}

完整代码

通过上述的错误处理机制,我们可以将错误统一处理,使得业务代码更加简洁清晰,提高了代码的可维护性和稳定性。

我这里用的是普通的html+js,如果用的是vue等脚手架,请手动引入文件依赖 import { } from

// src/utils/request/error.js
/**
 * 没网的处理逻辑
 */
function handleOffline(error) {
  Object.assign(error, {
    code: 0,
    message: '网络错误',
  });
  // 消息提醒,比如element的 message.error()
  console.log(error);
}

/**

  • 403的处理逻辑
    */
    function handle403(error) {
    Object.assign(error, {
    code: 403,
    message: ‘权限出错,请返回登录页面重新登录’,
    });
    // 消息提醒,比如element的 message.error()
    console.log(error);
    // 清除缓存,跳转登录页
    }

/**

  • 敏感词的处理逻辑
    */
    function handle1002(error) {
    Object.assign(error, {
    code: 1002,
    message: error.message,
    });
    // 消息提醒,比如element的 message.error()
    console.log(error);
    // 清除缓存,跳转登录页
    }
// src/utils/request/index.js
/**
 * 执行AJAX请求
 * @param {string} url 请求的URL
 * @param {string} type 请求类型(如GET、POST等)
 * @param {Object|string} data 要发送的数据
 * @returns {Promise} 返回一个Promise对象,成功时resolve响应结果,失败时reject错误对象
 */
const baseUrl = 'http://localhost:8080';
function ajaxFunc(config) {
  const { url, type = 'get', data, headers } = config;
  const realUrl = baseUrl + url;
  return new Promise((resolve, reject) => {
    // 请求拦截器
    if (requestInterceptor(config) === false) {
      return;
    }
    $.ajax({
      url: realUrl,
      type,
      data,
      success: async function (responseData) {
        // 响应拦截器:请求成功
        try {
          const result = await responseInterceptorSuccess(responseData);
          resolve(result);
        } catch (error) {
          reject(error);
        }
      },
      error: function (response) {
        //  响应拦截器:请求失败
        responseInterceptorError(response);
        // 把响应数据返回出去,页面如果要处理,可以获取
        reject(response.responseJSON);
      },
    });
  });
}

/**
 * 请求拦截器
 * @param {Object} config
 * @return false 表示取消请求
 */
function requestInterceptor(config) {
  return config;
}

/**
 * 响应拦截器:请求成功,返回2xx的code
 * @param {Object} response
 */
function responseInterceptorSuccess(responseData) {
 // 判断请求是否一个文件流
  if (typeof responseData !== 'object') {
    return Promise.resolve(responseData);
  }
  // 成功
  if (responseData.code === 0) {
    return Promise.resolve(responseData);
  }
   // 逻辑报错
  if (responseData.code === 1001) {
    handle1002(responseData);
    return Promise.reject(responseData);
  }
  //   最后统一处理
  // 消息提醒,比如element的 message.error()
  console.log(responseData);
  return Promise.reject(responseData);
}

/**
 * 响应拦截器:请求失败,即返回除了2xx外的code
 * @param {Object} response
 */
function responseInterceptorError(response) {
  const httpCode = response.status;
  const error = {
    type: 'http',
    code: httpCode,
    message: '请求出错',
  };
  if (!window.navigator.onLine) {
    // 网路错误
    handleOffline(error);
    return;
  }
  if (httpCode === 403) {
    handle403(error);
    return;
  }
  //   后面可以接上其他的处理逻辑

  //   最后统一处理
  // 消息提醒,比如element的 message.error()
  console.log(error);
}

使用

    
    
    
      async function a() {
        const res = await ajaxFunc({
          url: '/codeError',
          type: 'get',
          data: {
            name: 'zhangsan',
            age: 18,
          },
        });
        console.log('业务代码--请求成功返回:', res);
      }
  a();
</script>


这是一个从 https://juejin.cn/post/7368761530383728659 下的原始话题分离的讨论话题