Axios添加Retry

912 阅读4分钟

Axios原理

Axios的使用方式

import axios form 'axios'

// 第一种 axios(config)
axios({
  method: 'post',
  url: '/user/12345',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
});

// 第二种 axios(url[,config])
axios('/user/12345');

// 第三种 axios.[methods]
axios.request(config)
axios.get(url[,config])


// 第四种 创建axios实例
const instance = axios.create({
  baseURL: 'https://some-domain.com/api/',
  timeout: 1000,
  headers: {'X-Custom-Header': 'foobar'}
});

相关实现


/**
 * Create an instance of Axios
 *
 * @param {Object} defaultConfig The default config for the instance
 *
 * @returns {Axios} A new instance of Axios
 */
function createInstance(defaultConfig) {
  const context = new Axios(defaultConfig); //初始化axios实例
  
  // 绑定context的this为Axios.request
  // 实现axios(config)
  const instance = bind(Axios.prototype.request, context);
  
  // 给instance(绑定Axios.request的axios实例)挂载Axios.prototype
  // 实现axios.get() axios.post()等方法调用
  utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});

  // 给instance绑定context上的方法
  utils.extend(instance, context, {allOwnKeys: true});

  // 添加create方法 使得用户可以自己创建axios实例
  instance.create = function create(instanceConfig) {
    return createInstance(mergeConfig(defaultConfig, instanceConfig));
  };

  return instance;
}

// 创建axios实例导出
const axios = createInstance(defaults);

当我们调用axios()的方法的时候,事实上是调用了Axios.prototype.request方法,create创造的实例最终也是调用了Axios.prototype.request方法

// Axios.prototype.request
class Axios {

    request(configOrUrl, config){
         // 判断configOrUrl类型,合并配置
         if (typeof configOrUrl === 'string') {
              // 对应 axios(url,{})形式
              config = config || {};
              config.url = configOrUrl;
          } else {
              // 对应axios({})这种形式
              config = configOrUrl || {};
          }
          // 合并配置
          config = mergeConfig(this.defaults, config);
          
          // ...
    }
    
}


// 为Axios设置请求方法 使得满足axios.get axios.post等使用方式 最终都是调用 axios.request
utils.forEach(['delete', 'get', 'head', 'options'], 
  function forEachMethodNoData(method) {
      /*eslint func-names:0*/
      Axios.prototype[method] = function(url, config) {
        return this.request(mergeConfig(config || {}, {
          method,
          url,
          data: (config || {}).data
        }));
      };
});

utils.forEach(['post', 'put', 'patch'],
 function forEachMethodWithData(method) {
  /*eslint func-names:0*/

  function generateHTTPMethod(isForm) {
    return function httpMethod(url, data, config) {
      return this.request(mergeConfig(config || {}, {
        method,
        headers: isForm ? {
          'Content-Type': 'multipart/form-data'
        } : {},
        url,
        data
      }));
    };
  }

  Axios.prototype[method] = generateHTTPMethod();

  Axios.prototype[method + 'Form'] = generateHTTPMethod(true);
});

关于axios的多种使用方式通过上述源码逻辑,最终可以发现都会调用Axios.prototype.request方法。那么可以推断出我们日常写的请求和相应拦截器也是在request里面进行处理的。

// 注册拦截器
// axios.interceptors.request.use()
class Axios {
  constructor(instanceConfig) {
    this.defaults = instanceConfig;
    // 注册拦截器实现
    this.interceptors = {
      request: new InterceptorManager(),
      response: new InterceptorManager()
    };
}
  
  class InterceptorManager {
      constructor() {
        this.handlers = [];
      }
      use(fulfilled, rejected, options) {
        this.handlers.push({
          fulfilled,
          rejected,
          synchronous: options ? options.synchronous : false,
          runWhen: options ? options.runWhen : null
        });
        return this.handlers.length - 1;
      }
      
      // ...
}



// 执行请求和拦截器并且保证拦截器的执行顺序

class Axios {
  request(){
  
      // ...
      
    // 转换请求拦截器
    const requestInterceptorChain = [];
    let synchronousRequestInterceptors = true;
    this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
      if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
        return;
      }

      synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;

      requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
    });
    // 转换响应拦截器
    const responseInterceptorChain = [];
    this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
      responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
    });
    
    
    let promise; // 处理请求的promise
    let i = 0;
    let len;

    if (!synchronousRequestInterceptors) {
      // 整合请求拦截器  请求处理 响应拦截器 形成一个数组
      const chain = [dispatchRequest.bind(this), undefined];
      chain.unshift.apply(chain, requestInterceptorChain);
      chain.push.apply(chain, responseInterceptorChain);
      len = chain.length;
      promise = Promise.resolve(config);
      
      // 循环chain
      while (i < len) {
        promise = promise.then(chain[i++], chain[i++]);
      }

      return promise;
    
  }
}

axios整个拦截器相关的处理都在Axios.prototype.request函数里面处理了 ,真正发送请求的处理是在dispatchRequest这个函数中处理的。

export default function dispatchRequest(config) {
  
  // 转换请求数据
  config.data = transformData.call(
    config,
    config.transformRequest
  );
  
  // 请求适配器 区分浏览器和node环境(通过是否存在xhr)
  const adapter = config.adapter || defaults.adapter;

  return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);

    // 转换响应数据
    response.data = transformData.call(
      config,
      config.transformResponse,
      response
    );

    response.headers = AxiosHeaders.from(response.headers);

    return response;
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);

      // 转换响应数据
      if (reason && reason.response) {
        reason.response.data = transformData.call(
          config,
          config.transformResponse,
          reason.response
        );
        reason.response.headers = AxiosHeaders.from(reason.response.headers);
      }
    }

    return Promise.reject(reason);
  });
}

dispatchReques发送请求的部分是在adapter里面处理的,在浏览器中的请求是通过xhr进行发送的。

// 创建xhr实例
const xhr = new XMLHttpRequest()

// 打开一个请求 get请求 路径为/path true为异步发送
xhr.open('get', '/path', true)

// 设置请求头信息
xhr.setRequestHeader('MyHeader', 'MyValue')

// 监听readyState变化
// readyState === 0 (UNSENT)没有初始化,还没调用xhr.open()方法
// readyState === 1 (OPEN) 调用xhr.open(), 还没调用xhr.send()方法
// readyState === 2 (HEADERS_RECEIVED)调用了send()方法,还没接收到服务器响应
// readState === 3  (LOADING) 接收到部分数据
// readState === 4  (DONE) 已经接收到全部数据
xhr.onreadystatechange = function () {
  if(xhr.readyState !== 4) {
    return  
  }
  if(xhr.status >= 200 && xhr.status < 400) {
    console.log(xhr.responseText)
  } else {
      // ... 异常处理
  }
}

// timeout 
xhr.timeout = 6000 //设置请求超时时间

xhr.ontimeout = () => {
  console.log('请求超时')
}


xhr.onabort = () => {
  console.log('取消请求')
}

xhr.onerror = () => {
  console.log('网络异常')
}

xhr.onload = () => {
  console.log('请求数据接受完成')
}

// axios是在这个事件中进行请求响应处理的
xhr.onloadend = () => {
   console.log('请求结束')
}

// 发送请求 参数为请求数据没有传null
xhr.send(null)

// 取消请求
xhr.abort()

Axios请求响应处理

function onloadend() {
      if (!request) {
        return;
      }
      // Prepare the response
      const responseHeaders = AxiosHeaders.from(
        'getAllResponseHeaders' in request && request.getAllResponseHeaders()
      );
      const responseData = !responseType || responseType === 'text' ||  responseType === 'json' ?
        request.responseText : request.response;
      const response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config,
        request
      };

      settle(function _resolve(value) {
        resolve(value);
        done();
      }, function _reject(err) {
        reject(err);
        done();
      }, response);

      // Clean up request
      request = null;
  }


function settle(resolve, reject, response) {
  const validateStatus = response.config.validateStatus;
  if (!response.status || !validateStatus || validateStatus(response.status)) {
    resolve(response);
  } else {
    reject(new AxiosError(
      'Request failed with status code ' + response.status,
      [AxiosError.ERR_BAD_REQUEST, AxiosError.ERR_BAD_RESPONSE][Math.floor(response.status / 100) - 4],
      response.config,
      response.request,
      response
    ));
  }
}

Retry

有了上面的axios运行原理和请求发送原理可以总结出,要是添加retry机制,可通过添加请求拦截器进行处理。

