网络请求库-axios

本文探讨了axios在网络请求中的应用,分析了axios与Axios构造函数的关系、axios.create()的功能差异、数据转换过程,包括请求和响应数据的处理,并简述了请求过程和取消请求的实现。此外,还提到了axios底层基于ajax的封装细节,强调了在不同状态码下仍可获取响应数据的重要性。作者分享了自己的理解和学习经验,鼓励读者深入研究axios源码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在web开发中,往往需要在前端向后端请求数据,以前可能用到的就是ajax,但是大家现在可能用的是axios或者fetch,而在公司里往往会对axios进行二次封装去进行网络请求,今天就聊聊axios的一些源码知识,最后会给出一个我自己的axios封装。

首先说说axios的优点:

1、基于promise API2、请求拦截与响应拦截;
3、自动转换数据类型;
4、默认请求配置与取消请求;
axios与Axios

在axios中,可以发现一个很重要的构造函数Axios,那么先从axios与Axios关系说起。
在源码的axios.js中,有代码

function createInstance(defaultConfig) {

  var context = new Axios(defaultConfig);
  
  // axios,axios.create()其实返回的都是request函数,request函数才是真正发请求的函数
  // Axios.prototype.request.bind(context)
  var instance = bind(Axios.prototype.request, context); //axios

  // 将原型上的方法拷贝到instance上:get、request、post、put等
  utils.extend(instance, Axios.prototype, context);

  // 将context的属性拷贝到instance上:defaults以及拦截器
  utils.extend(instance, context);

  return instance;
}
// 可以看出axios其实就是一个request实例,而这个实例上挂载这很多方法。
var axios = createInstance(defaults);
//Axios构造函数挂载到axios上
axios.Axios = Axios;

Axios与axios的关系,可以从这段代码中知道:

1、从形式上看,Axios会被挂载到axios的Axios上;
2、从功能上看,axios具有Axios实例的功能,但是axios本身却是一个函数;
3、axios()等同于Axios.prototype.request.bind(context)(),也就是说我们发送请求的底层原理其实是调用了Axios.prototype.request方法。
// 第三点之所以指出是因为,在后续理解axios的执行流程时很重要。
axios与axios.create()

在axios中,有一个很好的点就是可以创建axios实例,其也可以发送网络,但是相较于axios本身,其又缺少一些功能,下面通过源码比较一下之间的区别:

// 本质上也是调用createInstance方法,得到拥有各种方法和属性的实例对象。
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');

可以看出axios.create得到也是一个可以发送任何网络请求以及设置拦截的实例对象,但是这个实例对象会缺少axios本身挂载的属性比如create以及取消请求的相关api,再者就是该实例的默认配置与axios的默认配置进行合并。

数据转换

数据包括请求数据与响应数据,首先来看看请求数据转换(defaults.js文件中:):

// 请求转换器:
  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) || (headers && headers['Content-Type'] === 'application/json')) {
      // 如果数据为对象,或者Content-Type为application/json,则进行序列化。
      setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
      return JSON.stringify(data);
    }
    return data;
  }],

这里需要特别注意的就是,从源码中可以看出,如果我们携带的数据是对象类型或者设置了请求头’Content-Type’: ‘application/json’,那么会默认对数据进行字符串序列化和加上请求头,也就是说自动做了类型转化。因为我本人在这个地方踩过坑,具体可以看我另一篇踩坑文章–node后台与前端。其实从源码看来,完全不用自己做序列化。
用过axios的人,应该都会发现结果都是对象,其实在这里axios内部是做了一定处理的,来看看响应数据类型转换相关源码:

// 响应转换器

transitional: {
   silentJSONParsing: true,
   forcedJSONParsing: true,
   clarifyTimeoutError: false
 },
  transformResponse: [function transformResponse(data) {
    var transitional = this.transitional;
    var silentJSONParsing = transitional && transitional.silentJSONParsing;
    var forcedJSONParsing = transitional && transitional.forcedJSONParsing;
    var strictJSONParsing = !silentJSONParsing && this.responseType === 'json';

    if (strictJSONParsing || (forcedJSONParsing && utils.isString(data) && data.length)) {
      try {
        return JSON.parse(data);
      } catch (e) {
        if (strictJSONParsing) {
          if (e.name === 'SyntaxError') {
            throw enhanceError(e, this, 'E_JSON_PARSE');
          }
          throw e;
        }
      }
    }

    return data;
  }],

这段代码的(个人)理解,其实就是看响应数据是不是可以进行json解析。

请求过程

接下来我们看看整个请求过程,关于拦截器方面,会在后面进行阐述

以axios(config)为例
1、axios(config)实际上调用的是Axios.prototype.request.bind(context)(config)
2、在Axios.prototype.request.bind(context)(config)中会进行dispatchRequest(newConfig)调用;
3、dispatchRequest(newConfig)会调用adapter(config),这里的adapter就是xhr.js中的xhrAdapter函数,这个函数其实封装的就是原生xhr对象。
其实整个请求过程其实就是这三个函数的调用,其顺序:request => dispatchRequest => adapter

