axios原理分析,让你知道拦截器的实现,取消请求,自定义配置。

6,994 阅读12分钟

做了好多个vue的项目一直使用axios用来接口请求,用法非常简单,功能很强大,不仅支持浏览器同时也支持node端,引用一下官网的介绍:

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。
从浏览器中创建 XMLHttpRequests
从 node.js 创建 http 请求
支持 Promise API
拦截请求和响应
转换请求数据和响应数据
取消请求
自动转换 JSON 数据
客户端支持防御 XSRF

Axios有默认设置,开发人员可以直接使用提供的get,post等方法也提供了用户自定义设置模式。
当做实例调用方法:
axios.get()
axios.post()
axios.delete()
axios.options()
当做构造函数使用:

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

自定义配置axios:
创建实例
可以使用自定义配置新建一个 axios 实例 可以根据自己项目需求配置,请求地址,请求头,超时时间等等

axios.create([config])
var instance = axios.create({
  baseURL: 'https://some-domain.com/api/',
  timeout: 1000,
  headers: {'X-Custom-Header': 'foobar'}
});

了解了Axios的使用方法同时支持不同的写法,那么它的内部是如何支持不同的写法呢?如何取消请求,如何进行请求拦截,和响应拦截的?接下来我们一探究竟。

源码分析

这是我项目里引入的axios的目录

adapters 请求的实现;
cancel 取消请求;
core axios主要的实现在这里 axios.js 就是入口文件;
defaults.js 默认配置;
utils.js 工具类;
先从入口文件去摸索一下;

var utils = require('./utils');
var bind = require('./helpers/bind');
var Axios = require('./core/Axios');
var defaults = require('./defaults');
function createInstance(defaultConfig) {
//这里创建了axios的实例
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);
====================列出bind方法的实现============================
  //这里用了bind方法改变了Axios.prototype.request,this的指向,返回了新的函数;
  module.exports = function bind(fn, thisArg) {
  return function wrap() {
    var args = new Array(arguments.length);
    for (var i = 0; i < args.length; i++) {
      args[i] = arguments[i];
    }
    return fn.apply(thisArg, args);
  };
};
=================bind方法的实现========================================  
  
  
  utils.extend(instance, Axios.prototype, context);
  utils.extend(instance, context);
  return instance;
}
var axios = createInstance(defaults);
axios.Axios = Axios;
axios.create = function create(instanceConfig) {
  return createInstance(utils.merge(defaults, instanceConfig));
};
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
axios.all = function all(promises) {
  return Promise.all(promises);
};
axios.spread = require('./helpers/spread');
module.exports = axios;
module.exports.default = axios;

这里介绍下各个引入的变量:
defaults: axios默认配置
Cancel、CancelToken 取消请求的实现
大概看完源码发现暴露出来的axios是通过createInstance()处理后的一个函数;这函数就是Axios.prototype.request;还有定义的create方法接受了一个配置对象,也是调用的createInstance(),并且使用使用了merge混合了defaults和自定义参数,axios核心就是Axios.prototype.request;

默认配置(defaults)

这里我们看一下axios的默认配置都有什么,都在defaults.js文件中 下面对主要的代码进行解释

var utils = require('./utils');
var normalizeHeaderName = require('./helpers/normalizeHeaderName');
var DEFAULT_CONTENT_TYPE = {
  'Content-Type': 'application/x-www-form-urlencoded'
};
function setContentTypeIfUnset(headers, value) {
  if (!utils.isUndefined(headers) && utils.isUndefined(headers['Content-Type'])) {
    headers['Content-Type'] = value;
  }
}
//这个方法判断了当前环境是浏览器还是node端否
//支持XMLHttpRequest 就是浏览器否则就是node
//这就是axios是如何支持浏览器和node端的原因
function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // 对弈浏览器使用XMLHttpRequest 
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined') {
    // node 使用 http
    adapter = require('./adapters/http');
  }
  return adapter;
}
//这里就是axios的默认配置项;
var defaults = {
//最终发起请求的方法;
//下面则是对请求头,cookie 的一些配置项;
  adapter: getDefaultAdapter(),
  transformRequest: [function transformRequest(data, headers) {
    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;
  }],

  transformResponse: [function transformResponse(data) {
    if (typeof data === 'string') {
      try {
        data = JSON.parse(data);
      } catch (e) { /* Ignore */ }
    }
    return data;
  }],
  timeout: 0,
  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',
  maxContentLength: -1,
  validateStatus: function validateStatus(status) {
    return status >= 200 && status < 300;
  }
};

