Axios是一个基于Promise的HTTP请求库,可以用在浏览器和Node.js中。平时在Vue项目中,经常使用它来实现HTTP请求。
它的使用简便、灵活,并且有interceptors、数据转换器等强大的功能,以前用的时候并没有仔细研究过这些功能是如何实现的,正好在知乎的大前端专栏看到一篇文章对Axios的源码进行了解读。借着这篇文章的帮助,我开始了自己阅读源码的道路。
以后要多多的读源码,更多的独立完成,向大神们学习。
Axios的目录结构
Axios的目录结构相对还是比较简单的

目录里面adapters/目录下定义的是如何发出一个HTTP请求,这也就是为什么Axios技能应用在浏览器中(XHR)又能用在Node.js中(http.request),core/Axios.js是Axios的核心主类,axios.js是整个Axios的入口。
Axios的实现流程
1 2 3 4 5 6 7 8
| graph TB 引入axios-->Axios构造函数实例化 Axios构造函数实例化-->Interceptors请求拦截器 Interceptors请求拦截器-->dispatchRequest方法 dispatchRequest方法-->请求转换器transformRequest 请求转换器transformRequest-->http请求适配器adapter http请求适配器adapter-->响应转换器transformResponse 响应转换器transformResponse-->Interceptors响应拦截器
|
工具函数的学习
forEach
这个forEach与原生的数组的forEach并不相同,它可以遍历对象,也可以遍历数组,还可以遍历基本值:
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
| 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); } } } }
|
merge和deepMerge
用来合并对象,二者的区别只是对于嵌套的深层的对象,deepMerge也会进行深层的拷贝,而不是指针的改变
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
| function merge(/* obj1, obj2, obj3, ... */) { var result = {}; function assignValue(val, key) { if (typeof result[key] === 'object' && typeof val === 'object') { result[key] = merge(result[key], val); } else { result[key] = val; } }
for (var i = 0, l = arguments.length; i < l; i++) { forEach(arguments[i], assignValue); } return result; }
function deepMerge(/* obj1, obj2, obj3, ... */) { var result = {}; function assignValue(val, key) { if (typeof result[key] === 'object' && typeof val === 'object') { result[key] = deepMerge(result[key], val); } else if (typeof val === 'object') { result[key] = deepMerge({}, val); } else { result[key] = val; } }
for (var i = 0, l = arguments.length; i < l; i++) { forEach(arguments[i], assignValue); } return result; }
|
isStandardBrowserEnv
用来判断是否是标准的浏览器环境,对于Web Workers,
1 2
| typeof window -> undefined typeof document -> undefined
|
对于RN和NativeScript
1 2 3 4 5
| react-native: navigator.product -> 'ReactNative'
nativescript navigator.product -> 'NativeScript' or 'NS'
|
所以有:
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
|
function isStandardBrowserEnv() { if (typeof navigator !== 'undefined' && (navigator.product === 'ReactNative' || navigator.product === 'NativeScript' || navigator.product === 'NS')) { return false; } return ( typeof window !== 'undefined' && typeof document !== 'undefined' ); }
|
Axios的多种使用方式
Axios有多种使用方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import axios from 'axios';
axios({ url, method, headers });
axios(url, { method, headers })
axios.get(url, { headers })
axios.post(url, data, { headers })
axios.request({ url, method, headers })
|
下面从入口文件axios.js来分析这些使用方式都是如何实现的
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 53 54
|
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);
axios.Axios = Axios;
axios.create = function create(instanceConfig) { return createInstance(mergeConfig(axios.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;
|
有个疑问,为什么createInstance函数需要绕那么大一个弯,而不是直接导出new Axios的实例呢?我的理解是如果直接导出new Axios是没有办法使用axios(option)和axios(url, option)这两种方式来实现调用。
createInstance最终返回的是一个函数,它指向Axios.prototype.request,并且绑定了new Axios实例作为上下文对象,同时这个导出的函数还有Axios.prototype以及new Axios实例的各个方法和属性作为其静态属性和方法,这些方法的上下文都指向new Axios这同一个对象。
上面的代码解释了除了第二种Axios的调用方法之外的调用方法,第二种调用方法是在Axios.prototype.request中对第一个参数的数据类型进行判断来实现的,后面在学习Axios.prototype.request代码时会提到。
Axios类
Axios.js是Axios的核心,它声明了Axios这个类,并在原型添加了一些方法,其中最核心的就是request方法,上面提到的各种调用方法都是通过调用request方法实现的。
首先来看Axios类的声明
1 2 3 4 5 6 7 8 9 10
| function Axios(instanceConfig) { this.defaults = instanceConfig; this.interceptors = { request: new InterceptorManager(), response: new InterceptorManager() }; }
|
Axios类只声明了两个实例属性,拦截器属性都是InterceptorManager的实例。InterceptorManager这个类位于core/InterceptorManager.js,并不复杂,定义了一个实例属性来存放拦截器,定义了一些原型方法来对队列中拦截器进行添加、移除和遍历的操作
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
| function InterceptorManager() { this.handlers = []; }
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; } };
InterceptorManager.prototype.forEach = function forEach(fn) { utils.forEach(this.handlers, function forEachHandler(h) { if (h !== null) { fn(h); } }); };
|
实际上Axios的实例属性interceptors.request用来存放请求拦截器,interceptors.response用来存放响应拦截器,
然后来看核心的request方法的代码:
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
| 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); config.method = config.method ? config.method.toLowerCase() : 'get';
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; };
|
个人感觉上面的代码十分巧妙,首先生命了chain这个数组,填了两个成员dispatchRequest和undefined,然后定义了一个立刻resolve的Promise对象promise,它返回的是config对象。
我们在添加请求拦截器时:
1 2 3 4 5 6 7 8
| const myRequestInterceptor = axios.interceptors.request.use(config => { return config; }, error => { return Promise.reject(error); });
|
使用了use方法,将请求拦截器添加到了this.interceptors.request.handlers对列中,然后通过forEach方法,对这个队列进行遍历,要注意请求拦截器使用的是unshift方法,添加到dispatchRequest前面,而响应拦截器使用push方法添加到undefined后面,所以对于请求拦截器来说,先添加的拦截器会后执行,对于响应拦截器来说,先添加的拦截器会先执行:
1 2 3 4
| axios.interceptors.request.use(fn1, fn1_1); axios.interceptors.request.use(fn2, fn2_1); axios.interceptors.response.use(fn3, fn3_1); axios.interceptors.response.use(fn4, fn4_1);
|
按照上面的顺序添加的拦截器,存储到chain数组中是这样的:
1
| [fn2, fn2_1, fn1, fn1_1, dispatchRequest, undefined, fn3, fn3_1, fn4, fn4_1]
|
InterceptorManager.prototype.use方法接受两个参数分别是Promise成功和失败的回调函数,如果之传入一个函数,那么默认失败的情况对应的就是undefined。所以chain数组中是两个成员为一组的,分别对应一次Promise状态改变的两个回调函数。
然后对chain进行循环:
1 2 3
| while (chain.length) { promise = promise.then(chain.shift(), chain.shift()); }
|
在循环过程中对promise重新复制,then方法中的两个参数分别是chain.shift(),chain.shift()的作用有二:
- 减小
chain的长度
- 将剪切得到的两个
chain成员作为then方法的两个参数执行。
Axios规定,在使用拦截器时,请求拦截器必须返回config对象,响应拦截器必须返回response对象,这样才能实现promise的链式调用
在请求拦截器中进行链式调用时,将config对象作为Promise的结果进行传递,使得所有请求拦截器共享config对象,直到真正发出请求的dispatchRequest接收到config对象并发出请求后将接收到的response作为结果返回给后续的响应拦截器,并继续传递。
chain数组中的undefined是作为dispatchRequest一组的then方法的第二个回调函数,它的作用是兜住最后一个响应拦截器的错误对象,不会破坏chain两个回调函数一组的匹配顺序。
Axios.js中还有一些其他的代码,主要的作用是为Axios.prototype添加了get、post等方法,实际上都是调用Axios.prototype.request方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) { Axios.prototype[method] = function(url, config) { return this.request(utils.merge(config || {}, { method: method, url: url })); }; });
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 })); }; });
|
dispathReqeust
上面提到的,真正发出HTTP请求的是dispathReqeust方法,dispathReqeust主要完成了三件事:
- 拿到
config对象,进行处理、合并,传递给HTTP请求适配器
- HTTP请求适配器根据
config对象发起HTTP请求
- 请求完成后,根据数据转换器对得到的数据进行二次处理,返回
response
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| 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;
return adapter(config).then(function onAdapterResolution(response) { throwIfCancellationRequested(config);
response.data = transformData( response.data, response.headers, config.transformResponse );
return response; }, function onAdapterRejection(reason) { 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); }); };
|
dispathReqeust方法返回一个Promise,携带着成功求情后的响应数据,或者是失败后的错误对象。用户就可以在调用axios()方法后的then或者catch中进行业务处理了。
Adapter
上面代码的注释里面提到了,在dispatchRequest中通过config.adapter或者defaults.adapter指定HTTP请求适配器,一般来说默认的适配器就可以满足需要,默认的适配器会根据环境自动选择XHR或者Node的http.request方法发送网络请求
在defaults.js中的adatper方法完成的就是根据环境选择HTTP适配器
1 2 3 4 5 6 7 8 9 10 11 12
| function getDefaultAdapter() { var adapter; if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { adapter = require('./adapters/http'); } else if (typeof XMLHttpRequest !== 'undefined') { adapter = require('./adapters/xhr'); } return adapter; }
|
Axios是基于Promise的,而HTTP请求的实现又是通过传统的Ajax实现的,所以adapter/xhr.js的主要功能就是面试时经常遇到的一道题,将Ajax改为Promise的形式。来学习一下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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
|
module.exports = function xhrAdapter(config) { return new Promise(function dispatchXhrRequest(resolve, reject) { var requestData = config.data; var requestHeaders = config.headers; if (utils.isFormData(requestData)) { delete requestHeaders['Content-Type']; } var request = new XMLHttpRequest();
if (config.auth) { var username = config.auth.username || ''; var password = config.auth.password || ''; requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password); } 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.onabort = function handleAbort() { if (!request) { return; } reject(createError('Request aborted', config, 'ECONNABORTED', request));
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 { 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); request = null; }); } if (requestData === undefined) { requestData = null; }
request.send(requestData); }); };
|
xhrAdapter的XHR发送请求成功后会执行Promise对象的resolve方法,将请求的数据传递出去,如果请求失败(超时、网络出错、终止)则执行reject方法,并将自定义的错误信息传递出去。
Settle
xhrAdapter中将改变Promise状态的功能抽离成为单独的settle方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
module.exports = function settle(resolve, reject, response) { var validateStatus = response.config.validateStatus; if (!validateStatus || validateStatus(response.status)) { resolve(response); } else { reject(createError( 'Request failed with status code ' + response.status, response.config, null, response.request, response )); } };
|
validateStatus接受response.stastus作为参数,对于给定的HTTP状态码确定其成功失败状态,比如:
1 2 3
| validateStatus: function (status) { return status >= 200 && status < 300; }
|
数据转换器
前面也提到了数据转换器,可以对请求对象和响应和数据进行转换,可以全局使用:
1 2 3 4 5 6 7 8 9 10 11
| axios.defaults.transformRequest.push((data, headers) => { return data; });
axios.defaults.transformResponse.push((data, headers) => { return data; });
|
也可以在单次请求中使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| axios.get(url, { transformRequest: [ ...axios.defaults.transformRequest, (data, headers) => { return data; } ], transformResponse: [ ...axios.defaults.transformResponse, (data, headers) => { return data; } ], })
|
在defaults配置项中已经默认自定义了一个请求转换器和响应转换器
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
| transformRequest: [ function transformRequest(data, headers) { normalizeHeaderName(headers, 'Accept'); 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) { } } return data; } ],
|
默认的请求转换器对请求数据和请求头进行标准化处理,默认的响应转换器用来自动将字符串解析为JSON对象。
使用的时候是通过transformData这个方法,对数组中的转换器进行遍历:
1 2 3 4 5 6 7 8 9
| module.exports = function transformData(data, headers, fns) { utils.forEach(fns, function transform(fn) { data = fn(data, headers); });
return data; };
|
转换器和拦截器都可以对请求和响应的数据进行拦截处理,但是一般情况下,拦截器主要负责拦截修改config配置项,数据转换器主要用来负责拦截转换请求主体和响应数据。
Cancel
Axios提供了取消请求的功能,有两种使用方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| axios.get(url, { cancelToken: new axios.CancelToken(cancel => { if () { cancel('取消日志'); } }) });
const CancelToken = axios.CancelToken; const source = CancelToken.source(); axios.get(url, { cancelToken: source.token }); source.cancel('取消日志');
|
Axios用了三个模块来实现这个功能,首先是Cancel这个类:
1 2 3 4 5 6 7 8 9 10 11 12
| function Cancel(message) { this.message = message; }
Cancel.prototype.toString = function toString() { return 'Cancel' + (this.message ? ': ' + this.message : ''); };
Cancel.prototype.__CANCEL__ = true;
module.exports = Cancel;
|
主要是定义了Cancel的message实例属性,和原型上的内部用的__CANCEL__属性,还定义了一个toString方法
isCancel返回布尔值,根据是否传入了value以及是否有__CANCEL__属性,判断是否是Cancel实例
核心的代码在CancelToken.js中:
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
|
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) { return; }
token.reason = new Cancel(message); resolvePromise(token.reason); }); }
CancelToken.prototype.throwIfRequested = function throwIfRequested() { if (this.reason) { throw this.reason; } };
CancelToken.source = function source() { var cancel; var token = new CancelToken(function executor(c) { cancel = c; }); return { token: token, cancel: cancel }; };
|
总结
阅读Axios的过程,还是学到了很多东西:
- Promise的串联操作
- 拦截器的添加和执行原理
- 将Promise的控制权导出,让外界决定Promise的状态
还有很繁琐也很重要的一部分没有涉及,就是针对HTTP请求的标准化处理,比如Heder的处理等,这也是大大方便开发者的功能之一,我们不用再担心这些细节的处理,只需要关注核心逻辑的实现。这也是优秀的组件和库的标准之一,暴露出简单、直接的接口让使用者调用,复杂、琐碎的逻辑隐藏在内部。
参考