Axios.js:
Axios.prototype.request = function request(config) {
  // config处理
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }
 // 默认配置合并
  config = mergeConfig(this.defaults, config);

  // GET、POST => get、post
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }

  var transitional = config.transitional;
  if (transitional !== undefined) {
    validator.assertOptions(transitional, {
      silentJSONParsing: validators.transitional(validators.boolean, '1.0.0'),
      forcedJSONParsing: validators.transitional(validators.boolean, '1.0.0'),
      clarifyTimeoutError: validators.transitional(validators.boolean, '1.0.0')
    }, false);
  }

  // 拦截器相关,后面会详细阐述。
  var requestInterceptorChain = [];
  var 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);
  });

  var responseInterceptorChain = [];
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    // 添加的响应拦截器放在数组尾部
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });

  var promise;

  if (!synchronousRequestInterceptors) {
    var chain = [dispatchRequest, undefined];

    Array.prototype.unshift.apply(chain, requestInterceptorChain);
    chain.concat(responseInterceptorChain);

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

    return promise;
  }

  var newConfig = config;
  while (requestInterceptorChain.length) {
    var onFulfilled = requestInterceptorChain.shift();
    var onRejected = requestInterceptorChain.shift();
    try {
      newConfig = onFulfilled(newConfig);
    } catch (error) {
      onRejected(error);
      break;
    }
  }

  // 特别注意
  try {
    promise = dispatchRequest(newConfig);
  } catch (error) {
    return Promise.reject(error);
  }

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


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

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

  // Transform request data
  config.data = transformData.call(
    config,
    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.call(
      config,
      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.call(
          config,
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

    return Promise.reject(reason);
  });
};
取消请求

关于取消请求,请移步另一篇我关于取消请求的详细阐述博客。

axios底层封装
xhr.js:
module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    var requestData = config.data;
    var requestHeaders = config.headers;
    var responseType = config.responseType;

    if (utils.isFormData(requestData)) {
      delete requestHeaders['Content-Type']; // Let the browser set it
    }
    
	极度需要注意:这里request不是请求而是xhr,第一次看,千万注意!!!
    var request = new XMLHttpRequest();

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

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

    request.timeout = config.timeout;

    function onloadend() {
      if (!request) {
        return;
      }
      var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
      var responseData = !responseType || responseType === 'text' ||  responseType === 'json' ?
        request.responseText : request.response;

      // 可以看出response在axios内部做了整理,所以默认为对象格式。
      var response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config: config,
        request: request
      };

      // 判断状态码:axios的有效状态码为200-299。这个函数里面会执行成功的回调,也就是.then(res=>res.data)。
      settle(resolve, reject, response);

      // 成功之后清除xhr,个人更习惯写为xhr。
      request = null;
    }

    if ('onloadend' in request) {
      // Use onloadend if available
      request.onloadend = onloadend;
    } else {
      // Listen for ready state to emulate onloadend
      request.onreadystatechange = function handleLoad() {
        if (!request || request.readyState !== 4) {
          return;
        }

        if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
          return;
        }
        异步返回数据,由于事件队列关系会最后执行。
        setTimeout(onloadend);
      };
    }
    
    request.onabort = function handleAbort() {
      if (!request) {
        return;
      }
      reject(createError('Request aborted', config, 'ECONNABORTED', 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(createError('Network Error', config, null, request));

      // Clean up request
      request = null;
    };

    request.ontimeout = function handleTimeout() {
      var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded';
      if (config.timeoutErrorMessage) {
        timeoutErrorMessage = config.timeoutErrorMessage;
      }
      reject(createError(
        timeoutErrorMessage,
        config,
        config.transitional && config.transitional.clarifyTimeoutError ? 'ETIMEDOUT' : 'ECONNABORTED',
        request));

      // Clean up request
      request = null;
    };

    // Add xsrf header

    if (utils.isStandardBrowserEnv()) {
      // 携带cookie
      var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && 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 {
          request.setRequestHeader(key, val);
        }
      });
    }
    // 是否携带cookie
    if (!utils.isUndefined(config.withCredentials)) {
      request.withCredentials = !!config.withCredentials;
    }
    // Add responseType to request if needed
    if (responseType && responseType !== 'json') {
      request.responseType = config.responseType;
    }
    // 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) {
      // 取消请求
      config.cancelToken.promise.then(cancel=> {
        if (!request) {
          return;
        }
        // 取消请求
        request.abort();
        reject(cancel);
        // Clean up request
        request = null;
      });
    }
    if (!requestData) {
      requestData = null;
    }
    request.send(requestData);
  });
};

从源码中可以看出,axios是基于ajax封装的,其过程依旧是满足ajax的几大步,但是在其中增加了监听事件以及配置项的处理,尤为需要注意的是,在axios中实际上是先得到响应数据,再根据状态码判断是否向外界传数据的,而不是先判断状态码的。所以我们可以得出,即使状态码不是在200-299之间,response中依旧可以有数据。明白这一点,对于二次封装axios非常重要。

未完待续····

ps:大家能看到这篇博文,想必也有在看axios源码,我在这里只是略作疏导以及自己的一些想法,axios里面其实有很多东西可以研究的,而我表达能力有限,之所以会写这篇博客更多是想让自己更加了解axios底层原理,可以很清晰地理解知识,也是对自己的表达能力一种考验,毕竟学习知识难,能很好地表达给更多人懂更难,但是我相信如果能跨过后面这一步,那么就可以对知识有着更深地理解,大家加油!

axios封装:gitub:https://github.com/heD0ng/axios
后续会慢慢完善。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值