defaults.headers = {
  common: {
    'Accept': 'application/json, text/plain, */*'
  }
};
//这里对常艳红方法进行设置headers
utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
  defaults.headers[method] = {};
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);
});

module.exports = defaults;

这里对默认配置进行总结一下,defaults里面判断了当前的运行环境,支持了浏览器和node端,然后对get,post,delete方法,cookie等进行默认设置。当我们不进行自定义配置时候就是使用了这套默认的设置,当我们有自定义配置传入的时候,会使用merge方法把默认的配置替换成自定义的设置,对于默认配置文件的梳理到这里;

axios浏览器的实现(XMLHttpRequest)

从defaults中我们看到了对于浏览器的支持是基于XMLHttpRequest实现的,同时axios是基于promise实现的我们看一下,axios是如何封装XMLHttpRequest和promise的。

var utils = require('./../utils');
var settle = require('./../core/settle');
var buildURL = require('./../helpers/buildURL');
var parseHeaders = require('./../helpers/parseHeaders');
var isURLSameOrigin = require('./../helpers/isURLSameOrigin');
var createError = require('../core/createError');
module.exports = function xhrAdapter(config) {
//函数第一行就是返回了一个Promise对象
  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
    }
    //创建XMLHttpRequest实例
    var request = new XMLHttpRequest();
    if (config.auth) {
      var username = config.auth.username || '';
      var password = config.auth.password || '';
      requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
    }
    //根据配置获取方法,GET,POST,请求的url,请求参数,超时时间,
    request.open(config.method.toUpperCase(), buildURL(config.url, config.params, config.paramsSerializer), true);
    request.timeout = config.timeout;
    request.onreadystatechange = function handleLoad() {
      if (!request || request.readyState !== 4) {
        return;
      }
      if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
        return;
      }

      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);
      request = null;
    };

    request.onerror = function handleError() {
      reject(createError('Network Error', config, null, request));
      request = null;
    };
    request.ontimeout = function handleTimeout() {
      reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED',
        request));
      request = null;
    };
    if (utils.isStandardBrowserEnv()) {
      var cookies = require('./../helpers/cookies');
      var xsrfValue = (config.withCredentials || isURLSameOrigin(config.url)) && config.xsrfCookieName ?
          cookies.read(config.xsrfCookieName) :
          undefined;

      if (xsrfValue) {
        requestHeaders[config.xsrfHeaderName] = xsrfValue;
      }
    }
    if ('setRequestHeader' in request) {
      utils.forEach(requestHeaders, function setRequestHeader(val, key) {
        if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
          delete requestHeaders[key];
        } else {
          // Otherwise add header to the request
          request.setRequestHeader(key, val);
        }
      });
    }

    if (config.withCredentials) {
      request.withCredentials = true;
    }

    if (config.responseType) {
      try {
        request.responseType = config.responseType;
      } catch (e) {
        if (config.responseType !== 'json') {
          throw e;
        }
      }
    }
    if (typeof config.onDownloadProgress === 'function') {
      request.addEventListener('progress', config.onDownloadProgress);
    }
    if (typeof config.onUploadProgress === 'function' && request.upload) {
      request.upload.addEventListener('progress', config.onUploadProgress);
    }

    //取消请求的地方
    if (config.cancelToken) {
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
          return;
        }

        request.abort();
        reject(cancel);
        // Clean up request
        request = null;
      });
    }
    if (requestData === undefined) {
      requestData = null;
    }
    //发送请求
    request.send(requestData);
  });
};

小结:xhr.js 返回了一个promise对象,里面创建了XMLHttpRequest的链接进行默认设置,请求成功的时候,调用promise的resolve ,失败了调用reject方法。没什么特殊的难点。耐心看一下就可以看的明白。然后有一个点:config.cancelToken,这里实现了axios取消请求的方法。这里先不讨论.

Axios核心方法request

