深入学习Axios源码(构建配置)

前言

axios是我们日常代码中常用的一个http库,它可以用来在浏览器或者node.js中发起http请求;它强大的功能和简单易用的API受到了广大前端童鞋们的青睐;那么它内部是如何来实现的呢,让我们走进它的源码世界一探究竟。

axios特性

  • 从浏览器中创建 XMLHttpRequests
  • 从 node.js 创建 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换 JSON 数据
  • 客户端支持防御 XSRF

axios大致处理流程

深入学习Axios源码(构建配置)_第1张图片

Axios多种请求方式

  • axios除了以上的特性,还支持了多种请求方式,方便我们通过不同的方式来请求。
  • 第一种方式axios(option)
axios({
  url,
  method,
  headers,
  data
})
  • 第二种方式axios(url[, config])
axios('/api/list', {
  method,
  headers,
  data
})
  • 第三种方式axios.request(url?,option),第三种方式同第一种方式本质上一样
axios.request({
  url,
  method,
  headers,
  data
})
  • 第四种方式axios.request(url,option),第四种方式同第三种方式本质上一样
axios.request(url, {
  method,
  headers,
  data
})
  • 第五种方式axios[method](url,option),这种请求方式主要针对get、delete、head、options方法
axios.get(url,{
  headers,
  params
})
  • 第六种方式axios[method](url,data,option),这种请求方式主要针对post、put、patch方法
axios.post(url, data, {
  headers,
})

这六种请求方式也是我们常见的方式;我们发现前两种请求方式axios作为一个函数直接来请求,而后面四种方式axios则是一个对象;因此我们猜测,axios首先肯定是一个函数,这个函数上又挂载了request、get、post等函数方便我们具体调用某个方法。

目录结构

看代码前先来看一下项目的整体结构

├── /dist/ # 项目输出目录
├── /lib/ # 项目源码目录
│ ├── /cancel/ # 定义取消功能
│ ├── /core/ # 一些核心功能
│ │ ├── Axios.js # axios的核心主类
│ │ ├── dispatchRequest.js # 用来调用http请求适配器方法发送请求
│ │ ├── InterceptorManager.js # 拦截器构造函数
│ │ └── settle.js # 根据http响应状态,改变Promise的状态
│ ├── /helpers/ # 一些辅助方法
│ ├── /adapters/ # 定义请求的适配器 xhr、http
│ │ ├── http.js # 实现http适配器
│ │ └── xhr.js # 实现xhr适配器
│ ├── axios.js # 对外暴露接口
│ ├── defaults.js # 默认配置
│ └── utils.js # 公用工具
├── package.json # 项目信息
├── index.d.ts # 配置TypeScript的声明文件
└── index.js # 入口文件

可以发现,我们需要用到的代码大多在/lib目录下。

工具函数

看源码之前我们首先来学习一下axios用到的几个易于混淆的工具函数,以及它们具体是用来实现什么功能的。

bind

bind函数用来给某一函数指定调用时的上下文,其源码如下:

//lib/helpers/bind.js
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);
  };
};

它的实现效果同Function.prototype.bind

import bind from '/lib/helpers/bind.js'

var obj = {
  name: 'hello'
}

function showName() {
  console.log(this.name)
}

var instance = bind(showName, obj)
//效果同下
//var instance = showName.bind(obj)
instance()

forEach

forEach用来遍历对象或者数组;我们知道对象需要for in遍历,而数组用for循环遍历,forEach将两者遍历的方式整合到一起,其源码如下:

//lib/utils.js
function forEach(obj, fn) {
  if (obj === null || typeof obj === 'undefined') {
    return;
  }
  //兼容不是数组对象的情况
  //如果是基本数据类型同样放到数组中遍历
  if (typeof obj !== 'object') {
    obj = [obj];
  }
  if (isArray(obj)) {
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj);
    }
  } else {
    for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj);
      }
    }
  }
}

可以看出forEach对字符串或者数字等基本数据类型做了兼容,对数组和对象做了不同的遍历处理;其中如果遍历的是对象,回调函数每次返回对象的值、键以及对象本身。