Axios进入reject情况

  • Axios请求流程进入reject状态原因可以为:
    • 请求取消
    • request.onabort = function handleAbort() {
            if (!request) {
              return;
            }
      
            reject(new AxiosError(
                'Request aborted', 
                 AxiosError.ECONNABORTED,
                 config, 
                 request
             ));
      
            // Clean up request
            request = null;
        };
      
    • 网络异常
    •  request.onerror = function handleError() {
            // Real errors are hidden from us by the browser
            // onerror should only fire if it's a network error
            reject(new AxiosError(
                'Network Error',
                 AxiosError.ERR_NETWORK, 
                 config, 
                 request
            ));
      
            // Clean up request
            request = null;
        };
      
    • 请求超时
    • request.ontimeout = function handleTimeout() {
            let timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';
            const transitional = config.transitional || transitionalDefaults;
            if (config.timeoutErrorMessage) {
              timeoutErrorMessage = config.timeoutErrorMessage;
            }
            reject(new AxiosError(
              timeoutErrorMessage,
              transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
              config,
              request));
      
            // Clean up request
            request = null;
       };
      
    • response.status || validateStatus || validateStatus(response.status)
function settle(resolve, reject, response) {
  const validateStatus = response.config.validateStatus;
  // response.status 表示服务器响应的HTTP状态码,如果服务器没有返回状态码,这个属性就默认
  // 是200 要是没有配置validateStatus的话 axios不会关心状态码
  if (!response.status || !validateStatus || validateStatus(response.status)) {
    resolve(response);
  } else {
    reject(new AxiosError(
      'Request failed with status code ' + response.status,
      [AxiosError.ERR_BAD_REQUEST, AxiosError.ERR_BAD_RESPONSE][Math.floor(response.status / 100) - 4],
      response.config,
      response.request,
      response
    ));
  }
}

response.status 表示服务器响应的HTTP状态码,如果服务器没有返回状态码,这个属性就默认是200 要是没有配置validateStatus的话 axios不会关心状态码。

测试

axios.defaults.validateStatus = (status)=>{
  console.log(status,'...testing')
  return false
}

axios.get('/')
  .then(function (response) {
    console.log(response,"-------success");
  })
  .catch(function (error) {
    console.log(error,'-----error');
 });

axios.defaults.validateStatus = (status)=>{
  console.log(status,'...testing')
  return true
}

axios.get('/')
  .then(function (response) {
    console.log(response,"-------success");
  })
  .catch(function (error) {
    console.log(error,'-----error');
 });

  • 因此当没有设置validateStatus的时候进入reject状态的只有
    • 请求取消
    • 网络异常
    • 请求超时

Retry机制可以针对这三种情况进行处理。

Retry拦截器

请求取消是主动结束请求,因此只需要针对请求异常、请求超时进行retry。

retry相关的属性有 请求次数、请求间隔时间。

// 把相关属性设置在axios.defaults.headers.common上
// 这里是所有请求错误都加重试机制 要是针对某一个请求需要在具体请求header上加属性
axios.defaults.headers.common['retry'] = 3 //重试请求次数
axios.defaults.headers.common['retryDelay'] = 1000 //重试请求间隔时间
axios.interceptors.response.use(undefined, err => {
  const config = err.config;

  if ((!config || !config.headers.retry)&&
      // 请求超时错误
      (error.message.indexOf('timeout')==-1 
      // 网络异常错误
      ||error.message.indexOf('Network Error')==-1)
  ) {
    return Promise.reject(err);
  }
  
  // 第一次重试设置_retryCount=0
  config.headers._retryCount = config.headers._retryCount || 0;

  // 请求次数超过设置的次数
  if (config.headers._retryCount >= config.headers.retry) {
    return Promise.reject(err);
  }
  
  // 请求次数加一
  config.headers._retryCount += 1;

  // 设置请求间隔 通过定时器来阻塞
  const backoff = new Promise(function (resolve) {
    setTimeout(function () {
      resolve();
    }, config.headers.retryDelay || 1);
  });

  return backoff.then(function () {
    // 请求重试
    return axios(config);
  });
});

测试(这里没有限制错误类型哈)

axios.defaults.validateStatus = (status)=>{
  console.log(status,'...testing')
  return false
}

axios.get('/',{
  headers:{
    retry:3,// 重试3次
    retryDelay:1000 // 重试间隔1s
  }
})
  .then(function (response) {
    console.log(response,"-------success");
  })
  .catch(function (error) {
    console.log(error,'-----error');
  });