学习目的

  • 了解axios的实现原理、学习温故js基础知识
  • 了解axios平时不常用的配置与功能
  • 有利于解决前后端交互遇到的问题
  • 提高阅读源码的能力,沉淀一些好用的方法
  • 扩展自己想要的功能-如果自己要写一个包或开源库,有利于提供思路

源码目录分析

下载地址:git clone https://github.com/axios/axios.git

└─ examples   // demo
└─ lib
	└─ adapters
   		├─ http.js // node环境下利用 http 模块发起请求
   		├─ xhr.js // 浏览器环境下利用 xhr 发起请求
	└─ cancel
   		├─ Cancel.js      // Cancel构造函数
   		├─ CancelToken.js // 取消请求构造函数
   		├─ isCancel.js   // 是否取消boolean
	└─ core
    	├─ Axios.js // 生成 Axios 实例
    	├─ InterceptorManager.js // 拦截器
    	├─ dispatchRequest.js  // 调用适配器发起请求
    	├─ mergeConfig.js // 合并配置
    	├─ transformData  // 数据转化
    	...
	└─ helpers
    	├─ bind.js  // bind函数
    	├─ buildURL.js // 根据类型转换为最终的url
    	├─ spread.js // sprend扩展
    	├─ validator.js // 校验函数
    	├─ cookies.js // cookie的读写方法
    	├─ ...
	├─ axios.js  // 入口文件
	├─ defaults.js  // axios 默认配置项
	├─ utils.js     // 工具函数
└─ sandbox // 沙箱测试
	client.html // 前端页面
	server.js // 后端服务
└─ webpack.config.js // webpack配置

基本功能分析

入口文件

查看webpack.config.js , 入口文件为index.js