import { forEach } from '/lib/utils.js'
forEach([1,2],(elem, index, array)=>{})
forEach({
    name: 'hi',
    age: 18
},(value, key, object)=>{})

extend

extend将一个对象b上面所有的属性和方法扩展到另一个对象a上,并且指定方法调用的上下文,其源码如下:

//lib/utils.js
function extend(a, b, thisArg) {
  forEach(b, function assignValue(val, key) {
    if (thisArg && typeof val === 'function') {
      a[key] = bind(val, thisArg);
    } else {
      a[key] = val;
    }
  });
  return a;
}

这里forEach就用来遍历对象了;a是目标对象,b是源对象,thisArg是执行上下文,我们可以通过代码尝试一下:

import { extend } from '/lib/utils.js'
var context = {
    name: 'context'
}
var target = {
    name: 'target',
    say() {
        console.log('i am target,my name is ' + this.name)
    }
}
var source = {
    name: 'source',
    say() {
        console.log('i am source,my name is ' + this.name)
    }
}
extend(target, source, context)
//source
target.name
//i am source,my name is context
target.say()

最后运行可以发现source对象上的属性方法都赋值到target对象上,执行上下文是context对象了。

merge

merge函数用来将多个对象深度合并为一个新的对象,其源码如下:

//lib/utils.js
function merge(/* obj1, obj2, obj3, ... */) {
  var result = {};
  function assignValue(val, key) {
    if (isPlainObject(result[key]) && isPlainObject(val)) {
      result[key] = merge(result[key], val);
    } else if (isPlainObject(val)) {
      result[key] = merge({}, val);
    } else if (isArray(val)) {
      result[key] = val.slice();
    } else {
      result[key] = val;
    }
  }

  for (var i = 0, l = arguments.length; i < l; i++) {
    forEach(arguments[i], assignValue);
  }
  return result;
}

isPlainObject判断一个对象是否是一个JS原生对象,即使用Object构造函数创建的对象;如果是对象的话就进行深度的合并,我们写一个demo测试一下:

import { merge } from '/lib/utils.js'

var obj1 = {
  a:1,
  b: {
    b1:1,
    b2:2
  }
}
var obj2 = {
  a:2
  b: {
    b1:4,
    b3:5
  },
}
var newObj = merge(obj1, obj2)
//最后结果
//{
//  a:2,
//  b: {
//    b1:4,
//    b2:2,
//    b3:5
//  }
//}

构造实例

介绍完了工具函数我们就真正的进入axios的核心源码部分;首先在index.js中,我们看到通过module.exports = require('./lib/axios');导出了axios,因此我们找到/lib/axios文件:

//省略部分代码
///lib/axios.js
var Axios = require('./core/Axios');
var utils = require('./utils');
var bind = require('./helpers/bind');
//默认配置
var defaults = require('./defaults');

function createInstance(defaultConfig) {
  //创建axios实例
  var context = new Axios(defaultConfig);
  //将instance指向Axios.prototype.request函数
  var instance = bind(Axios.prototype.request, context);
  //将Axios.prototype上的属性和方法扩展到instance上
  utils.extend(instance, Axios.prototype, context);
  //将context上的属性和方法扩展到instance上
  utils.extend(instance, context);
  return instance;
}

var axios = createInstance(defaults);
module.exports = axios;

这段代码看上去比较绕,不过我们发现核心部分就是通过createInstance创建了一个axios实例对象,创建的同时传入了defaultConfig对象(根据名字我们也能猜出来这是默认配置),然后将实例对象导出;因此createInstance创建的就是我们使用的那个axios函数。

由于createInstance创建是通过Axios构造函数创建的,因此我们把createInstance放一放,先看一下Axios构造函数做了哪些操作:

var utils = require('./../utils');
var buildURL = require('../helpers/buildURL');
var InterceptorManager = require('./InterceptorManager');
var dispatchRequest = require('./dispatchRequest');
var mergeConfig = require('./mergeConfig');
function Axios(instanceConfig) {
  //默认配置
  this.defaults = instanceConfig;
  //拦截器
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

Axios.prototype.request = function request(config) {
  //兼容axios.request(url,config)的情况
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }
  config = mergeConfig(this.defaults, config);
  var promise = Promise.resolve(config);
  //省略部分发送请求的代码
  return promise;
};

utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url
    }));
  };
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  Axios.prototype[method] = function(url, data, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});
module.exports = Axios;

Axios构造函数仅仅做了两个事,一个是将默认配置保存到defaults,另一个则是构造了interceptors拦截器对象;Axios函数在原型对象上还挂载了request、get、post等函数,但是get、post等函数最终都是通过request函数来发起请求的。而且request函数最终返回了一个Promise对象, 因此我们才能通过then函数接收到请求结果。对原型链不了解的童鞋可以看这篇文章一文读懂JS中类、原型和继承。

了解了Axios构造函数的本质,让我们再回到createInstance函数:

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

我们发现Axios构造出了实例对象context,然而createInstance并不是直接返回了context对象;这是因为上面我们也说了axios是一个函数,然而context是对象,返回对象的话是并不能直接调用的,那怎么办呢?

我们在Axios源码中发现,真正调用的是Axios原型链上的request方法;因此导出的axios需要关联到request方法,这里巧妙的通过bind函数进行关联,生成关联后的instance函数,同时指定它的调用上下文就是Axios的实例对象,因此instance调用时也能获取到实例对象上的defaults和interceptors属性;但是仅仅关联request还不够,再通过extend函数将Axios原型对象上的所有get、post等函数扩展到instance函数上,因此这也是我们才能够使用多种方式调用的原因所在。

同时,如果我们需要创建多个axios实例,但是某几个axios实例的配置(用了同样的域名等)是一样的,我们不希望每次都要写重复的配置,axios还提供了另一种创建实例模板的方式:

const instance = axios.create({
  baseURL: 'https://some-domain.com/api/',
  timeout: 1000,
  headers: {'X-Custom-Header': 'foobar'}
});
instance.get('/list')
instance.post('/add')

通过create函数创建了一个有默认配置的实例,这样我们只需要愉快的调用axios的API方法即可;需要请求其他域名只需要再次create即可,这也是工厂模式的一种体现,它的源码实现也很简单,也是通过createInstance创建一个合并配置后的实例:

axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

配置合并

实例对象创建好之后,我们就需要把配置进行合并,方便后面发送请求。在看源码前,我们发现上面代码中主要有两种config,一种是defaultConfig,即默认配置,在构造实例的时候传入;另一种是userConfig,也就是我们调用实例进行请求时传入的配置;在上面的代码中,也出现了很多次mergeConfig这个函数,根据命名我们也能看出来,这是用来合并两种配置的。

默认配置

首先让我们看一下axios给我们默认配置了哪些属性:

//lib/defaults
var utils = require('./utils');
var DEFAULT_CONTENT_TYPE = {
  'Content-Type': 'application/x-www-form-urlencoded'
};
function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    //浏览器环境使用xhr
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' 
    && Object.prototype.toString.call(process) === '[object process]') {
    //node环境使用http
    adapter = require('./adapters/http');
  }
  return adapter;
}
var defaults = {
  adapter: getDefaultAdapter(),
  transformRequest: [function transformRequest(data, headers) {
    //省略转换请求数据代码
    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,
  maxBodyLength: -1,
  validateStatus: function validateStatus(status) {
    return status >= 200 && status < 300;
  }
};
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(DEFAULT_CONTENT_TYPE);
});
module.exports = defaults;

可以发现,axios定义了默认的适配器(用于发送请求)、转换器(转换请求和响应数据)和请求头等一些数据;getDefaultAdapter函数用来获取默认的适配器,这样在浏览器端和node环境都可以发送请求;可以看到axios给每个请求方法都定义了一个默认的请求头和一个公共的请求头common,在后面发送请求时会根据传入的请求方法类型使用相应的请求头。

属性分类

