Axios 源码浅析(二)—— 请求核心与取消请求

537 阅读10分钟

引言

上一次我们初步认识了 Axios 的基本实例与基础配置,这一次我们主要分析一下他的核心请求逻辑以及取消请求逻辑

Axios 源码浅析(一)—— Axios 实例及配置

axios.request

接下来我们看看发请求的核心逻辑,这部分逻辑由 3 个部分组成,我们一层层看下去

request

// lib/core/Axios.js
var dispatchRequest = require('./dispatchRequest');

Axios.prototype.request = function request(config) {
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }

  config = mergeConfig(this.defaults, config);

  // Set config.method
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }

  // Hook up interceptors middleware
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

这段代码其实不难理解,我们分解一下

  • if (typeof config === 'string') {
      config = arguments[1] || {};
      config.url = arguments[0];
    } else {
      config = config || {};
    }
    
    config = mergeConfig(this.defaults, config);
    
    if (config.method) {
      config.method = config.method.toLowerCase();
    } else if (this.defaults.method) {
      config.method = this.defaults.method.toLowerCase();
    } else {
      config.method = 'get';
    }
    

    这里先做了一下兼容,如果 config 是字符串,那么就认为它是 config 对象的 url 属性,最终使用的还是 config 对象,保证数据结构的一致性,然后将传参与本实例的默认配置合并,并保证 method 属性一定有,至少是 'get'

    这里特意判断 method,一是为了格式化为小写,再一个默认配置里没有 method 的配置,所以为了不能为空,必须赋一个值,不过这里我不是很理解为何不给 method 一个默认值。。

  • var chain = [dispatchRequest, undefined];
    var promise = Promise.resolve(config);
    
    this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
      chain.unshift(interceptor.fulfilled, interceptor.rejected);
    });
    
    this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
      chain.push(interceptor.fulfilled, interceptor.rejected);
    });
    
    while (chain.length) {
      promise = promise.then(chain.shift(), chain.shift());
    }
    
    return promise;
    

    dispatchRequest 是真正发请求的抽象方法,我们会在下面详细说明,而 interceptors.requestinterceptors.response 则是请求和响应的拦截器,连续两个方法为一组,分别对应 thencatch 中,每个方法都返回一个 Promise,放到 chain 数组中,然后通过循环这个数组,生成一个 Promise 调用链,中间只要有一步抛异常,就会走到最近的 catch 中,如果 catch 返回了一个 resolve 状态的 Promise,那么调用链还可以继续往下走,有点迷惑的童鞋可以看下图帮助理解

拦截器调用链

这里虽然文档没有明说,但是分析代码可以看出,请求拦截器 interceptors.request 我们使用的时候是顺序加入到 InterceptorManager 类里,但是调用的时候却是顺序循环并通过 Array.prototype.unshift 到调用链数组里,那么也就是说第一个加入的拦截器会放在最后一个去调用,实际上也确实如此,这里可能有点小坑,如果加了多个请求拦截器规则又需要有顺序的话,一定要倒着写,不然无法得到想要的结果

dispatchRequest

module.exports = function dispatchRequest(config) {
  throwIfCancellationRequested(config);

  // Ensure headers exist
  config.headers = config.headers || {};

  // Transform request data
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  // Flatten headers
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers
  );

  utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );

  var adapter = config.adapter || defaults.adapter;

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

    // Transform response data
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

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

      // Transform response data
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

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

这里还有 Cancel 的代码,我们暂且略过,着重看请求部分

  • config.data = transformData(
      config.data,
      config.headers,
      config.transformRequest
    );
    

    这里通过 transformData 这个方法,将配置中的 config.transformRequest 方法,应用到 config.data 中,关于 transformRequest,可以参见文档

  • config.headers = utils.merge(
      config.headers.common || {},
      config.headers[config.method] || {},
      config.headers
    );
    
    utils.forEach(
      ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
      function cleanHeaderConfig(method) {
        delete config.headers[method];
      }
    );
    

    这部分合并了不同来源的 headers,都有什么来源呢,代码里也能很清晰的看到,包括通用配置 config.headers.common、特定方法的配置 config.headers[config.method](也就是config.headers.get / config.headers.post 等等....),以及当下请求传来的 headers 配置,最后,因为 commongetpost 等等这些配置都写在 headers 里,发请求时是不需要的,所以通通删了,当然这里 merge 方法是个深拷贝,所以随便删,不影响原对象

  • return adapter(config).then(function onAdapterResolution(response) {
      throwIfCancellationRequested(config);
    
      // Transform response data
      response.data = transformData(
        response.data,
        response.headers,
        config.transformResponse
      );
    
      return response;
    }, function onAdapterRejection(reason) {
      if (!isCancel(reason)) {
        throwIfCancellationRequested(config);
    
        // Transform response data
        if (reason && reason.response) {
          reason.response.data = transformData(
            reason.response.data,
            reason.response.headers,
            config.transformResponse
          );
        }
      }
    
      return Promise.reject(reason);
    });
    

    最后这部分也很好理解,调用适配器,把 config 传过去,然后处理 then,或者 catch 步骤,这里对结果应用了 config.transformResponse 方法

