axios是我们日常代码中常用的一个http库,它可以用来在浏览器或者node.js中发起http请求;它强大的功能和简单易用的API受到了广大前端童鞋们的青睐;那么它内部是如何来实现的呢,让我们走进它的源码世界一探究竟。
首先来看一下axios有哪些特性:
从浏览器中创建 XMLHttpRequests
从 node.js 创建 http 请求
支持 Promise API
拦截请求和响应
转换请求数据和响应数据
取消请求
自动转换 JSON 数据
客户端支持防御 XSRF
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等函数方便我们具体调用某个方法。
目录结构 看代码前先来看一下项目的整体结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ├── /dist/ ├── /lib/ │ ├── /cancel/ │ ├── /core/ │ │ ├── Axios.js │ │ ├── dispatchRequest.js │ │ ├── InterceptorManager.js │ │ └── settle.js │ ├── /helpers/ │ ├── /adapters/ │ │ ├── http.js │ │ └── xhr.js │ ├── axios.js │ ├── defaults.js │ └── utils.js ├── package.json ├── index.d.ts └── index.js
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
可以发现,我们需要用到的代码大多在/lib
目录下。
工具函数 看源码之前我们首先来学习一下axios用到的几个易于混淆的工具函数,以及它们具体是用来实现什么功能的。
bind bind函数用来给某一函数指定调用时的上下文,其源码如下:
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)instance ()
forEach forEach用来遍历对象或者数组;我们知道对象需要for in
遍历,而数组用for
循环遍历,forEach将两者遍历的方式整合到一起,其源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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上,并且指定方法调用的上下文,其源码如下:
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是执行上下文,我们可以通过代码尝试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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) target.name target.say ()
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
最后运行可以发现source对象上的属性方法都赋值到target对象上,执行上下文是context对象了。
merge merge函数用来将多个对象深度合并为一个新的对象,其源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function merge ( ) { 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测试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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)
构造实例 介绍完了工具函数我们就真正的进入axios的核心源码部分;首先在index.js
中,我们看到通过module.exports = require('./lib/axios');
导出了axios,因此我们找到/lib/axios
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 var Axios = require ('./core/Axios' );var utils = require ('./utils' );var bind = require ('./helpers/bind' );var defaults = require ('./defaults' );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; }var axios = createInstance (defaults);module .exports = axios;
这段代码看上去比较绕,不过我们发现核心部分就是通过createInstance
创建了一个axios实例对象,创建的同时传入了defaultConfig
对象(根据名字我们也能猜出来这是默认配置),然后将实例对象导出;因此createInstance
创建的就是我们使用的那个axios函数。
由于createInstance
创建是通过Axios构造函数创建的,因此我们把createInstance
放一放,先看一下Axios构造函数做了哪些操作:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 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 ) { 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给我们默认配置了哪些属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 var utils = require ('./utils' );var DEFAULT_CONTENT_TYPE = { 'Content-Type' : 'application/x-www-form-urlencoded' };function getDefaultAdapter ( ) { var adapter; if (typeof XMLHttpRequest !== 'undefined' ) { adapter = require ('./adapters/xhr' ); } else if (typeof process !== 'undefined' && Object .prototype .toString .call (process) === '[object process]' ) { 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) { } } 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
的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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是如何使用整合后的配置来通过适配器发起请求的。