上篇 Axios 源码解析(二):通用工具方法 解析了通用工具方法部分的源码。下面继续解析适配器部分的代码,也就是 /adapters
目录。
在github.com/MageeLin/ax… 中的analysis
分支可以看到当前已解析完的文件。
/adapters
在《Axios 源码解析(一):模块分解》中已经分析过,/adapters
目录中包含如下这些文件:
├─adapters
│ http.js
│ README.md
│ xhr.js
同样在 README.md
中,介绍了该目录的作用:
adapters/
下的模块负责发送请求并在收到响应后处理返回的 Promise
。也就是整个 Axios
中负责对外的部分。
var settle = require('./../core/settle');
module.exports = function myAdapter(config) {
// 在此时:
// - 配置已与默认配置合并
// - 请求转换器已经执行
// - 请求拦截器已经执行
/*
* ------
*/
// 使用提供的配置发出请求
// 根据响应来 settle Promise
return new Promise(function (resolve, reject) {
var response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config: config,
request: request,
};
settle(resolve, reject, response);
/*
* ------
*/
// 从这里开始:
// - 响应转换器开始执行
// - 响应拦截器开始执行
});
};
由于 Axios
的使用环境其实就是两种
,一种是在浏览器端发送 XHR
请求,另一种是在 nodejs
中发送 http
请求。所以 Axios
的适配器只有两个:http.js
和 xhr.js
。
xhr.js
在浏览器环境中,Axios
直接封装的 XMLHttpRequest
,流程大致如下所示:
- 新建一个
XHR
对象 - 解析
URL
- 处理
data
、headers
和responseType
- 设置超时时间
- 打开请求
- 添加
onloadend
、onloadend
、onabort
、onerror
、ontimeout
、onUploadProgress
事件 - 发送请求
xhr
适配器的具体代码如下:
'use strict';
var utils = require('./../utils');
var settle = require('./../core/settle');
var cookies = require('./../helpers/cookies');
var buildURL = require('./../helpers/buildURL');
var buildFullPath = require('../core/buildFullPath');
var parseHeaders = require('./../helpers/parseHeaders');
var isURLSameOrigin = require('./../helpers/isURLSameOrigin');
var createError = require('../core/createError');
/**
* @description: 浏览器环境中使用XHR对象来发送请求
* @param {Object} config 已经合并并且标准化后的配置对象
* @return {Promise} 返回一个promise对象
*/
module.exports = function xhrAdapter(config) {
// 标准的新建Promise对象的写法
return new Promise(function dispatchXhrRequest(resolve, reject) {
// 拿到data,headers,和responseType
var requestData = config.data;
var requestHeaders = config.headers;
var responseType = config.responseType;
if (utils.isFormData(requestData)) {
delete requestHeaders['Content-Type']; // 删掉content-type,让浏览器来设置
}
// 新建一个XHR对象
var request = new XMLHttpRequest();
// HTTP basic 认证
if (config.auth) {
var username = config.auth.username || '';
var password = config.auth.password
? unescape(encodeURIComponent(config.auth.password))
: '';
// 编码base64字符串,构造出一个Authorization
requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
}
// 构造全路径
var fullPath = buildFullPath(config.baseURL, config.url);
// 打开请求
request.open(
config.method.toUpperCase(),
buildURL(fullPath, config.params, config.paramsSerializer),
true
);
// 设置毫秒级的超时时间限制
request.timeout = config.timeout;
/**
* @description: 设置loadend的回调
*/
function onloadend() {
if (!request) {
return;
}
// 响应头处理
var responseHeaders =
'getAllResponseHeaders' in request
? parseHeaders(request.getAllResponseHeaders())
: null;
// 响应内容处理
var responseData =
!responseType || responseType === 'text' || responseType === 'json'
? request.responseText
: request.response;
// 构造出response
var response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config: config,
request: request,
};
// 调用settle方法来处理promise
settle(resolve, reject, response);
// 清空request
request = null;
}
// 如果request上有onloadend属性,则直接替换
if ('onloadend' in request) {
request.onloadend = onloadend;
} else {
// 否则就用onreadystatechange来模拟onloadend
request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) {
return;
}
// 请求出错,我们没有得到响应,这将由 onerror 处理。
// 但只有一个例外:请求使用 file:协议,此时即使它是一个成功的请求,大多数浏览器也将返回状态为 0,
if (
request.status === 0 &&
!(request.responseURL && request.responseURL.indexOf('file:') === 0)
) {
return;
}
// readystate 处理器在 onerror 或 ontimeout处理器之前调用, 因此我们应该在next 'tick' 上调用onloadend
setTimeout(onloadend);
};
}
// 处理浏览器对request的取消(与手动取消不同)
request.onabort = function handleAbort() {
if (!request) {
return;
}
reject(createError('Request aborted', config, 'ECONNABORTED', request));
// 清空request
request = null;
};
// 处理更低级别的网络错误
request.onerror = function handleError() {
// 真正的错误被浏览器掩盖了
// onerror应当只可被网络错误触发
reject(createError('Network Error', config, null, request));
// 清空request
request = null;
};
// 处理超时
request.ontimeout = function handleTimeout() {
var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded';
if (config.timeoutErrorMessage) {
timeoutErrorMessage = config.timeoutErrorMessage;
}
reject(
createError(
timeoutErrorMessage,
config,
config.transitional && config.transitional.clarifyTimeoutError
? 'ETIMEDOUT'
: 'ECONNABORTED',
request
)
);
// 清空request
request = null;
};
// 添加 xsrf 头
// 只能在浏览器环境中生效
// 在工作者线程或者RN中不生效
if (utils.isStandardBrowserEnv()) {
// 添加 xsrf 头
var xsrfValue =
(config.withCredentials || isURLSameOrigin(fullPath)) &&
config.xsrfCookieName
? cookies.read(config.xsrfCookieName)
: undefined;
if (xsrfValue) {
requestHeaders[config.xsrfHeaderName] = xsrfValue;
}
}
// 给request添加headers
if ('setRequestHeader' in request) {
utils.forEach(requestHeaders, function setRequestHeader(val, key) {
if (
typeof requestData === 'undefined' &&
key.toLowerCase() === 'content-type'
) {
// 如果data是undefined,则移除Content-Type
delete requestHeaders[key];
} else {
// 否则把header添加给request
request.setRequestHeader(key, val);
}
});
}
// 添加withCredentials
if (!utils.isUndefined(config.withCredentials)) {
request.withCredentials = !!config.withCredentials;
}
// 添加 responseType
if (responseType && responseType !== 'json') {
request.responseType = config.responseType;
}
// 处理progess
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
request = null;
});
}
if (!requestData) {
requestData = null;
}
// 发送请求
request.send(requestData);
});
};
http.js
在 nodejs
环境中,Axios
封装的是 http
库,流程大致如下所示:
- 转换数据格式
- 处理代理
- 解析
URL
- 创建请求
- 添加
error
、end
、data
等事件 - 发送请求
http
适配器的具体代码如下:
'use strict';
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');
var zlib = require('zlib');
var pkg = require('./../../package.json');
var createError = require('../core/createError');
var enhanceError = require('../core/enhanceError');
var isHttps = /https:?/;
/**
* @description 设置代理用的方法
* @param {http.ClientRequestArgs} options
* @param {AxiosProxyConfig} proxy
* @param {string} location
*/
function setProxy(options, proxy, location) {
options.hostname = proxy.host;
options.host = proxy.host;
options.port = proxy.port;
options.path = location;
// basic形式的Proxy-Authorization头
if (proxy.auth) {
var base64 = Buffer.from(
proxy.auth.username + ':' + proxy.auth.password,
'utf8'
).toString('base64');
options.headers['Proxy-Authorization'] = 'Basic ' + base64;
}
// 如果使用了代理,那么重定向时必须要经过代理
options.beforeRedirect = function beforeRedirect(redirection) {
redirection.headers.host = redirection.host;
setProxy(redirection, proxy, redirection.href);
};
}
/*eslint consistent-return:0*/
module.exports = function httpAdapter(config) {
return new Promise(function dispatchHttpRequest(
resolvePromise,
rejectPromise
) {
var resolve = function resolve(value) {
resolvePromise(value);
};
var reject = function reject(value) {
rejectPromise(value);
};
var data = config.data;
var headers = config.headers;
// 设置 User-Agent (某些服务端强制要求)
// 详见 https://github.com/axios/axios/issues/69
if ('User-Agent' in headers || 'user-agent' in headers) {
// 当不需要UA头时
if (!headers['User-Agent'] && !headers['user-agent']) {
delete headers['User-Agent'];
delete headers['user-agent'];
}
// 当需要UA头时就指定一个
} else {
// 只有在config中没有指定UA时才设置
headers['User-Agent'] = 'axios/' + pkg.version;
}
// 转换数据格式
if (data && !utils.isStream(data)) {
if (Buffer.isBuffer(data)) {
// 什么都不做...
} else if (utils.isArrayBuffer(data)) {
data = Buffer.from(new Uint8Array(data));
} else if (utils.isString(data)) {
data = Buffer.from(data, 'utf-8');
} else {
return reject(
createError(
'Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream',
config
)
);
}
// 如果data存在,则需要设置Content-Length
headers['Content-Length'] = data.length;
}
// HTTP basic authentication
var auth = undefined;
if (config.auth) {
var username = config.auth.username || '';
var password = config.auth.password || '';
auth = username + ':' + password;
}
// 解析 url
var fullPath = buildFullPath(config.baseURL, config.url);
var parsed = url.parse(fullPath);
var protocol = parsed.protocol || 'http:';
if (!auth && parsed.auth) {
var urlAuth = parsed.auth.split(':');
var urlUsername = urlAuth[0] || '';
var urlPassword = urlAuth[1] || '';
auth = urlUsername + ':' + urlPassword;
}
if (auth) {
delete headers.Authorization;
}
var isHttpsRequest = isHttps.test(protocol);
var agent = isHttpsRequest ? config.httpsAgent : config.httpAgent;
var options = {
path: buildURL(
parsed.path,
config.params,
config.paramsSerializer
).replace(/^\?/, ''),
method: config.method.toUpperCase(),
headers: headers,
agent: agent,
agents: { http: config.httpAgent, https: config.httpsAgent },
auth: auth,
};
if (config.socketPath) {
options.socketPath = config.socketPath;
} else {
options.hostname = parsed.hostname;
options.port = parsed.port;
}
var proxy = config.proxy;
if (!proxy && proxy !== false) {
var proxyEnv = protocol.slice(0, -1) + '_proxy';
var proxyUrl =
process.env[proxyEnv] || process.env[proxyEnv.toUpperCase()];
if (proxyUrl) {
var parsedProxyUrl = url.parse(proxyUrl);
var noProxyEnv = process.env.no_proxy || process.env.NO_PROXY;
var shouldProxy = true;
if (noProxyEnv) {
var noProxy = noProxyEnv.split(',').map(function trim(s) {
return s.trim();
});
shouldProxy = !noProxy.some(function proxyMatch(proxyElement) {
if (!proxyElement) {
return false;
}
if (proxyElement === '*') {
return true;
}
if (
proxyElement[0] === '.' &&
parsed.hostname.substr(
parsed.hostname.length - proxyElement.length
) === proxyElement
) {
return true;
}
return parsed.hostname === proxyElement;
});
}
if (shouldProxy) {
proxy = {
host: parsedProxyUrl.hostname,
port: parsedProxyUrl.port,
protocol: parsedProxyUrl.protocol,
};
if (parsedProxyUrl.auth) {
var proxyUrlAuth = parsedProxyUrl.auth.split(':');
proxy.auth = {
username: proxyUrlAuth[0],
password: proxyUrlAuth[1],
};
}
}
}
}
// 如果代理存在时要进行专门处理
if (proxy) {
options.headers.host =
parsed.hostname + (parsed.port ? ':' + parsed.port : '');
setProxy(
options,
proxy,
protocol +
'//' +
parsed.hostname +
(parsed.port ? ':' + parsed.port : '') +
options.path
);
}
var transport;
var isHttpsProxy =
isHttpsRequest && (proxy ? isHttps.test(proxy.protocol) : true);
if (config.transport) {
transport = config.transport;
} else if (config.maxRedirects === 0) {
transport = isHttpsProxy ? https : http;
} else {
if (config.maxRedirects) {
options.maxRedirects = config.maxRedirects;
}
transport = isHttpsProxy ? httpsFollow : httpFollow;
}
if (config.maxBodyLength > -1) {
options.maxBodyLength = config.maxBodyLength;
}
// 创建 request
var req = transport.request(options, function handleResponse(res) {
if (req.aborted) return;
// 在需要的情况下,自动解压响应体
var stream = res;
// 如果重定向,则返回最后一次请求的信息
var lastRequest = res.req || req;
// 如果没有内容, HEAD请求禁止解压
if (
res.statusCode !== 204 &&
lastRequest.method !== 'HEAD' &&
config.decompress !== false
) {
switch (res.headers['content-encoding']) {
/*eslint default-case:0*/
case 'gzip':
case 'compress':
case 'deflate':
// 给处理流程中添加未压缩的body stream
stream = stream.pipe(zlib.createUnzip());
// 移除content-encoding, 目的是避免拒绝下载操作
delete res.headers['content-encoding'];
break;
}
}
var response = {
status: res.statusCode,
statusText: res.statusMessage,
headers: res.headers,
config: config,
request: lastRequest,
};
if (config.responseType === 'stream') {
response.data = stream;
settle(resolve, reject, response);
} else {
var responseBuffer = [];
var totalResponseBytes = 0;
stream.on('data', function handleStreamData(chunk) {
responseBuffer.push(chunk);
totalResponseBytes += chunk.length;
// 确保内容长度不超过指定的最大长度
if (
config.maxContentLength > -1 &&
totalResponseBytes > config.maxContentLength
) {
stream.destroy();
reject(
createError(
'maxContentLength size of ' +
config.maxContentLength +
' exceeded',
config,
null,
lastRequest
)
);
}
});
stream.on('error', function handleStreamError(err) {
if (req.aborted) return;
reject(enhanceError(err, config, null, lastRequest));
});
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);
}
}
response.data = responseData;
settle(resolve, reject, response);
});
}
});
// 处理错误
req.on('error', function handleRequestError(err) {
if (req.aborted && err.code !== 'ERR_FR_TOO_MANY_REDIRECTS') return;
reject(enhanceError(err, config, null, req));
});
// 处理请求超时
if (config.timeout) {
// 如果`req`接口无法处理其他类型,将强制用一个整数的超时时间。
var timeout = parseInt(config.timeout, 10);
if (isNaN(timeout)) {
reject(
createError(
'error trying to parse `config.timeout` to int',
config,
'ERR_PARSE_TIMEOUT',
req
)
);
return;
}
// 有时响应将非常缓慢,甚至没有响应,连接事件将被事件循环系统打断
// 此时触发定时器回调,在连接前将调用abort(),然后获取"socket hang up" 和ECONNRESET码
// 此时,如果出现大量的请求,nodejs会在幕后挂起一些socket。并且数目会不断增长。
// 然后这些挂起的socket将一点点占用 CPU。
// ClientRequest.setTimeout 将在指定毫秒内启动,并且可以确保连接之后触发abort()。
req.setTimeout(timeout, function handleRequestTimeout() {
req.abort();
reject(
createError(
'timeout of ' + timeout + 'ms exceeded',
config,
config.transitional && config.transitional.clarifyTimeoutError
? 'ETIMEDOUT'
: 'ECONNABORTED',
req
)
);
});
}
if (config.cancelToken) {
// 处理取消操作
config.cancelToken.promise.then(function onCanceled(cancel) {
if (req.aborted) return;
req.abort();
reject(cancel);
});
}
// 发送请求
if (utils.isStream(data)) {
data
.on('error', function handleStreamError(err) {
reject(enhanceError(err, config, null, req));
})
.pipe(req);
} else {
req.end(data);
}
});
};
总结
在工程中,把相似但又不同、且可替代的部分抽取出来,形成一个专用的模块,对外的接口统一,这是相当优秀的设计模式。
下一篇 Axios 源码解析(四):核心工具方法(1)
来解析 Axios
中耦合度较高的工具方法。