adapter

发请求的最终过程,还是在不同的适配器里实现,因为我个人用 Node 比较少,这里就看一下浏览器的适配器,也就是 lib/adapters/xhr.js

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    var requestData = config.data;
    var requestHeaders = config.headers;

    // formData 时删除 Content-Type
    if (utils.isFormData(requestData)) {...}

    var request = new XMLHttpRequest();

    // 设置 header Authorization
    if (config.auth) {...}

    var fullPath = buildFullPath(config.baseURL, config.url);
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);

    // 设置超时时间
    request.timeout = config.timeout;

    // 监听 xhr 各种事件
    request.onreadystatechange = function handleLoad() {...};
    request.onabort = function handleAbort() {...};
    request.onerror = function handleError() {...};
    request.ontimeout = function handleTimeout() {...};

    // 应对 xsrf/csrf 攻击,可以配置将 cookie 放在 header 中
    if (utils.isStandardBrowserEnv()) {...}

    // 将其他 config.headers 设置到 header 中
    if ('setRequestHeader' in request) {...}

    // 设置 withCredentials 属性
    if (!utils.isUndefined(config.withCredentials)) {...}

    // 设置 responseType
    if (config.responseType) {...}

    // 设置上传文件的进度
    if (typeof config.onDownloadProgress === 'function') {...}
    if (typeof config.onUploadProgress === 'function' && request.upload) {...}

    // 取消请求的操作
    if (config.cancelToken) {...}

    // 请求主体
    if (!requestData) {
      requestData = null;
    }

    // 发请求
    request.send(requestData);
  });
};

这里我将大部分代码都省略了,这样对整个适配过程更加清楚,基本的过程我都标注在上面代码里的注释了,接下来我们一步一步的细看

  • if (utils.isFormData(requestData)) {
      delete requestHeaders['Content-Type']; // Let the browser set it
    }
    

    如果是 FormData,就删除 Content-Type,让浏览器自己设置,这里一般都会设置成 multipart/form-data

  • if (config.auth) {
      var username = config.auth.username || '';
      var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
      requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
    }
    

    这里提供了一个快速设置 HTTP 认证的方法,设置方法就是通过用户名和密码,在请求头里增加 Authorization 字段,值为 Basic 加 base64 编码后的用户名密码字符串,具体可以参见 HTTP 身份认证

  • var fullPath = buildFullPath(config.baseURL, config.url);
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
    

    这里初始化一个请求,包括请求方法,请求地址,是否为异步等等,请求方法全部转为小写,请求地址是参数和地址拼起来的,就如:xxx?a=b&c=d

  • request.onreadystatechange = function handleLoad() {
      if (!request || request.readyState !== 4) {
        return;
      }
    
      // The request errored out and we didn't get a response, this will be
      // handled by onerror instead
      // With one exception: request that using file: protocol, most browsers
      // will return status as 0 even though it's a successful request
      if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
        return;
      }
    
      // Prepare the response
      var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
      var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;
      var response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config: config,
        request: request
      };
    
      settle(resolve, reject, response);
    
      // Clean up request
      request = null;
    };
    
    module.exports = function settle(resolve, reject, response) {
      var validateStatus = response.config.validateStatus;
      if (!response.status || !validateStatus || validateStatus(response.status)) {
        resolve(response);
      } else {
        reject(createError(
          'Request failed with status code ' + response.status,
          response.config,
          null,
          response.request,
          response
        ));
      }
    };
    

    这里主要看一下 onreadystatechange ,也就是请求响应之后的处理,我们注意到在判断 status 的时候有一句注释,很重要,就是说:如果请求发生错误了,是通过 onerror 这个 handler 来处理的,但是有一种例外,那就是用 file: 发起的请求,大多数浏览器会返回 status:0,即使这个请求成功了,我们知道,一般 HTTP 状态码,200 表示成功,所以,这里代码特殊判断了一下

    其余的就正常处理,将 resolvereject 交给 settle 方法来处理,这里判断逻辑也可以定义自己的 validateStatus 方法

    最后还有几个额外的处理,虽然不影响主流程,但是还是可以看一下

  • if (utils.isStandardBrowserEnv()) {
      // Add xsrf header
      var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
        cookies.read(config.xsrfCookieName) :
        undefined;
    
      if (xsrfValue) {
        requestHeaders[config.xsrfHeaderName] = xsrfValue;
      }
    }
    

    如果你配置了 xsrfHeaderNamexsrfCookieName 这个属性,那么在发请求时,会自动读取 cookie 中的相应值并带到 header

  • var requestHeaders = config.headers;
    if ('setRequestHeader' in request) {
      utils.forEach(requestHeaders, function setRequestHeader(val, key) {
        if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
          // Remove Content-Type if data is undefined
          delete requestHeaders[key];
        } else {
          // Otherwise add header to the request
          request.setRequestHeader(key, val);
        }
      });
    }
    

    如果设置了 headers 属性,则设置到请求头里,注意,这里如果请求体没有数据的话,会直接删掉 content-type

