本文是"axios源码系列"第五篇,你可以查看以下链接了解过去的内容。
本文我们将讨论 axios 在浏览器端是如何实现的。
通过之前的学习,我们已经了解到 axios 内部是通过适配器模式,同时支持在浏览器端和 Node.js 端使用。
- 在浏览器环境是通过封装 XMLHttpRequest API 实现请求的,源代码位于 lib/adapters/xhr.js
- 在 Node.js 环境则是通过封装 http/https 模块实现请求的,源代码位于 lib/adapters/http.js
本文我们只讲解浏览器端实现 xhr.js。
总体结构
打开 xhr.js,大概浏览一下文件内容,就能梳理大概的实现逻辑。
js
// /v1.6.8/lib/adapters/xhr.js#L48
export default isXHRAdapterSupported && function (config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
// ...
}
}
首先,能观察到的是:导出时会先判断当前是否是浏览器环境(isXHRAdapterSupported)。如果不是(即位于 Node.js 环境),那么 xhr.js 最终会解析结果是。
js
export default false
这样在打包 Node.js 环境包时,其后的浏览器端实现就成功 Tree Shaking 掉了。
其次,能发现我们的请求结果最终是以 Promise 对象形式返回,内部我们通过 resolve(response) 返回请求结果,使用 reject(err) 抛出错误。这也能让我们使用 .then() .actch 或 await 这些特定于 Promise API 的操作。
再来看一下,内部实现的情况。
先会看到创建一个 XMLHttRequest 实例对象
js
// /v1.6.8/lib/adapters/xhr.js#L76
let request = new XMLHttpRequest();
接下来,利用传入的 config 参数发起请求。
js
// /v1.6.8/lib/adapters/xhr.js#L87
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
// Set the request timeout in MS
request.timeout = config.timeout;
// Send the request
request.send(requestData || null);
最后,就是监听 request 上的各种事件处理请求了。这块内容属于核心内容,我们仔细来讲。
处理请求
在处理请求的过程当中,我们会处理包括 abort、timeout、error、readystatechange/onloaded 在内的事件,同时还有下载和上传的进度事件 progress(分别在 request 和 request.update 上注册)。
针对进度事件,为了避免行文冗长,在本文我们不会讲解。有兴趣的读者可以自行阅读源码,实现比较简短,不难理解。
abort 事件
js
// /v1.6.8/lib/adapters/xhr.js#L146-L156
// Handle browser request cancellation (as opposed to a manual cancellation)
request.onabort = function handleAbort() {
if (!request) {
return;
}
reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, request));
// Clean up request
request = null;
};
当调用 request.abort() 方法时,就会触发 abort 事件。在 axios 中,当我们使用 AbortController API 作为 signal 参数代入,并调用 signal.abort() 时,内部其实就在调用 request.abort(),实现取消请求的逻辑。
在 onabort 方法内部,实际上就是调用 reject(AxiosError) 返回请求被取消的结果。
注意,在开始会有一个 if (!request)
的保护判断,在 axios 内部,被处理后的请求会被手动置空(request = null
),这样能够一些意料之外的 BUG 出现(其他事件同理,不再赘述)。
timeout 事件
js
// /v1.6.8/lib/adapters/xhr.js#L168-L183
// Handle timeout
request.ontimeout = function handleTimeout() {
let timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';
if (config.timeoutErrorMessage) {
timeoutErrorMessage = config.timeoutErrorMessage;
}
reject(new AxiosError(
timeoutErrorMessage,
AxiosError.ECONNABORTED,
config,
request));
// Clean up request
request = null;
};
timeout 事件处理更加简单,在根据配置确定好错误信息 后,直接 reject(new AxiosError()) 返回请求请求超时的结果。
还记得之前 request.timeout 的设置吗?
js
// Set the request timeout in MS
request.timeout = config.timeout;
config.timeout 默认值是 0,也是没有超时限制。
js
// /v1.6.8/lib/defaults/index.js#L123-L127
/**
* A timeout in milliseconds to abort a request. If set to 0 (default) a
* timeout is not created.
*/
timeout: 0,
error 事件
js
// /blob/v1.6.8/lib/adapters/xhr.js#L158-L166
// Handle low level network errors
request.onerror = function handleError() {
// Real errors are hidden from us by the browser
// onerror should only fire if it's a network error
reject(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request));
// Clean up request
request = null;
};
error 事件处理逻辑比 timeout 还简单,只不过错误信息变成了"Network Error"。
abort、timeout 和 error 这三个事件都有一个共同特点,即请求发出后还没收到响应,请求就意外中断了。
- abort 事件是手动取消请求造成的
- timeout 则是在超过前端设置最长等待时间后,自动触发的
- error 则是由于网络原因,自动触发的。这里的网络原因包括网络意外中断、请求了一个无效地址或是跨域错误(CORS)等情况
至此,我们已经将 request 实例上所有没有响应返回的错误都监听了。接下来,就是处理请求完成的场景(即有响应返回,不管是 2xx 状态码,还是 4xx、5xx 状态码)。
请求完成事件
js
// /v1.6.8/lib/adapters/xhr.js#L123-L144
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
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);
};
}
看起来很长(而且还没贴出 onloaded 实现),但其实可以简化成:
js
request.onload = onloaded
之所以写成现在这样,是为了向前兼容。
最早 axios 只用 request.onreadystatechange 实现请求完成处理。
js
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
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);
};
实现中:
- 首先,保证请求是完成状态(request.readyState === 4,请求有响应或者无响应)
- 其次,排除已被处理了的错误事件 abort、timeout 和 error。这 3 个事件都是请求无响应的,最终 request.status 值为 0
- 最后,将 onloadend 放入 setTimeout 中,因为 onreadystatechange 事件是在之前触发的,为了保证 onloaded 函数执行是在错误事件处理之后,就用了 setTimeout 放在下一次事件队列中
当然,后面再有 onloaded 标准事件支持之后,就可以替换了。
js
if ('onloadend' in request) {
// Use onloadend if available
request.onloadend = onloadend;
}
相当于说,onloadend 是 onreadystatechange 的平替。其实这里从语义上讲,直接适用 onload 事件即可,因为我们已经处理完所有的异常场景了。
js
// better
request.onload = onloaded
这些说完之后,就可以看看 onloaded 的函数内容了。
js
// /v1.6.8/lib/adapters/xhr.js#L92-L121
function onloadend() {
if (!request) {
return;
}
// Prepare the response
const responseHeaders = AxiosHeaders.from(
'getAllResponseHeaders' in request && request.getAllResponseHeaders()
);
const responseData = !responseType || responseType === 'text' || responseType === 'json' ?
request.responseText : request.response;
// 1)
const response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config,
request
};
// 2)
settle(function _resolve(value) {
resolve(value);
done();
}, function _reject(err) {
reject(err);
done();
}, response);
// Clean up request
request = null;
}
虽然看起来很长,但 onloadend 要做的事情就 2 件。
- 组装 reponse 对象。这是 axios 自身自定义的返回对象。比如包含请求响应数据(rresponse.data),还包含像状态码、响应头、请求配置和请求实例(XMLHttpRequest 实例)信息
- 判断 response 信息,决定请求成功或是失败。这一步是基于 settle() 工具函数
settle() 工具函数实现如下:
js
// /v1.6.8/lib/core/settle.js#L14-L27
export default function settle(resolve, reject, response) {
const validateStatus = response.config.validateStatus;
if (!response.status || !validateStatus || validateStatus(response.status)) {
resolve(response);
} else {
reject(new AxiosError(
'Request failed with status code ' + response.status,
[AxiosError.ERR_BAD_REQUEST, AxiosError.ERR_BAD_RESPONSE][Math.floor(response.status / 100) - 4],
response.config,
response.request,
response
));
}
}
settle 核心逻辑就是判断 response.status 参数值,默认 2xx 会认为是请求成功,否则就是请求失败。
这里借助了 config.validateStatus 工具函数,它的实现是:
js
// /v1.6.8/lib/defaults/index.js#L140-L142
validateStatus: function validateStatus(status) {
return status >= 200 && status < 300;
},
当然,你也可以在发起 axios 请求时自定义。比如:
js
axios.get('/user/12345', {
validateStatus: function (status) {
return status < 500; // Resolve only if the status code is less than 500
}
})
当然,针对失败请求,axios 还做了错误信息的区分。
js
reject(new AxiosError(
'Request failed with status code ' + response.status,
[AxiosError.ERR_BAD_REQUEST, AxiosError.ERR_BAD_RESPONSE][Math.floor(response.status / 100) - 4],
response.config,
response.request,
response
));
[AxiosError.ERR_BAD_REQUEST, AxiosError.ERR_BAD_RESPONSE][Math.floor(response.status / 100) - 4]
表示 4xx 属于 ERR_BAD_REQUEST 信息,5xx 错误属于 ERR_BAD_RESPONSE 信息。
回到 onloaded 中调用 settle 的地方。
js
// /v1.6.8/lib/adapters/xhr.js#L111-L117
settle(function _resolve(value) {
resolve(value);
done();
}, function _reject(err) {
reject(err);
done();
}, response);
会看到这里还有一个 done 的收尾工作。它的作用就是清理之前传入 config.signal 时注册的事件。
js
// /v1.6.8/lib/adapters/xhr.js#L54-L62
function done() {
if (config.signal) {
config.signal.removeEventListener('abort', onCanceled);
}
}
至此,我们就讲完了 axios 的浏览器端所有实现的代码。
总结
本文我们讲解了 axios 在浏览器端的实现。axios 内部是通过适配器模式,同时支持在浏览器端和 Node.js 端使用。其中,浏览端是使用 XMLHttpRequest API 封装实现的。
axios 内部首先使用 new XMLHttpRequest() 创建了一个实例 request,继而先后注册了 abort、timeout、error 和 onloaded 事件处理函数进行处理。
- abort、timeout、error 针对的是请求无响应的报错场景。
- onloaded 则是用来处理请求有响应的处理(如果不考虑向前兼容,可以使用 onload 替代)
在请求完成处理函数 onloadend() 内部,会根据 response.status + config.validateStatus() 方式判断请求成功还是失败。
希望本文的内容对大家理解 axios 在浏览器端的表现有更深的理解。感谢你的阅读,再见。