Axios请求适配器:XHR与HTTP模块实现详解
<|begin▁of▁sentence|># 1. 前言
在上一篇文章中,我们分析了`axios` 的请求响应流程,以及拦截器的实现原理。我们知道,在发送请求的过程中,配置对象`config` 已经经历了请求拦截器`request interceptor` 和 请求转换`transformRequest` 的处理,那么接下来就该真正地把请求发出去了,而发出请求的正是我们的**请求适配器**。
在`axios` 中,如果用户没有手动配置适配器,那么`axios` 就会根据当前环境来自动选择适配器,**它优先选择`XMLHttpRequest(浏览器端)`,如果没有就选择`http(node端)**`。那么,`axios` 内部是如何来实现的呢?接下来,我们就对这块内容进行分析。
# 2. 如何选择请求适配器
我们通过`axios` 发送请求都是通过`axios.request` 方法,而该方法最终调用的是`dispatchRequest` 方法,在该方法中有这样一段代码,如下:
```javascript
// 如果已经配置了适配器,那么就使用配置的适配器
var adapter = config.adapter || defaults.adapter;
```
从代码中可以看到,`axios` 允许用户手动配置适配器,如果配置了就使用用户配置的适配器,如果没有配置就使用默认的适配器`defaults.adapter`。而默认的适配器是在`lib/defaults.js` 文件中,我们看看它内部是如何定义的,如下:
```javascript
function getDefaultAdapter() {
var adapter;
// 判断XMLHttpRequest对象是否存在,如果存在则使用该对象来发送请求
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;
}
var defaults = {
adapter: getDefaultAdapter(),
// ...
};
```
从代码中可以看到:`axios` 会判断当前环境是否存在`XMLHttpRequest` 对象,如果存在就使用`XHR` 适配器来发送请求,如果不存在就判断当前环境是否为`node` 环境,如果是就使用`http` 适配器来发送请求。所以,`axios` 的默认适配器在浏览器端就是`XHR` 适配器,在`node` 端就是`http` 适配器。
# 3. XHR 适配器
`XHR` 适配器的源码是在`lib/adapters/xhr.js` 文件中,由于该文件代码较多,我们就分部分来解读。
## 3.1 模块引入
首先,该模块引入了一些工具函数,如下:
```javascript
var utils = require('./../utils');
var settle = require('./../core/settle');
var buildURL = require('./../helpers/buildURL');
var buildFullPath = require('../core/buildFullPath');
var parseHeaders = require('./../helpers/parseHeaders');
var isURLSameOrigin = require('./../helpers/isURLSameOrigin');
var createCookie = require('./../helpers/createCookie');
```
- `utils`:工具函数;
- `settle`:根据响应状态改变`Promise` 的状态;
- `buildURL`:构建带查询参数的`URL`;
- `buildFullPath`:构建完整的请求`URL`;
- `parseHeaders`:将字符串解析为响应头对象;
- `isURLSameOrigin`:判断请求`URL` 是否与当前页面同源;
- `createCookie`:创建`cookie`;
## 3.2 导出模块
接下来,该模块导出了一个函数,如下:
```javascript
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
// ...
});
};
```
从代码中可以看到:该模块导出了一个函数,该函数接收配置对象`config` 作为参数,并且返回一个`promise`。
## 3.3 函数内部实现
接下来,我们看看该函数内部是如何实现的。
### 3.3.1 初始化变量
首先,函数内部初始化了一些变量,如下:
```javascript
var requestData = config.data;
var requestHeaders = config.headers;
var responseType = config.responseType;
// 如果请求数据是FormData类型,则删除Content-Type请求头,让浏览器自动设置
if (utils.isFormData(requestData)) {
delete requestHeaders['Content-Type'];
}
```
### 3.3.2 创建 XHR 对象
接下来,创建`XHR` 对象,如下:
```javascript
var request = new XMLHttpRequest();
```
### 3.3.3 监听 readystatechange 事件
然后,监听`XHR` 对象的`readystatechange` 事件,如下:
```javascript
request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) {
return;
}
// 请求出错或者超时,该事件也会触发,但是此时status为0,所以需要排除
if (
request.status === 0 &&
!(request.responseURL && request.responseURL.indexOf('file:') === 0)
) {
return;
}
// 响应头
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,
};
// 根据响应状态改变Promise的状态
settle(resolve, reject, response);
// 清理请求对象,避免内存泄漏
request = null;
};
```
从代码中可以看到:当`XHR` 对象的`readyState` 变为`4` 时,表示请求已经完成,此时就可以获取响应头和响应数据了。然后,根据响应状态改变`Promise` 的状态。
### 3.3.4 监听错误事件
接下来,监听`XHR` 对象的错误事件,如下:
```javascript
request.onerror = function handleError() {
reject(createError('Network Error', config, null, request));
request = null;
};
```
### 3.3.5 监听超时事件
然后,监听`XHR` 对象的超时事件,如下:
```javascript
request.ontimeout = function handleTimeout() {
reject(
createError(
'timeout of ' + config.timeout + 'ms exceeded',
config,
'ECONNABORTED',
request
)
);
request = null;
};
```
### 3.3.6 取消请求
如果配置了取消请求,那么还需要监听取消事件,如下:
```javascript
if (config.cancelToken) {
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
request = null;
});
}
```
### 3.3.7 发送请求
最后,发送请求,如下:
```javascript
request.send(requestData);
```
## 3.4 完整代码
以上就是`XHR` 适配器的完整实现,由于代码较长,这里就不贴完整代码了,有兴趣的同学可以自行查看源码。
# 4. http 适配器
`http` 适配器的源码是在`lib/adapters/http.js` 文件中,由于该文件代码较多,我们就分部分来解读。
## 4.1 模块引入
首先,该模块引入了一些工具函数,如下:
```javascript
var utils = require('./../utils');
var settle = require('./../core/settle');
var buildFullPath = require('../core/buildFullPath');
var buildURL = require('./../helpers/buildURL');
var http = require('http');
var https = require('https');
var httpFollow = require('follow-redirects').http;
var httpsFollow = require('follow-redirects').https;
var URL = require('url').URL;
var zlib = require('zlib');
var VERSION = require('./../env/data').version;
var createError = require('../core/createError');
var enhanceError = require('../core/enhanceError');
var defaults = require('../defaults');
var Cancel = require('../cancel/Cancel');
```
- `utils`:工具函数;
- `settle`:根据响应状态改变`Promise` 的状态;
- `buildFullPath`:构建完整的请求`URL`;
- `buildURL`:构建带查询参数的`URL`;
- `http`:`node` 的`http` 模块;
- `https`:`node` 的`https` 模块;
- `httpFollow`:支持重定向的`http` 模块;
- `httpsFollow`:支持重定向的`https` 模块;
- `URL`:`node` 的`URL` 模块;
- `zlib`:`node` 的`zlib` 模块,用于解压缩;
- `VERSION`:`axios` 的版本号;
- `createError`:创建错误对象;
- `enhanceError`:增强错误对象;
- `defaults`:默认配置;
- `Cancel`:取消请求的类;
## 4.2 导出模块
接下来,该模块导出了一个函数,如下:
```javascript
module.exports = function httpAdapter(config) {
return new Promise(function dispatchHttpRequest(
resolvePromise,
rejectPromise
) {
// ...
});
};
```
从代码中可以看到:该模块导出了一个函数,该函数接收配置对象`config` 作为参数,并且返回一个`promise`。
## 4.3 函数内部实现
接下来,我们看看该函数内部是如何实现的。
### 4.3.1 初始化变量
首先,函数内部初始化了一些变量,如下:
```javascript
var resolve = function resolve(value) {
resolvePromise(value);
};
var reject = function reject(value) {
rejectPromise(value);
};
var data = config.data;
var headers = config.headers;
// 如果用户设置了User-Agent请求头,则使用用户设置的,否则使用默认的
if (!headers['User-Agent'] && !headers['user-agent']) {
headers['User-Agent'] = 'axios/' + VERSION;
}
```
### 4.3.2 解析 URL
接下来,解析`URL`,如下:
```javascript
var parsed = new URL(config.url);
var protocol = parsed.protocol || 'http:';
```
### 4.3.3 判断是否是 https
然后,判断是否是`https`,如下:
```javascript
var isHttps = protocol === 'https:';
```
### 4.3.4 选择 http 模块
接下来,选择`http` 模块,如下:
```javascript
var http = isHttps ? https : http;
```
### 4.3.5 处理重定向
然后,处理重定向,如下:
```javascript
if (config.maxRedirects) {
http = isHttps ? httpsFollow : httpFollow;
}
```
### 4.3.6 创建请求选项
接下来,创建请求选项,如下:
```javascript
var options = {
path: buildURL(parsed.pathname, config.params, config.paramsSerializer),
method: config.method.toUpperCase(),
headers: headers,
agent: config.agent,
auth: config.auth,
// ...
};
```
### 4.3.7 发送请求
然后,发送请求,如下:
```javascript
var req = http.request(options, function handleResponse(res) {
// ...
});
```
### 4.3.8 处理响应
接下来,处理响应,如下:
```javascript
var stream = res;
// 如果响应是gzip压缩的,则解压缩
if (res.headers['content-encoding'] === 'gzip') {
stream = res.pipe(zlib.createGunzip());
}
// 响应数据
var responseBuffer = [];
stream.on('data', function handleStreamData(chunk) {
responseBuffer.push(chunk);
});
stream.on('error', function handleStreamError(err) {
if (req.aborted) return;
reject(enhanceError(err, config, null, req));
});
stream.on('end', function handleStreamEnd() {
var responseData = Buffer.concat(responseBuffer);
if (config.responseType !== 'arraybuffer') {
responseData = responseData.toString(config.responseEncoding);
if (!config.responseEncoding || config.responseEncoding === 'utf8') {
responseData = utils.stripBOM(responseData);
}
}
var response = {
data: responseData,
status: res.statusCode,
statusText: res.statusMessage,
headers: res.headers,
config: config,
request: req,
};
settle(resolve, reject, response);
});
```
### 4.3.9 处理错误
然后,处理错误,如下:
```javascript
req.on('error', function handleRequestError(err) {
if (req.aborted && err.code !== 'ERR_CANCELED') {
return;
}
reject(enhanceError(err, config, null, req));
});
```
### 4.3.10 处理超时
接下来,处理超时,如下:
```javascript
if (config.timeout) {
req.setTimeout(config.timeout, function handleTimeout() {
req.abort();
reject(
createError(
'timeout of ' + config.timeout + 'ms exceeded',
config,
'ECONNABORTED',
req
)
);
});
}
```
### 4.3.11 取消请求
如果配置了取消请求,那么还需要监听取消事件,如下:
```javascript
if (config.cancelToken) {
config.cancelToken.promise.then(function onCanceled(cancel) {
if (req.aborted) {
return;
}
req.abort();
reject(cancel);
});
}
```
### 4.3.12 发送数据
最后,发送数据,如下:
```javascript
req.end(data);
```
## 4.4 完整代码
以上就是`http` 适配器的完整实现,由于代码较长,这里就不贴完整代码了,有兴趣的同学可以自行查看源码。
# 5. 总结
本篇文章我们分析了`axios` 的请求适配器,包括`XHR` 适配器和`http` 适配器。通过分析,我们知道了`axios` 是如何根据当前环境来选择适配器的,以及`XHR` 适配器和`http` 适配器的内部实现。
在下一篇文章中,我们将分析`axios` 的取消请求功能,看看`axios` 是如何实现取消请求的。
最新文章
- 动力电池突破与激光雷达革新:车联网引领智能出行新时代
- 车辆保养指南:胎压、机油与刹车片关键要点
- 2023年四大汽车科技趋势:智能驾驶与固态电池领跑
- Axios拦截器原理:链式调用与请求响应流程解析
- 网联自动驾驶技术革新未来出行
- 汽车溃缩区设计:高强度钢与复合材料的安全防护之道
- 车险指南:交强险、车损险与三者险全面解析
- 宝马X5越野性能卓越适合各种路况
- 机油滤清器保养汽车引擎关键
- 自动驾驶与车联网:未来出行的三大技术革命
- 新能源与智能网联技术引领未来出行革命
- 电动汽车电池寿命与续航:关键影响因素与保养指南
- 2023智能驾驶与固态电池技术突破:车联网引领汽车产业变革
- 智能座舱与固态电池:未来汽车的三大技术变革
- 智能汽车传感器网络:毫米波雷达与激光雷达如何重塑自动驾驶
- 计算机控制汽车智能驾驶技术
- 交强险定损全流程解析:从报案到理赔的完整指南
- 新能源汽车续航里程提升技术突破
- 电动汽车能否取代燃油车?三大技术突破解析未来趋势
- 新能源汽车的“隐形大脑”:BMS如何守护电池安全?
