axios网络请求框架源码解析

早期axios0.1.0版本做了对IE浏览器与包含XmlHttpRequest的浏览器的支持。并且做了对请求参数拼接、Json对象序列化等基本功能。

到0.19.0版本时,内部请求已经变为了在Node环境下与主流浏览器的支持,其中Node环境下支持http请求与https请求。并且支持取消、拦截。

Axios执行开始之初,首先执行createInstance方法,createInstance方法用来创建一个新的Axios实例。但这里奇怪的是,返回的示例并不是真正new出来的实例,而是一个幻影。实际在执行时,内部代码的指向还是内部的Axios对象。Axios内部使用了wrap来表示各个方法,可能真的是为了将真实的Axios实例隐藏。这么做作用在于防止外部修改内部的方法,做好了封装和防护。

function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);

  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context);

  // Copy context to instance
  utils.extend(instance, context);

  return instance;
}

不过,外部代码还是可以访问到内部的Axios实例的,在创建了幻影之后,继续执行以下代码:

// Expose Axios class to allow class inheritance
axios.Axios = Axios;

// Factory for creating new instances
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

// Expose Cancel & CancelToken
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');

// Expose all/spread
axios.all = function all(promises) {
  return Promise.all(promises);
};

axios.spread = require('./helpers/spread');

module.exports = axios;

// Allow use of default import syntax in TypeScript
module.exports.default = axios;

好,这里是外部的一些构造。其实我们拿到axios对象时就可以发起请求了。接下来我们通过一个使用示例来说明请求过程。以下是我们的request请求层示例:

// 使用示例,业务网络层request.js
const service = axios.create({
    baseURL: BASE_API, 
    timeout: TIMEOUT 
});

service.interceptors.request.use(
    config => {
        return config;
    },
    error => {
        Promise.reject(error);
    }
);

service.interceptors.response.use(
    response => {
        return Promise.reject(response);
    },
    error => {
        return Promise.reject(error);
    }
);

export default service;

假设我们的业务网络层是上面的用法,然后在具体的业务代码处通过get方法发起了一次业务请求:

axios.get('/get/server').then(function (response) {

}).catch(function (err) {

});

当业务请求代码发起时,具体执行的是lib/core/Axios.js中的request方法:

// lib/core/Axios.js
Axios.prototype.request = function request(config) {
  /*eslint no-param-reassign:0*/
  // Allow for axios('example/url'[, config]) a la fetch API
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }

  config = mergeConfig(this.defaults, config);
  config.method = config.method ? config.method.toLowerCase() : '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;
};

request方法的参数为一个config对象,而这个对象是由以下信息组成的:

{
    method: 'get',
    url: '/get/server'
}

紧接着会通过mergeConfig方法将自定义的config对象与默认的config对象进行合并:

// lib/core/Axios.js
  config = mergeConfig(this.defaults, config);

而这里的this.defaults实际内部如下:

// lib/defaults.js
var defaults = {
  adapter: getDefaultAdapter(),

  // 默认的请求转换
  transformRequest: [function transformRequest(data, headers) {
    normalizeHeaderName(headers, 'Accept');
    normalizeHeaderName(headers, 'Content-Type');
    if (utils.isFormData(data) ||
      utils.isArrayBuffer(data) ||
      utils.isBuffer(data) ||
      utils.isStream(data) ||
      utils.isFile(data) ||
      utils.isBlob(data)
    ) {
      return data;
    }
    if (utils.isArrayBufferView(data)) {
      return data.buffer;
    }
    if (utils.isURLSearchParams(data)) {
      setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
      return data.toString();
    }
    if (utils.isObject(data)) {
      setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
      return JSON.stringify(data);
    }
    return data;
  }],

  // 默认的相应转换,自定义转换方法,Json解析
  transformResponse: [function transformResponse(data) {
    /*eslint no-param-reassign:0*/
    if (typeof data === 'string') {
      try {
        data = JSON.parse(data);
      } catch (e) { /* Ignore */ }
    }
    return data;
  }],

  /**
   * A timeout in milliseconds to abort a request. If set to 0 (default) a
   * timeout is not created.
   */
  timeout: 0,

  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',

  maxContentLength: -1,

  validateStatus: function validateStatus(status) {
    return status >= 200 && status < 300;
  }
};

总之是一些待会会用到的默认配置。合并的机制是优先取自定义的配置,再取默认配置,组成一个新的合并对象。

request的方法继续向下,到了关键的地方:

// lib/core/Axios.js
  // 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);
  });