var defaults = require('./../defaults');
var utils = require('./../utils');
var InterceptorManager = require('./InterceptorManager');
var dispatchRequest = require('./dispatchRequest');
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  //这里声明了请求拦截,和相应拦截
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
//先可以不看,下一节我们讲解
Axios.prototype.request = function request(config) {
  if (typeof config === 'string') {
    config = utils.merge({
      url: arguments[0]
    }, arguments[1]);
  }
  config = utils.merge(defaults, {method: 'get'}, this.defaults, config);
  config.method = config.method.toLowerCase();
  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;
};
//这里我们看到常用的方法挂载到了原型上,
//拿我们项目里会用到
axios.get(url,{{
    params:123
}})
//可以分析出来,第一个参数url,第二个是config参数然后通过merge方法处理,最后调用request
//所以我们可以知道不管我们调用get,delete,head等方法并且传入参数,最后都通过merge处理传给了request,所以
//最后执行的都是request方法;
//这就是为什么说request 方法是axios的核心方法;
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
});
//挂载post等方法到原型上
//解析如上最后都是用的request方法;
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  Axios.prototype[method] = function(url, data, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});
module.exports = Axios;

小结:看了Axios.js文件,我们发现这里处理了我们常用的get,post,head等方法挂载到了原型上,当我们调用这些方法并且传入参数后,他们会通过merge方法对参数处理返回了this.request方法,所以我们最终执行的是request方法。 这也是说Axios.prototype.reques是核心方法的原因。下面我们对request进行分析一下。

request方法

//参数是merge处理后的参数
Axios.prototype.request = function request(config) {
  if (typeof config === 'string') {
    config = utils.merge({
      url: arguments[0]
    }, arguments[1]);
  }
  //这里对开发人员自定义配置进行处理,默认是get方法,把this.defaults覆盖到默认defaults,同时也处理了传进来的config
  config = utils.merge(defaults, {method: 'get'}, this.defaults, config);
  
  config.method = config.method.toLowerCase();
  //dispatchRequest就是发送请求的方法
  var chain = [dispatchRequest, undefined];
  
  //这里定义一个成功的promist,config作为参数
  var promise = Promise.resolve(config);
  
  //当定义拦截器的时候,这里加入请求拦截和响应拦截
  //当执行到这里的时候,我们配置了拦截方法,就会调动interceptors的foreach方法遍历出所有的
  //拦截方法,添加到chain的前面。当有多个拦截方法的时候后添加的在前面先添加的在后面成组添加,每组两个,标识成功的回调,和失败的回调;
  //chain=[funsucess1,funerr1,funsucess2,funerr2,dispatchRequest,undefined]
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  
  //相应拦截器放到了dispatchReques的后面,这样:
  // [dispatchRequest,undefined,funsucess1,funerr1,funsucess2,funerr2,]
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });
  
  //添加完了请求拦截,相应拦截,开始执行所有的方法
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
    
  //这里从chain数组的前面取函数进行执行,每次取两个,正好对应拦截器添加的成功函数和失败函数。
  //[funsucess1,funerr1,,dispatchRequest,undefined,funsucess2,funerr2]
  //这样就保证了,请求拦截,在发送请求(dispatchRequest)前执行,响应拦截在请求后执行;
  //那么axios的拦截器就是这样实现的。
  //上面promise把config作为参数,所以在拦截函数里接收的就是这个config参数;
  }
  return promise;
};

InterceptorManager(拦截器)

拦截器,定义了handlers 收集了所有的拦截方法。 提供了foreach,遍历所有的拦截方法; 定义了use,我们自定义拦截器的时候使用的use就是这个

================配置请求拦截的时候=================
_axios.interceptors.request.use(
  function(config) {
    return config;
  },
  function(error) {
    return Promise.reject(error);
  }
);
=================================
var utils = require('./../utils');
function InterceptorManager() {
  this.handlers = [];
}
//提供了use方法,
我们进行配置请求,相应拦截的时候会使用到use方法,传入了成功的回调,和事变的回调
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};
//提供了forEach 遍历添加到handlers的拦截方法;
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};
module.exports = InterceptorManager;

小结,这一节我们知道了Reques方法会处理自定义的配置项,和传入的参数,默认的方法就是GET方法,知道了然后dispatchRequest是发送请求的函数,了解了axios发送请求前会有一个数组, chain=[dispatchRequest,undefined],请求拦截的函数加到请求方法dispatchRequest的的前面,响应拦截放到发送请求undefined的后面,然后顺序调用,就保证了请求拦截--->请求--->响应拦截; 接下来我们去看一下请求是如何发送的

dispatchRequest发请求

