一、Axios核心功能梳理
先看下Axios官方文档的介绍:
Axios is a promise-based HTTP Client for node.js and the browser. It is isomorphic (= it can run in the browser and nodejs with the same codebase). On the server-side it uses the native node.js http module, while on the client (browser) it uses XMLHttpRequests.
通过介绍我们了解到Axios
是一个可以运行在浏览器和Node.js
环境的基于Promise
的HTTP
库,那么既然是一个HTTP
库,核心功能自然是构造并发起HTTP
请求。那么我们就从Axios
如何实现一个GET/POST
请求为切入点进行阅读。
二、源码阅读
引用文档上第一个示例代码:
const axios = require('axios');
// Make a request for a user with a given ID
axios.get('/user?ID=12345')
.then(function (response) {
// handle success
console.log(response);
})
.catch(function (error) {
// handle error
console.log(error);
})
.then(function () {
// always executed
});
通过以上代码我们了解到:
-
Axios
库暴露出的axios
上有一个get
方法; - 使用
get
方法可以发起HTTP
请求; -
get
方法返回一个promise
对象;
下面我们围绕这三点展开阅读。
2.1 axios 对象和 get 方法
首先通过入口文件index.js
找到暴露对象来自./lib/axios.js
,打开该文件找到了module.exports.default = axios
这行代码,所以这个文件的axios
对象就是示例代码中使用的对象,我们看下这个对象是如何产生的。
传送门:./lib/axios.js
var Axios = require('./core/Axios');
/**
* Create an instance of Axios
*
* @param {Object} defaultConfig The default config for the instance
* @return {Axios} A new instance of Axios
*/
function createInstance(defaultConfig) {
var context = new Axios(defaultConfig);
var instance = bind(Axios.prototype.request, context);
// Copy axios.prototype to instance
utils.extend(instance, Axios.prototype, context);
// Copy context to instance
utils.extend(instance, context);
return instance;
}
// Create the default instance to be exported
var axios = createInstance(defaults);
传送门:./lib/core/Axios.js
/**
* Create a new instance of Axios
*
* @param {Object} instanceConfig The default config for the instance
*/
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
我们整理下上面代码的逻辑:
- 使用库提供的默认配置
defaults
通过Axios
构造函数生成一个上下文对象context
; - 声明变量
instance
为Axios.prototype.request
并且绑定this
为context
; - 在
instance
上扩展了Axios.prototype
的属性以及defaults
和interceptors
属性; - 返回
instance
,也就是说Axios
库暴露的对象就是这个的instantce
对象。
总结一下:axios
的本质是Axios.prototype.request
,并且扩展了Axios.prototype
的属性和方法。那么axios.get
也就是Axios.prototype
上的方法咯,所以我们再次打开Axios.js
文件一探究竟。
传送门:.lib/core/Axios.js
// 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
}));
};
});
从上面代码可以看出,axios
上的get
、post
、put
等方法都是Axios.prototype.request
的别名,所以使用Axios
发出的HTTP
请求其实都是Axios.prototype.request
发起的。这也就解释了为什么发起一个get
请求可以有2种写法:
axios.get(url)
axios({method: 'get', url: url})
2.2 axios.get 发起请求的过程
通过前面的代码,我们已经知道axios
所有的请求都是通过Axios.prototype.request
发起的,所以我们找到这个方法:
传送门:./lib/core/Axios.js
/**
* Dispatch a request
*
* @param {Object} config The config specific for this request (merged with this.defaults)
*/
Axios.prototype.request = function request(config) {
/*eslint no-param-reassign:0*/
// Allow for axios('example/url'[, config]) a la fetch API
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
config = mergeConfig(this.defaults, config);
// 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';
}
// 注意:这里我删去了与主流程无关的代码
// 删去的代码主要功能:
// 1. 判断拦截器是异步调用还是同步调用
// 2. 把拦截器的 fulfilled回调 和 rejected回调整理到数组中
var promise;
if (!synchronousRequestInterceptors) {
// 如果拦截器异步调用
var chain = [dispatchRequest, undefined];
Array.prototype.unshift.apply(chain, requestInterceptorChain);
chain.concat(responseInterceptorChain);
promise = Promise.resolve(config);
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
// 返回一个从请求拦截器开始的链式调用的 promise 对象
return promise;
}
// 如果拦截器是同步调用的
var newConfig = config;
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());
}
// 返回一个从 dispatchRequest 开始的链式调用的 promise 对象
return promise;
};
从上面代码我们了解到:在reqeust
方法中使用promise
对整个处理过程进行了封装,在没有拦截器的情况下返回的是dispatchRequest(newConfig)
,也就是说在dispatchRequest
中发起请求并返回一个promise
,我们找到dispatchRequest
的代码:
传送门: ./lib/core/dispatchRequest.js
module.exports = function dispatchRequest(config) {
throwIfCancellationRequested(config);
// 删除了对 headers 和对 data 的处理,查看源代码点击上方传送门
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
// 对响应的处理
return response;
}, function onAdapterRejection(reason) {
// 请求失败后对响应的处理
return Promise.reject(reason);
});
};
dispatchRequest
返回的是adapter
的结果,adapter
来自config
,而axios
是通过默认配置产生的,所以我们找到defaults.js
的代码:
传送门: ./lib/defaults.js
var defaults = {
adapter: getDefaultAdapter(),
// 删除无关的属性
}
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;
}
这里adapter
是一个适配器,因为axios
在浏览器时依赖XHR
对象,在node
环境运行时依赖底层的http
库,这两个对象都不是基于promise
的,但是Axios
希望和他们用promise
交互,所以这里需要适配器来做一个兼容,为交互双方提供桥梁。我们找到浏览器环境下的xhr.js
代码:
传送门: ./lib/adapters/xhr.js
module.exports = function xhrAdapter(config) {
// 在这里使用 axios 对 xhr 进行封装
return new Promise(function dispatchXhrRequest(resolve, reject) {
// 这里我去掉了与认证以及请求头相关的处理
// 实例化 xhr 对象
var request = new XMLHttpRequest();
// 构造 url
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
// Set the request timeout in MS
// 设置超时时间
request.timeout = config.timeout;
// loadend回调
function onloadend() {
if (!request) {
return;
}
// Prepare the response
var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
var responseData = !responseType || responseType === 'text' || responseType === 'json' ?
request.responseText : request.response;
var response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config: config,
request: request
};
settle(resolve, reject, response);
// Clean up request
request = null;
}
if ('onloadend' in request) {
// Use onloadend if available
request.onloadend = onloadend;
} else {
// Listen for ready state to emulate onloadend
request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) {
return;
}
// The request errored out and we didn't get a response, this will be
// handled by onerror instead
// With one exception: request that using file: protocol, most browsers
// will return status as 0 even though it's a successful request
// 这里看注释是为了处理当使用文件传输协议的时候浏览器会返回状态码 0
if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
return;
}
// readystate handler is calling before onerror or ontimeout handlers,
// so we should call onloadend on the next 'tick'
setTimeout(onloadend);
};
}
// 这里删除了防范xsrf,以及一些其他的 eventhandler
request.onabort = function handleAbort() {
// do sth ...
request = null;
};
// Handle low level network errors
request.onerror = function handleError() {
// do sth ...
request = null;
};
// Handle timeout
request.ontimeout = function handleTimeout() {
// do sth ...
request = null;
};
// Send the request
request.send(requestData);
});
};
上面的代码直接印证了文档所说的Axios
在浏览器是基于XHR
对象的,并且在这里清晰的展示了如何用promise
对xhr
进行封装。
总结下axios
发起请求的过程:
- 在
Axios.prototype.request
方法中针对请求拦截器的同步/异步返回不同的promise
(分别是拦截器的promise
和dispatchRequest
的promise
); - 在
dispatchReques
中对请求头和请求的实体做了处理,然后返回adapter
执行结果的promise
; - 通过判断环境选择不同的
adapter
,在浏览器中使用的是xhr.js
; - 在
xhr.js
中展示了使用XHR
对象请求的过程以及事件处理,还有promise
的封装过程。
2.3 axios 返回的是 promise 对象
在研究axios
如何发起http
请求时我们已经得到了结果:
- 使用了异步拦截器的请情况下,返回的是拦截器的
promise
; - 未使用异步拦截器或者未使用拦截器,返回的是
dispatchRequest
的promise
,底层是用promise
对xhr
的封装。
在查看xhr.js
的代码时,我对作者为什么要在onreadystatechange
中setTimeout(onloadend);
异步调用onloadend
有些不解,看了作者的注释说是因为onreadystatechange
会在ontimeout/onerror
之前调用,所以如果这里同步调用的话,就会使用settle
改变promise
的状态了,但是作者希望不在settle
中处理错误,而是通过xhr
的事件去处理,因为我从来直接使用过xhr
,所以我这里验证下:
var xhr = new XMLHttpRequest();
xhr.timeout = 1;
xhr.open('get', '/api', true);
xhr.ontimeout = function () {
console.log('timeout & redystate = ', xhr.readyState);
}
xhr.onerror = function () {
console.log('onerror & redystate = ', xhr.readyState);
}
xhr.onreadystatechange = function () {
console.log('onreadystatechange & redystate = ', xhr.readyState);
}
xhr.send(null)
结果如下:
执行结果
总结下:看xhr.js
的过程中了解了一些使用XHR
对象的坑,也借此机会去 MDN 上又重新学习了下XHR
,获益匪浅~
三、总结及计划
总结
通过阅读代码,我了解到:
-
Axios
整个项目的代码结构以及项目的大体思路; -
Axios
是如何管理拦截器的,方便下一步阅读拦截器代码; -
Axios
构造函数以及axios
对象的属性以及方法; -
Axios
是如何使用Promise
封装的;
计划
后续会按照官网给出的feature
分步阅读源码:
- Intercept request and response
- Transform request and response data
- Cancel requests
- Automatic transforms for JSON data
- Client side support for protecting against XSRF
网友评论