var config = {
  entry: './index.js',
  output: {
    path: __dirname + '/dist/',
    filename: name + '.js',
    sourceMapFilename: name + '.map',
    library: 'axios',
    libraryTarget: 'umd',
    globalObject: 'this'
  }

查看根目录下的index.js

module.exports = require('./lib/axios');

顺藤摸瓜,查看./lib/axios.js 文件

我们来分析下真正的入口文件./lib/axios.js

导入依赖模块

'use strict';

var utils = require('./utils'); // 工具函数
var bind = require('./helpers/bind'); // bind方法
var Axios = require('./core/Axios'); // 核心Axio方法
var mergeConfig = require('./core/mergeConfig'); // 配置参数的处理
var defaults = require('./defaults'); // 默认配置参数

创建实例

/**
 * Create an instance of Axios
 *
 * @param {Object} defaultConfig The default config for the instance
 * @return {Axios} A new instance of Axios
 */
function createInstance(defaultConfig) {
  // context实例自身拥有2个属性:defaults、interceptors
  // 原型对象上有一些方法:request、getUri、delete、get、post、head、options、put、patch
  var context = new Axios(defaultConfig);
  // bind返回一个新函数,这个新函数内部会调用这个request函数,改变Axios.prototype.request中的this指向
  var instance = bind(Axios.prototype.request, context);
	
  // Axios原型上的方法拷贝到instance上:request()/get()/post()/put()/delete()
  utils.extend(instance, Axios.prototype, context);

  // 将Axios实例对象(context)上的属性拷贝到instance上:defaults和interceptors属性
  utils.extend(instance, context);

  // 工厂模式,用于创建新的实例
  instance.create = function create(instanceConfig) {
    return createInstance(mergeConfig(defaultConfig, instanceConfig));
  };

  return instance;
}

// 创建实例
var axios = createInstance(defaults);

// 往外暴漏
axios.Axios = Axios;

在实例上挂着其他方法和属性

// 暴漏取消请求相关实例
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
axios.VERSION = require('./env/data').version;

// 暴漏all方法
axios.all = function all(promises) {
  return Promise.all(promises);
};
// 暴漏spread方法
axios.spread = require('./helpers/spread');

// 暴漏isAxiosError
axios.isAxiosError = require('./helpers/isAxiosError');
// 导出
module.exports = axios;

// TS方式下导出
module.exports.default = axios;

axios、axios.Axios与axios.instance三种方式创建实例的区别是什么?

  • 配置可能会有所不同
  • axios 全部采用默认参数的形式创建实例
  • axios.Axios 全部采用开发者传入的形式创建实例
  • axios.instance 采用开发者传入与默认合并的形式创建实例
  • 挂载的属性会有不同
  • axios 上挂载了更多的属性 (Cancel、 CancelToken、 isCancel、 all、 spread )

axios.spread和axios.all配合使用 axios.spread作为回调函数使用

// 源码
/*
 * @param {Function} callback
 * @returns {Function}
 */
module.exports = function spread(callback) {
  return function wrap(arr) {
    return callback.apply(null, arr);
  };
};
// 使用
axios.all([
  axios.get('https://api.github.com/users/mzabriskie'),
  axios.get('https://api.github.com/users/mzabriskie/orgs')
]).then(axios.spread(function (user, orgs) {})

参数配置处理

默认参数

默认参数在/lib/defaults.js

var defaults = {
  // 对transitional 属性进行版本校验,会提示自某个版本之后移除该属性
  // transitional包含了一些过度属性配置项,这些项对开发者不可见,主要是为了兼容之前的版本,经过配置可在较新版本中删除向后兼容的某些性质
  transitional: {},
  adapter: getDefaultAdapter(),
  transformRequest: [function transformRequest(data, headers) {}],
  transformResponse: [function transformResponse(data) {}],
  // `params` 是即将与请求一起发送的 URL 参数
  // 必须是一个无格式对象(plain object)或 URLSearchParams 对象
  params: {
    ID: 12345
  },
  // `paramsSerializer` 是一个负责 `params` 序列化的函数
  paramsSerializer: function(params) {
    return Qs.stringify(params, {arrayFormat: 'brackets'})
  },
  timeout: 0,
  // xsrfCookieName 是用作 xsrf token 值的 cookie 名称
  xsrfCookieName: 'XSRF-TOKEN',
  // xsrfHeaderName 是 xsrf token 值的 http 头名称
  xsrfHeaderName: 'X-XSRF-TOKEN',
  // maxContentLength 限制http响应的最大字节数
  maxContentLength: -1,
  // maxContentLength 限制请求体的最大字节数
  maxBodyLength: -1,
  // 提前拿到服务器相应过来的状态信息。如果该函数返会True或者null或者undefined, 则axios返回的promise状态则会被更改为resolved。 如果该函数返会false, 则该promise状态变为rejected
  validateStatus: function validateStatus(status) {
    return status >= 200 && status < 300;
  },
  // headers 是即将被发送的自定义请求头
  headers: {
    common: {
      'Accept': 'application/json, text/plain, */*'
    }
  }
}

adapter适配器,主要区分是浏览器环境还是node环境

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  }
  return adapter;
}

transformRequest, 允许在向服务器发送前,修改请求数据, 只能用在 ‘PUT’, ‘POST’ 和 ‘PATCH’ 这几个请求方法, 后面数组中的函数必须返回一个String,或 ArrayBuffer,或 Stream

transformResponse, 在传递给 then/catch 前,允许修改响应数据

xsrfCookieNamexsrfHeaderName是用来预防xsrf(跨站请求伪造)攻击

小插曲:登录认证一般有session认证,JWT(token)认证, session认证有一些缺点,最好的认证方式是token,token在前后端通信中一般有三种方式

1. get放在查询字符串中,URL中拼接(post可以放body中) 2. 放到Authorization/或者自定义headers中 3. 放到cookie中

xsrfCookieName && xsrfHeaderName主要就是对放到cookie中的token进行处理的键值

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

没有默认处理的配置

  • withCredentials
  • auth
  • responseEncoding
  • onUploadProgress
  • onDownloadProgress
  • maxRedirects
  • socketPath // socketPath 定义了一个在 node.js 中使用的 UNIX Socket。 例如 ‘/var/run/docker.sock’ 向 docker 守护进程发送请求。只能指定 socketPathproxy。如果两者都指定,则使用 socketPath
  • httpAgent // node.js 中用于自定义代理
  • httpsAgent
  • proxy
  • cancelToken
  • signal: new AbortController().signal, // an alternative way to cancel Axios requests using AbortController

除此之外还有一个配置项auth (密码式授权)

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