刚刚说dispatchRquest是发送请求的,这个文件暴露了dispatchRequest方法,这个方法处理的发送请求前的一些配置,headers,data,请求方法,最后返回了adapter,adapter就是xhr暴露出来的基于promise的xmlHttpReques,这里就是发送了请求,当请求成功后,promise执行resolve,把返回值传给了.then方法的onAdapterResolution(),然后进一步处理响应数据并且返回 resolve(response);如果失败了则调用onAdapterRejection()处理失败后的响应数据返回了Promise.reject,所以最后返回的都是Promise的resolve,reject;

'use strict';

var utils = require('./../utils');
var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults');
var isAbsoluteURL = require('./../helpers/isAbsoluteURL');
var combineURLs = require('./../helpers/combineURLs');
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}

module.exports = function dispatchRequest(config) {
  throwIfCancellationRequested(config);
  if (config.baseURL && !isAbsoluteURL(config.url)) {
    config.url = combineURLs(config.baseURL, config.url);
  }
  config.headers = config.headers || {};
  config.data = transformData(
    config.data,
    config.headers,
    config.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];
    }
  );
  var adapter = config.adapter || defaults.adapter;
  //返回基于Promise的xhr
  return adapter(config).then(function onAdapterResolution(response) {
  //成功的回调,处理响应数据;
    throwIfCancellationRequested(config);
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );
    return response;
  }, function onAdapterRejection(reason) {
  //失败的回调并且返回return Promise.reject
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }
    return Promise.reject(reason);
  });
};

我们知道了axios是如何发送请求的了。成功的时候返回了成功的数据 resolve(response);,失败了返回Promise.reject,然后我们再回到 Axios.js的 Axios.prototype.request方法中

Axios.prototype.request = function request(config) {
//删除了无关的======================================
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);
//删除了无关的======================================
  n
  while (chain.length) {
  那么这里执行完了dispatchRequest 这个promise得到dispatchRequest返回的Promise,resolve(response)或者reject,
    promise = promise.then(chain.shift(), chain.shift());
  }
  //这里最后返回了这个promise,那么我们在调用的时候就会得到最后请求返回的数据
  return promise;
};

项目代码================================如下
axios.get(url)这个就是返回的promise
那么调用axios.get(url).then(res=>{
    这个res就是上面返回的 resolve(response)
})
catch则是失败的返回

小结:到这里我们知道了整个axios发送请求,获取响应,应用promise进行链式调用,那么如何取消请求呢?

取消请求

引用一个例子,创建请求的时候传入了CancelToken,然后调用这个c参数,调用之后就可以取消了,我们去创建请求的时候传入了CancelToken看一看究竟。

var CancelToken = axios.CancelToken;
var cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // executor 函数接收一个 cancel 函数作为参数
    cancel = c;
  })
});

// 取消请求
cancel();

我们看一下 CancelToken.js的主要代码。 当我们new一个CancelToken的时候,设置了promise,默认是resolve成功的回调。 调用executro把cancel方法暴露出去,当再外部调用cancel的时候,this.promise的状态变成了resole

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) {
      // Cancellation has already been requested
      return;
    }

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

在xhr.js中发现了这样几行代码

//使用了cancelToken
if (config.cancelToken) {
//这里当promise变成成功后,调用了then执行request.abort()这样就取消了这个请求;
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
          return;
        }
        //取消请求
        request.abort();
        reject(cancel);
        // Clean up request
        request = null;
      });
    }

总结

到这里对于axios的大概就了解的差不多了,axios支持自定义配置,支持get等api,其实最终都是调用原型上的Axios.prototype.request方法,这个方法会对传入的参数进行混合。 axios对于浏览器基于Promise创建了XmlHttpReques对象,成功失败返回的是Promise,所以我们可以调用.then方法接收返回值。
对于请求拦截响应拦截,当使用拦截的.use的时候传入了成功和失败的回调方法,在axios调用request放发的时候,会创建个数组,chain=[dispatchRequest,undefined] 把请求拦截放在dispatchRequest前面,响应拦截放在undifined后面,然后按照顺序循环调用每次取出两个,对应传入的成功回调,和失败回调,这个数组保证了,请求拦截和响应拦截的顺序。
对于取消请求在创建axios的时候传入了取消的对象cancelToken,这个对象创建了一个promise并向外暴露了一个取消方法,当调用这个取消方法的时候,会把这个promise状态设置为成功,当成功的时候,会在xhr文件里调用 取消请求的 request.abort()方法。