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的处理等,这也是大大方便开发者的功能之一,我们不用再担心这些细节的处理,只需要关注核心逻辑的实现。这也是优秀的组件和库的标准之一,暴露出简单、直接的接口让使用者调用,复杂、琐碎的逻辑隐藏在内部。
参考