onUplodProgress, 方便文件上传时,进度的获取

var config = {
  onUploadProgress: function(progressEvent) {
    var percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total );
    console.log('percentCompleted', percentCompleted)
  }
};

axios.put('/upload/server', data, config)
  .then(function (res) {
  output.className = 'container';
  output.innerHTML = res.data;
})
  .catch(function (err) {
  output.className = 'container text-danger';
  output.innerHTML = err.message;
});

调取适配器发起请求

function dispatchRequest(config) {
  // ....config参数的处理
  var adapter = config.adapter || defaults.adapter;
  return adapter(config).then(function onAdapterResolution(response) {
    // 成功
  },function onAdapterRejection(reason) {
    // 失败
  })
}

浏览器环境的 xhr请求处理

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    // ...定义一些变量
    function done() {
      // 取消一些监听
    }
    
    var request = new XMLHttpRequest();
    // HTTP basic authentication
    // url的处理
    // Set the request timeout
    // 定义loadend函数
    function onloadend() {
      // 解析响应
      // 根据校验状态去处理promise
      settle(function _resolve(value) {
        resolve(value);
        done();
      }, function _reject(err) {
        reject(err);
        done();
      }, response);
    }
    if ('onloadend' in request) {
      // Use onloadend if available
      request.onloadend = onloadend;
    } else {
      // 使用 onreadystatechange 模拟 onload 事件
    }
    
    request.onabort = function () {}
    request.onerror = function () {}
    request.ontimeout = function () {}
    
    // Add xsrf header
    // Add headers to the request
    // Add withCredentials to request if needed
    // Add responseType to request if needed
    // Handle progress if needed 下载
    // Not all browsers support upload events
    
    // 。。。
    // 取消请求对处理
    if (config.cancelToken || config.signal) {
      // Handle cancellation
      // eslint-disable-next-line func-names
      onCanceled = function(cancel) {
        ...
      }
        
    }
    if (!requestData) {
      requestData = null;
    }

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

axios get设置参数 axios参数类型_vue

node环境下的适配器处理(lib/adapters/http.js),可以自行去学习了解

第二节:重要功能的实现

cancelToken实现

执行CancelToken函数,

1.创建一个promise对象,把promise对象赋值给promise属性,把resolve执行权暴漏出来

2.执行executor方法,把cancel作为executor的参数,在内部调用resolve,来取消请求

3.定义promise的.then方法和then的回调函数,.then方法中执行了订阅和取消订阅的监听方法,回调函数中执行取消方法的具体动作

实例上挂载source方法,方便调用,返回两个方法,token是创建实例,cancel是触发取消请求的方法

function CancelToken(executor) {
  // 取promise的resolve方法的控制权给resolvePromise
  var resolvePromise;

  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });
  this.promise.then(cancel)  // token.reason 传递过来的cancel实例
  this.promise.then = function (onfulfilled) {}
  ......
  executor(function cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }

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

module.exports = CancelToken;

拦截器与request处理

Axios原型对象,实例上定义了一个默认配置defaults,两个拦截器

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

原型方法request