由于咱们的业务网络层使用了interceptors.request.useinterceptors.response.use这样的字样,所以我们应该看看对应的use方法做了什么:

// lib/core/InterceptorManager.js
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};

而InterceptorManager的构造方法如下:

// lib/core/InterceptorManager.js
function InterceptorManager() {
  this.handlers = [];
}

InterceptorManager在构造之初内部只有一个数组属性handlers,所以当业务网络层使用use方法时,会将对应的fulfilled, rejected组成一个新的对象Push到这个数组中。所以再回来forEach的地方:

// lib/core/InterceptorManager.js
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

forEach内部仅仅将handlers遍历,将数组中的对象通过forEach的回调方法传出,所以在Axios的request方法内,就是将业务网络层所定义的request interceptors与response interceptors压入数组chain中。所以在两个forEach执行完之后,数组chain的执行如下:

chain = [request.interceptors.success, request.interceptors.fail, dispatchRequest, undefined, response.interceptors.success, response.interceptors.fail]

然后就是根本性的一环:

// lib/core/Axios.js
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

当while方法执行时,开始触发chain中所塞进去的各个方法。在这里首先执行的是request.interceptors.success,也就是业务网络层所定义的请求拦截层。在我们的示例中什么处理都没做,于是再执行dispatchRequest。这个dispatchRequest可大有来头,是请求的核心:

// lib/core/dispatchRequest.js
function dispatchRequest(config) {
  throwIfCancellationRequested(config);

  // Support baseURL config
  if (config.baseURL && !isAbsoluteURL(config.url)) {
    config.url = combineURLs(config.baseURL, config.url);
  }

  // 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 || {}
  );

  // 为什么要删除Header中的请求方式?
  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);
  });
};

dispatchRequest方法执行了以下事情:

  • 1.校验这次的请求是否已取消。
  • 2.构造完整的请求Url。
  • 3.确认header存在。
  • 4.转换请求数据与header。转换方法在这里也可以通过config自定义。默认转换为Json串。
  • 5.合并config.headers。
  • 6.删除config.headers中请求方法所对应的值。
  • 7.获取对应的网络请求适配器并发起请求。默认的adapter主要有两种:1.浏览器环境下为XMLHttpRequest. 2.Node环境下为https库或者http库,这里还支持自定义网路请求框架。

在这里代码分析我们假设运行环境为浏览器,那我们进入浏览器环境的dispatchRequest方法:

// lib/adapters/xhr.js
function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    var requestData = config.data;
    var requestHeaders = config.headers;

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

    var request = new XMLHttpRequest();

    // HTTP basic authentication
    if (config.auth) {
      var username = config.auth.username || '';
      var password = config.auth.password || '';
      requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
    }

    // 这里的method从哪来的?
    request.open(config.method.toUpperCase(), buildURL(config.url, config.params, config.paramsSerializer), true);

    // Set the request timeout in MS
    request.timeout = config.timeout;

    // Listen for ready state
    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;
    };

    // Handle browser request cancellation (as opposed to a manual cancellation)
    request.onabort = function handleAbort() {
      if (!request) {
        return;
      }

      reject(createError('Request aborted', config, 'ECONNABORTED', request));

      // Clean up request
      request = null;
    };

    // Handle low level network errors
    request.onerror = function handleError() {
      // Real errors are hidden from us by the browser
      // onerror should only fire if it's a network error
      reject(createError('Network Error', config, null, request));

      // Clean up request
      request = null;
    };

    // Handle timeout
    request.ontimeout = function handleTimeout() {
      reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED',
        request));

      // Clean up request
      request = null;
    };

    // Add xsrf header
    // This is only done if running in a standard browser environment.
    // Specifically not if we're in a web worker, or react-native.
    if (utils.isStandardBrowserEnv()) {
      var cookies = require('./../helpers/cookies');

      // Add xsrf header
      var xsrfValue = (config.withCredentials || isURLSameOrigin(config.url)) && config.xsrfCookieName ?
        cookies.read(config.xsrfCookieName) :
        undefined;

      if (xsrfValue) {
        requestHeaders[config.xsrfHeaderName] = xsrfValue;
      }
    }

    // Add headers to the request
    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);
        }
      });
    }

    // Add withCredentials to request if needed
    if (config.withCredentials) {
      request.withCredentials = true;
    }

    // Add responseType to request if needed
    if (config.responseType) {
      try {
        request.responseType = config.responseType;
      } catch (e) {
        // Expected DOMException thrown by browsers not compatible XMLHttpRequest Level 2.
        // But, this can be suppressed for 'json' type as it can be parsed by default 'transformResponse' function.
        if (config.responseType !== 'json') {
          throw e;
        }
      }
    }

    // Handle progress if needed
    if (typeof config.onDownloadProgress === 'function') {
      request.addEventListener('progress', config.onDownloadProgress);
    }

    // Not all browsers support upload events
    if (typeof config.onUploadProgress === 'function' && request.upload) {
      request.upload.addEventListener('progress', config.onUploadProgress);
    }

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

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

    if (requestData === undefined) {
      requestData = null;
    }

    // Send the request
    request.send(requestData);
  });
};