下面我们就来看一下mergeConfig是如何将两种config合并的;首先mergeConfig将所有config中用到的字段进行了划分,分成了三类:

  • 没有初始值,其值必须由初始化的时候指定
  • 需要合并的属性,这些属性一般都是对象,处理时需要将对象进行合并
  • 普通属性,这些属性一般都是值类型,如果userConfig中有,则以userConfig为准;没有取defaultConfig的值
//可以把config1看做defaultConfig,config2看做userConfig
module.exports = function mergeConfig(config1, config2) {
  config2 = config2 || {};
  //最终返回合并后的配置
  var config = {};
  //第一种属性
  var valueFromConfig2Keys = ['url', 'method', 'data'];
  //第二种属性
  var mergeDeepPropertiesKeys = ['headers', 'auth', 'proxy', 'params'];
  //第三种属性
  var defaultToConfig2Keys = [
    'baseURL', 'transformRequest', 'transformResponse', 'paramsSerializer',
    'timeout', 'timeoutMessage', 'withCredentials', 'adapter', 'responseType', 'xsrfCookieName',
    'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress', 'decompress',
    'maxContentLength', 'maxBodyLength', 'maxRedirects', 'transport', 'httpAgent',
    'httpsAgent', 'cancelToken', 'socketPath', 'responseEncoding'
  ];
  var directMergeKeys = ['validateStatus'];
  //省略下面代码
}

对于第一种属性,url、method和data不能从默认配置中取值,因此如果userConfig中有就直接取userConfig中的值。

function getMergedValue(target, source) {
  if (utils.isPlainObject(target) && utils.isPlainObject(source)) {
    return utils.merge(target, source);
  } else if (utils.isPlainObject(source)) {
    return utils.merge({}, source);
  } else if (utils.isArray(source)) {
    return source.slice();
  }
  return source;
}
utils.forEach(valueFromConfig2Keys, function valueFromConfig2(prop) {
  if (!utils.isUndefined(config2[prop])) {
    config[prop] = getMergedValue(undefined, config2[prop]);
  }
});

对于第二种属性,如果userConfig中有,就将其与defaultConfig进行合并。

function mergeDeepProperties(prop) {
  if (!utils.isUndefined(config2[prop])) {
    config[prop] = getMergedValue(config1[prop], config2[prop]);
  } else if (!utils.isUndefined(config1[prop])) {
    config[prop] = getMergedValue(undefined, config1[prop]);
  }
}
utils.forEach(mergeDeepPropertiesKeys, mergeDeepProperties);

对于第三种普通属性,如果userConfig中有就取userConfig,没有就取defaultConfig中的值。

utils.forEach(defaultToConfig2Keys, function defaultToConfig2(prop) {
  if (!utils.isUndefined(config2[prop])) {
    config[prop] = getMergedValue(undefined, config2[prop]);
  } else if (!utils.isUndefined(config1[prop])) {
    config[prop] = getMergedValue(undefined, config1[prop]);
  }
});

对于除这三种属性之外的其他属性,当做第二种属性处理,通过mergeDeepProperties函数进行整合。

var axiosKeys = valueFromConfig2Keys
  .concat(mergeDeepPropertiesKeys)
  .concat(defaultToConfig2Keys)
  .concat(directMergeKeys);

//获取不在上述三种属性的中的其他属性的数组
var otherKeys = Object
  .keys(config1)
  .concat(Object.keys(config2))
  .filter(function filterAxiosKeys(key) {
    return axiosKeys.indexOf(key) === -1;
  });

utils.forEach(otherKeys, mergeDeepProperties);

总结

axios的源码还是有很多的地方值得我们来深入学习的,比如工具函数和它如何构造实例等;我们根据axios的多种请求方式,找到了它在构造实例时巧妙的绑定方式来实现多种请求的调用,构造实例后就需要将用户传入的配置和默认的配置进行整合起来;在下一篇文章中我们会了解axios是如何使用整合后的配置来通过适配器发起请求的。

你可能感兴趣的:(Javascript,Vue,笔记,javascript,vue.js)