Axios.prototype.request = function request(configOrUrl, config) {
  /*eslint no-param-reassign:0*/
  // Allow for axios('example/url'[, config]) a la fetch API
  console.log('request', configOrUrl, config)
  // 1. 配置项处理
  if (typeof configOrUrl === 'string') {
    config = config || {};
    config.url = configOrUrl;
  } else {
    config = configOrUrl || {};
  }

  config = mergeConfig(this.defaults, config);
  // 2. 请求方式处理
  // 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';
  }
 
  // 拦截器处理
  // filter out skipped interceptors
  // 请求拦截器储存数组
  var requestInterceptorChain = [];
  // 默认所有请求拦截器的变量
  var synchronousRequestInterceptors = true;

  // 遍历注册好的请求拦截器数组
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    // 这里interceptor是注册的每一个拦截器对象,axios请求拦截器向外暴露了runWhen配置
    // 来针对一些需要运行时检测来执行的拦截器
    // 如果配置了该函数,并且返回结果为true,则记录到拦截器链中,反之则直接结束该层循环
    if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
      return;
    }
    // interceptor.synchronous 是对外提供的配置,可标识该拦截器是异步还是同步 默认为false(异步)
    // 这里是来同步整个执行链的执行方式的,如果有一个请求拦截器为异步,
    // 那么下面的promise执行链则会有不同的执行方式
    synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
    // 塞到请求拦截器数组中
    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  // 响应拦截器存储数组
  var responseInterceptorChain = [];
  // 遍历按序push到拦截器存储数组中
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });

  var promise;
  // ***** 如果为异步 其实也是默认情况 ******//
  if (!synchronousRequestInterceptors) {
    // 创建存储链式调用的数组 首位是核心调用方法dispatchRequest,第二位是空
    var chain = [dispatchRequest, undefined];
    // 请求拦截器塞到前面
    Array.prototype.unshift.apply(chain, requestInterceptorChain);
    // 响应拦截器塞到后面
    chain = chain.concat(responseInterceptorChain);
    // 传参数config给dispatchRequest方法
    promise = Promise.resolve(config);
     // 循环 每次取两个出来组成promise链.then执行[]
    while (chain.length) {
      promise = promise.then(chain.shift(), chain.shift());
    }

    return promise;
  }

  // ***** 这里则是同步的逻辑  ******//
  // 请求拦截器一个一个的走 返回 请求前最新的config
  var newConfig = config;
  // 循环 每次取两个出来组成promise链.then执行
  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;
};

从源码中可以看出来,请求拦截器可以设置多个,并且请求拦截器是unshift方式存入数组,后定义的请求拦截器会先执行,因此同一个处理先定义的拦截器会覆盖后定义的拦截器

基于request 实现 其他方法

// Provide aliases for supported request methods
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: (config || {}).data
    }));
  };
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, data, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});

在源码中自己实现一个节流逻辑

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
  // 添加一个对象,来存储请求参数
  this.throttleParamsConfig = {}
}
// 节流的实现
function throttle(handle, wait) {
  let lastTime = new Date().getTime();
  return function (configOrUrl, config) {
    let nowTime = new Date().getTime();
    // 超过wait时间,清空缓存参数
    if (nowTime - lastTime > wait) {
      this.throttleParamsConfig = {}
    }
    // 处理当前请求的参数
    config = handleConfig(configOrUrl, config, this.defaults)
    // 不同的请求
    const nextFlag = this.saveRequestConfig(config)
    if(nextFlag) {
      lastTime = nowTime  
      const promise = handle.apply(this, arguments);
      return promise;
    }
  }
}
// 存储请求配置
Axios.prototype.saveRequestConfig = function saveRequestConfig (config) {
  // 节流处理
  const prevConfigArray = this.throttleParamsConfig[config.url]
  if (prevConfigArray) {
    console.log(2)
    if (prevConfigArray.includes(JSON.stringify(config))) {
      console.log(prevConfigArray)
      return false;
    } else {
      console.log(3, this.throttleParamsConfig[config.url])
      this.throttleParamsConfig[config.url] = prevConfigArray.concat([JSON.stringify(config)])
    }
  } else {
    console.log(1)
    this.throttleParamsConfig[config.url] = [JSON.stringify(config)]
  }
  return true;
}
// throttle包裹request,时间可以自定义,暴漏出去
Axios.prototype.request = throttle(function (configOrUrl, config) {
  // 1. 配置项处理
  config = handleConfig(configOrUrl, config, this.defaults)
  // 改变this指针
  bind(Axios.prototype.saveRequestConfig, this);
  // 以下为原有逻辑处理
  ....
}, 1000)

存储方式

axios get设置参数 axios参数类型_axios_02

思考->其他扩展

  • jsonp
  • 钩子函数(处理数据)
  • 队列的处理 (await的实现)
  • 防抖

总结

重点概念:

axios内部核心代码主要由适配器取消请求类request 方法实现,拦截器处理。

一些配置:

url处理,数据转换,上传下载的进度,auth授权,防xsrf攻击、代理等配置的支持

一些方法:

判断类型和环境 如:isPlainObject isStream isStandardBrowserEnv…

绑定类的 如:bind、extend

设计方法:

发布订阅者模式(取消请求类)

自定义实现:

节流实现、可尝试其他功能实现