从源码层分析axios的使用

309 阅读5分钟

前言

日常开发中,使用axios传递参数我们总是会混淆参数传递的位置,因为axios对我们传递的参数做了合并兼容,现在让我们从源码层面了解axios参数的使用。

axios的三种使用方式

  • axios.request(config)
  • axios.get(url,config)/axios.post(url,data,config)
  • axios(config) axios是返回了一个方法,再遍历Axios.prototype上的所有方法,赋值给axios,所以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的参数合并

axios的最终接收的都只是一个config对象,里面包含一些我们经常使用的url、method、params、body、headers等参数,可以分开传递是因为内部对参数再做了一次合并,参照下面的部分源码:

utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {
    // 合并为一个config对象
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: (config || {}).data
    }));
  };
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  Axios.prototype[method] = function(url, data, config) {
  // 合并为一个config对象
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});

axios的params和data参数

axios的params参数是通过简单处理,拼接到url后面,再去请求接口,data参数直接作为body,通过 xhr.send(data)传递给后台接口,源码如下:

  • params参数处理
var request = new XMLHttpRequest();
var fullPath = buildFullPath(config.baseURL, config.url);
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params), true);
// ... 
function buildURL(url, params) {
  var serializedParams;
  var parts = [];
  utils.forEach(params, function serialize(val, key) {
    if (val === null || typeof val === 'undefined') return;
    // 对数组参数做处理
    if (utils.isArray(val)) { 
      key = key + '[]';
    } else {
      val = [val];
    }
    // 对日期参数和普通对象处理
    utils.forEach(val, function parseValue(v) {
      if (utils.isDate(v)) {
        v = v.toISOString();
      } else if (utils.isObject(v)) {
        v = JSON.stringify(v);
      }
      parts.push(encode(key) + '=' + encode(v));
    });
  });
  // 参数拼接到url
  serializedParams = parts.join('&');
  if (serializedParams) {
    var hashmarkIndex = url.indexOf('#');
    if (hashmarkIndex !== -1) {
      url = url.slice(0, hashmarkIndex);
    }
    url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams;
  }
  return url;
};
  • data参数
module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    var requestData = config.data;
    var request = new XMLHttpRequest();
    // ...
    request.send(requestData);
  })
}

axios的headers默认参数

axios的headers的参数,axios支持根据你不同的请求方法,设置不同的默认参数,通过axios.defaults.headers.common = {xxx: "xxx"}设置,axios的参数都支持axios.defaults设置,但是优先级要低于传递的config参数,源码如下

defaults.headers = {
  common: {
    'Accept': 'application/json, text/plain, */*'
  }
};

utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
  defaults.headers[method] = {};
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  defaults.headers[method] = utils.merge({
    'Content-Type': 'application/x-www-form-urlencoded'
  });
});

axios的拦截器和适配器

基本使用

// 拦截器
axios.interceptors.request.use(config => { 
  config.headers = { 
    'Content-Type': 'application/x-www-form-urlencoded' //配置请求头 
  } 
  return config; 
}, error => {  });
// 适配器
axios.interceptors.response.use(response => {
  response.data = "XXX"
  return response
}, error => {  })

实现思路:

  • 通过use方法将传入的回调函数分别push到拦截器和适配器数组里面
  • 创建一个数组,放一个ajax的方法,前面unshift拦截器的回调函数,后面push适配器的回调函数
  • 通过promise.then的链式调用,保证调用顺序
  • 这样,在每次发送请求之前(拦截器)和请求成功之后(适配器),分别去调用对应的参数。

核心源码如下:

// 适配器和拦截器使用同一个构造函数,实现一个use方法
function InterceptorManager() {
  this.handlers = [];
}
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};

// 实例挂载在axios.interceptors上
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(), // 拦截器
    response: new InterceptorManager() // 适配器
  };
}


Axios.prototype.request = function request(config) {
  var requestInterceptorChain = [];
  // 拦截器的函数收集到数组
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
    
  // 适配器的函数收集到数组
  var responseInterceptorChain = [];
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });
    
  var promise;
  // 中间是发送ajax请求
  var chain = [dispatchRequest, undefined];
  // 拦截器的函数放在ajax之前
  Array.prototype.unshift.apply(chain, requestInterceptorChain);
  // 适配器的函数放在ajax之后
  chain.concat(responseInterceptorChain);
  // 一开始传入config作为函数参数,dispatchRequest之后的函数参数变为response
  promise = Promise.resolve(config);
  // 将所有函数通过promise.then连接起来
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }
  return promise;
};

axios的取消请求

2种使用方式

  • 传入一个cancelToken参数,值是CancelToken函数的一个实例
  • 保存resolve函数,在必要的时候执行
// 方式一
const source = axios.CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
});
// 取消请求
source.cancel();

// 方式二
let cancel;
axios.get('/user/12345', {
  cancelToken: new axios.CancelToken(function executor(c) {
    cancel = c;
  })
});

// 取消请求
cancel();

源码实现

核心思路就是promise+xhr.abort实现

  • 创建一个CancelToken实例,传入一个回调函数
  • 实例上挂载一个promise,promise的resolve方法,被作为参数传递出去
  • 在发送xhr请求的时候,注册promise.then,中断请求
  • 在外面执行promise的resolve方法,中断请求
function CancelToken(executor) {
  var resolvePromise;
  // 创建一个promise对象
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });
  
  // 将promise的resolve方法,在执行构造函数的时候,传递出去
  executor(function cancel() {
    resolvePromise();
  });
}
CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    var request = new XMLHttpRequest();
    request.open(config.method.toUpperCase(), true);
    // 原生js的onabort方法
    request.onabort = function handleAbort() {
      reject();
    };
    // ...
    if (config.cancelToken) {
    // 创建的promise,在这里等待被执行
      config.cancelToken.promise.then(function onCanceled(cancel) {
        request.abort();
        reject(cancel);
      });
    }

    request.send(requestData);
  });
};

上传与下载进度监控方法

基本使用

  • 使用方法和传递其他headers等参数一样,最终都是去config里面取。
axios({
  onUploadProgress: function (progressEvent) { 
    // Do whatever you want with the native progress event 
  }, 
  // `onDownloadProgress` 允许为下载处理进度事件 
  onDownloadProgress: function (progressEvent) { 
  // 对原生进度事件的处理 
  }
}
})

源码实现

这里的源码实现就很简单,直接调用xhr两个原生的对应的方法

function xhrAdapter(config) {
      return new Promise(function dispatchXhrRequest(resolve, reject) {
        var request = new XMLHttpRequest();
        request.open(config.method.toUpperCase(), url, true);

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

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

最后

axios的实现,从源码角度来分析,其实并不是很难,文中贴出来的源码是经过一些删减,有兴趣的朋友,可以参考源码看一下具体实现,在工作中使用axios更加的顺滑,最后,文章对你有帮助的话,不要忘了点赞喔~