xhrAdapter方法为浏览器环境下的核心,主要做了以下事情:

  • 1.构造XMLHttpRequest对象。
  • 2.判断是否配置用户名密码,如果有,则添加到Header中。
  • 3.通过open方法打开请求。
  • 4.设置超时时间。
  • 5.配置onreadystatechange回调方法。
  • 6.配置onabort回调方法。
  • 7.配置onerror回调方法。
  • 8.配置ontimeout回调方法。
  • 9.如果是标准的浏览器环境,则添加xsrf值。
  • 10.如果要求withCredentials,则将withCredentials设为true。
  • 11.如果配置了responseType,则配置responseType。
  • 12.如果配置了下载回调方法,则添加下载回调方法。
  • 13.如果配置了上传回调方法,则添加上传回调方法。
  • 14.判断这时是否取消了请求,如果取消请求,则取消请求。
  • 15.发起请求。

可以看到,xhrAdapter内部很贴心的为我们做了各种适配和校验。发起请求后,如果网络执行正常,那么我们的关注点应该在onreadystatechange回调方法内:

// lib/adapters/xhr.js
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;
    };

onreadystatechange内部做了以下事情:

  • 1.对file:请求协议做特殊处理。
  • 2.构造响应对象。
  • 3.进入settle方法。
// lib/core/settle.js
module.exports = function settle(resolve, reject, response) {
  var validateStatus = response.config.validateStatus;
  if (!validateStatus || validateStatus(response.status)) {
    resolve(response);
  } else {
    reject(createError(
      'Request failed with status code ' + response.status,
      response.config,
      null,
      response.request,
      response
    ));
  }
};

settle内部允许自行判断可接收的状态,通过validateStatus方法校验。执行完这一部之后,就算执行完成了。

到此为止应该是执行刚刚xhrAdapter中所返回的Promise,我们需要回到这个Promise所对应的then方法:

// lib/core/dispatchRequest.js
  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);
  });

一切顺利,这里应该执行onAdapterResolution方法。onAdapterResolution方法内执行了以下事情:

  • 1.校验是否取消请求。
  • 2.对返回的对象做反序列化处理,这里同样支持自定义反序列化方法。
  • 3.返回反序列化后的对象。

到这里,我们应该又去哪里找对应的then方法呢?我们需要一层层返回。刚刚dispatchRequest方法是在lib/core/Axios.js中的request中调用的,所以这里的then方法应该还在这里。还记得之前在request中执行过的这段代码吗?

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

没错,由于之前chain中保存了业务网络层自定义的请求拦截器与响应拦截器,所以dispatchRequest方法会被Promise执行到业务网络层的响应拦截器中,我们在这里再贴一下业务网络层的响应拦截器:

service.interceptors.response.use(
    response => {
        return Promise.reject(response);
    },
    error => {
        return Promise.reject(error);
    }
);

嗯,我们还是什么都没处理,这里执行完,就开始触发我们的业务请求处的then方法了:

axios.get('/get/server').then(function (response) {

}).catch(function (err) {

});

当时我们的业务代码中什么都没写,如果通过控制台输出的话,就能看到最终的返回结果了。

到这里为止就是Axios的整个请求过程了,你是否清楚了呢?


通过阅读Axios源码,有以下总结:

  • Promise是其中的粘合剂,又做了协作的约定。
  • 面向对象的熟练使用。
  • 良好的代码规范,入参出参与分号、隔行、注释。
  • 良好的说明文档:https://www.npmjs.com/package/axios
  • 允许控制内部的更多细节,例如数据转换方式。

缺点:

  • 没有对高并发做处理。

前端中的其它源码分析文章:
Promise源码解析 https://sahadev.blog.csdn.net/article/details/90722543
深入解析Node.js setTimeout方法的执行过程 https://sahadev.blog.csdn.net/article/details/90703250
Vue源码探究笔记 https://sahadev.blog.csdn.net/article/details/87943168

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页