最后发送 ajax 请求,如果没有 requestData ,则 send(null)

取消一个请求

之前的几个代码里,我们都跳过了取消请求相关的处理逻辑,因为跟主题的逻辑关系不大,在这里,我们统一解析一下

用法

我们先回顾一下,Cancel 这个功能是怎么用的,可能大部分人都没用过

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function (thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // handle error
  }
})

// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');

首先,通过 CancelToken.source() 方法生成一个 source 对象,source 对象里有两个属性,一个是 token,一个是 cancel 方法,请求的时候将 token 传到配置里,即可在任意时刻通过调用 cancel 方法来取消请求

所以,这个 source 就是取消的核心

Cancel 核心代码

var Cancel = require('./Cancel');

function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }

  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;
  executor(function cancel(message) {
    if (token.reason) {
      return;
    }

    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });
}

CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason;
  }
};

CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};

module.exports = CancelToken;

解析:

CancelToken.source 方法,通过 CancelToken 生成一个 token,并且生成的同时, CancelToken 可以传一个回调函数,将某些方法绑定到了 cancel 属性上

继续看 CancelToken 工厂,通过 new 操作符调用的时候,会返回实例本身,也就是说,token 就是 CancelToken 的一个实例对象

在初始化过程中,token 对象会有 promisereason 两个属性

首先将 resolvePromise 变量绑定为一个 promise 属性的 resolve 方法,接着在回调函数中传一个 cancel 方法(也就是 CancelToken.source().cancel 方法),当 cancel 被调用的时候,会 resolve Promise,同时,token 这个对象中的 reason 属性也可能会有值,有值的话即代表已经取消过了,可以防止重复使用

token 对象还有一个 throwIfRequested 方法,即通过判断是否有 reason 值,来抛一个异常,使请求直接走到 catch 阶段

两外两个文件 lib/cancel/Cancel.jslib/cancel/isCancel.js 这里就不详细说明了,代码很少,基本是用某个实例的静态属性用来判断是否已经取消的逻辑,大家可以自行看一下

请求中使用 Cancel

之前我们分析发请求的过程中,出现了 Cancel 逻辑,一起将这部分补完

// lib/axios.js
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');

这里主要是导出了生产 source 的工厂 CancelToken

// lib/core/dispatchRequest.js
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}

function dispatchRequest(config) {
  throwIfCancellationRequested(config);
  ...

  return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);
    ...
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);
      ...
    }
    return Promise.reject(reason);
  });
}

这里在发请求的初始化、请求成功、请求失败时都通过 throwIfRequested 判断了用户是否调用过 cancel 方法,一旦判断调用过,直接抛出异常,走到 catch 流程,不管请求状态如何都不去处理了

throwIfRequested 方法只有当 tokenreason 属性的时候,才会抛异常,当没有调用 cancel 方法的时候,token 永远没有 reason 属性,因此 throwIfRequested 可以在任何时候调用,而不用担心请求是否被取消

// lib/adapters/xhr.js 
if (config.cancelToken) {
  // Handle cancellation
  config.cancelToken.promise.then(function onCanceled(cancel) {
    if (!request) {
      return;
    }

    request.abort();
    reject(cancel);
    // Clean up request
    request = null;
  });
}

在适配器里,初始化 ajax 对象的时候,绑定一个 promise,大家还记得 token 对象里有一个 promise 属性吧,就是用在这里的,当这个 promiseresolve 的时候,也就是用户调用了 cancel 方法,这里会通过闭包获取到 ajax 对象,然后调用 request.abort() ,这样在 xhr 流程中,也中断了请求的继续发送,同时将整个适配器的状态置为 reject

到这里,请求的所有阶段就都有了 cancel 的参与,在任何时候取消,都可以直接走入 reject 流程

结语

以上就是 Axios 的核心功能,其实逻辑并不难理解,主要是封装思路以及一些设计模式值得我们学习,另外,也可以了解一些请求过程中的坑(比如:file 开头的请求),以后在使用的过程中,如果遇到了文档里看不到的问题,不妨在源码里寻找答案,如果有新的想法,也可以直接去 GitHub 提需求,希望大家不要畏惧看源码,毕竟,源码写的要比我们接手的项目更加优美,不是吗?