前言
用过axios
或者了解过axios
源码的同学们,或多或少都知道,axios
在浏览器中的请求,底层使用的是XMLHttpRequest
发起。
那我如果想要使用fetch
,那么不好意思,并没有哦!
既然如此,何不自己开动双手,给axios
写一个fetch
适配器。
2 axios的dispatchRequest
要写fetch
适配器,肯定要先知道axios
是在哪里使用的适配器,是如何使用的。
在axios
源码的dispatchRequest.js
文件中,有如下代码段:
js
export default function dispatchRequest (config) {
// .....do something
// 获取适配器,如果是浏览器环境获取xhr,如果是Node环境,获取http
const adapter = adapters.getAdapter(config.adapter || defaults.adapter);
// 这里的adapter就是我们要重写的方法
return adapter(config).then(function onAdapterResolution (response) {
// ...... do success
}, function onAdapterRejection (reason) {
// ...... do error
});
}
可以看出来,adapter
是一个函数,参数是config
,也就是我们调用axios
请求时传入的。
另外adapter
返回值是一个promise
。
3 axios的xhr适配器
既然要写fetch
适配器,那么少不了要参考xhr
适配器是怎么写的,那么让我们先看一下代码:
js
// 判断是否支持XMLHttpRequest
const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined';
// 适配器的请求参数是config
export default isXHRAdapterSupported && function (config) {
// 返回Promise
return new Promise(function dispatchXhrRequest (resolve, reject) {
// 请求体
let requestData = config.data;
// 请求头
const requestHeaders = AxiosHeaders.from(config.headers).normalize();
// ....省略
// xhr请求
let request = new XMLHttpRequest();
// 开启请求
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
// 监听请求完成
request.onreadystatechange = function handleLoad () {
//....省略
};
// 处理浏览器请求取消事件
request.onabort = function handleAbort () {
//....省略
};
// 处理低级的网络错误
request.onerror = function handleError () {
//....省略
};
// 处理超时
request.ontimeout = function handleTimeout () {
//....省略
};
// 增加下载过程的监听函数
if (typeof config.onDownloadProgress === 'function') {
request.addEventListener('progress', progressEventReducer(config.onDownloadProgress, true));
}
// 增加上传过程的监听函数
if (typeof config.onUploadProgress === 'function' && request.upload) {
request.upload.addEventListener('progress', progressEventReducer(config.onUploadProgress));
}
// 请求过程中取消的监听事件
if (config.cancelToken || config.signal) {
onCanceled = cancel => {
if (!request) {
return;
}
reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
request.abort();
request = null;
};
config.cancelToken && config.cancelToken.subscribe(onCanceled);
if (config.signal) {
config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
}
}
// 发送请求
request.send(requestData || null);
});
}
可以看出来,xhr
适配器主要做了5件事:
- 将
config
传入的信息转为request
属性 - 对请求取消事件的监听
- 对请求超时时间的监听
- 监听上传和下载进度事件
- 发送请求
由于fetch
不能监听上传进度,所以我们就先监听实现下载进度就行了,其他的就按照这个思路来写。
将config
传入request
的操作,很多代码都可以复用。
4 手写fetch适配器
接下来看看我写的fetch
适配器吧。
在lib->adapters
下新增fetch.js
文件,内容如下:
想尝试的小伙伴可以直接复制粘贴使用。
js
'use strict';
import utils from '../utils.js';
import settle from '../core/settle.js';
import cookies from '../helpers/cookies.js';
import buildURL from '../helpers/buildURL.js';
import buildFullPath from '../core/buildFullPath.js';
import isURLSameOrigin from '../helpers/isURLSameOrigin.js';
import transitionalDefaults from '../defaults/transitional.js';
import AxiosError from '../core/AxiosError.js';
import CanceledError from '../cancel/CanceledError.js';
import parseProtocol from '../helpers/parseProtocol.js';
import platform from '../platform/index.js';
import AxiosHeaders from '../core/AxiosHeaders.js';
const isFetchAdapterSupported = typeof fetch !== 'undefined';
// 适配器的请求参数是config
export default isFetchAdapterSupported &&
function (config) {
// 返回Promise
return new Promise(function dispatchFetchRequest(resolve, reject) {
// 这行代码非常重要,代表进入我们的适配器了O(∩_∩)O哈哈~
console.log('进入了fetch适配器。。。');
// 请求体
let requestData = config.data;
// 请求头
const requestHeaders = AxiosHeaders.from(config.headers).normalize();
// 返回数据类型
const responseType = config.responseType;
// 接下来先把config转为fetch的请求参数,
// 大多都是从xhr复用
// 自动帮我们设置contentType,
// 这就是为什么我们使用的时候都不需要
// 特别设置contentType的原因了
if (utils.isFormData(requestData)) {
if (
platform.isStandardBrowserEnv ||
platform.isStandardBrowserWebWorkerEnv
) {
// 浏览器环境让浏览器设置
requestHeaders.setContentType(false);
} else {
requestHeaders.setContentType('multipart/form-data;', false);
}
}
// 设置auth,帮我们转码好了
if (config.auth) {
const username = config.auth.username || '';
const password = config.auth.password
? unescape(encodeURIComponent(config.auth.password))
: '';
requestHeaders.set(
'Authorization',
'Basic ' + btoa(username + ':' + password)
);
}
// 拼接完整URL路径
const fullPath = buildFullPath(config.baseURL, config.url);
// Add xsrf header
if (platform.isStandardBrowserEnv) {
const xsrfValue =
(config.withCredentials || isURLSameOrigin(fullPath)) &&
config.xsrfCookieName &&
cookies.read(config.xsrfCookieName);
if (xsrfValue) {
requestHeaders.set(config.xsrfHeaderName, xsrfValue);
}
}
requestData === undefined && requestHeaders.setContentType(null);
const headers = new Headers();
// 构造headers
utils.forEach(
requestHeaders.toJSON(),
function setRequestHeader(val, key) {
headers.append(key, val);
}
);
// 移除取消事件订阅
function done() {
if (config.cancelToken) {
config.cancelToken.unsubscribe(onCanceled);
}
}
// 获取请求协议,比如https这样的
const protocol = parseProtocol(fullPath);
// 判断当前环境是否支持该协议
if (protocol && platform.protocols.indexOf(protocol) === -1) {
reject(
new AxiosError(
'Unsupported protocol ' + protocol + ':',
AxiosError.ERR_BAD_REQUEST,
config
)
);
return;
}
// 这里已经初步构造出了请求参数,接下来的才是重点
const requestParams = {
method: config.method.toUpperCase(),
headers,
body: requestData,
mode: 'cors',
credentials: config.withCredentials ? 'include' : 'omit'
// signal,responseType在下面补充
};
let onCanceled;
// 如果用户传入了signal,那么直接用用户传入的
// 如果用户传入了cancelToken,则构造一个signal,
// 以便fetch适配器可以兼容cancelToken
let signal;
if (!utils.isUndefined(config.signal)) {
// 直接使用signal
signal = config.signal;
} else if (!utils.isUndefined(config.cancelToken)) {
// 自己构造一个signal
const controller = new AbortController();
signal = controller.signal;
// 取消回调函数,内部也是调用
// controller.abort()
onCanceled = (cancel) => {
reject(
!cancel || cancel.type
? new CanceledError(null, config, requestParams)
: cancel
);
controller.abort();
};
// cancelToken订阅回调
config.cancelToken.subscribe(onCanceled);
}
// 设置signal
if (!utils.isUndefined(signal)) {
requestParams.signal = signal;
}
// 设置responseType
if (responseType && responseType !== 'json') {
requestParams.responseType = config.responseType;
}
const url = buildURL(fullPath, config.params, config.paramsSerializer);
// 由于fetch没有现成的timeout属性
// 此处是开启另一个超时Promise进行竞赛
// 来实现超时控制
Promise.race([
fetch(url, requestParams),
new Promise((_, innerReject) => {
setTimeout(
() => {
let timeoutErrorMessage = config.timeout
? 'timeout of ' + config.timeout + 'ms exceeded'
: 'timeout exceeded';
const transitional = config.transitional || transitionalDefaults;
if (config.timeoutErrorMessage) {
timeoutErrorMessage = config.timeoutErrorMessage;
}
innerReject(
new AxiosError(
timeoutErrorMessage,
transitional.clarifyTimeoutError
? AxiosError.ETIMEDOUT
: AxiosError.ECONNABORTED,
config,
requestParams
)
);
// 如果timeout是设置为0,说明永不超时
// 就不需要用Pormise.race,此处为了方便,我将
// 超时时间设置为99999999,当做永不超时
},
config.timeout > 0 ? config.timeout : 99999999
);
}),
])
.then(async (res) => {
//判断返回类型
let responseData = '';
// 如果配置了监听下载进度回调,自己来读取数据
// 至于监听上传进度,目前fetch好像还做不到
if (typeof config.onDownloadProgress === 'function') {
const total = +res.headers.get('content-length');
const reader = res.body.getReader();
let loaded = 0;
// 此处仅写了返回文本的情况
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) {
break;
}
// 每一次读取都累加起来
loaded += value.length;
// 每一次读取都对数据解码并拼接起来
responseData += decoder.decode(value);
// 执行回调,此处简单写了,只回传loaded和total,基本也够用了
config.onDownloadProgress({
loaded,
total,
});
}
// 没配置的情况就直接用现成的方法解析
} else if (!responseType || responseType === 'text') {
responseData = await res.text();
} else if (responseType === 'json') {
responseData = await res.json();
} else if (responseType === 'blob') {
responseData = await res.blob();
}
// 获取返回头信息
const responseHeaders = AxiosHeaders.from(res.headers);
// 构造axios的返回体
const response = {
data: responseData,
status: res.status,
statusText: res.statusText,
headers: responseHeaders,
config,
request: requestParams,
};
// 内部会再次判断请求是否成功
settle(
function _resolve(value) {
resolve(value);
done();
},
function _reject(err) {
reject(err);
done();
},
response
);
})
.catch((error) => {
if (error.isAxiosError) {
// 说明是超时的错误,直接抛出
reject(error);
}
// 判断error的类型,
if (error && error.type === 'abort') {
reject(
new AxiosError(
'Request aborted',
AxiosError.ECONNABORTED,
config,
requestParams
)
);
}
// 如果是网络错误
if (error && error.type === 'error') {
reject(
new AxiosError(
'Network Error',
AxiosError.ERR_NETWORK,
config,
requestParams
)
);
}
// 其他错误直接抛出
reject(error);
});
});
};
总结一下:
- 将
config
转为请求参数,主要还是参考了xhr的原代码 - 实现超时控制,使用
Promise.race()
- 实现请求取消,使用的是
AbortController
,还做了兼容CancelToken
的使用,底层也是用的AbortController
- 实现下载进度的监听,采用的方法是自己读取文件流,每读取一次,调用一次回调事件。
文件写完了之后我们还需要改动一下axios
浏览器默认适配器为fetch
,这样才能调用到我们写的代码。
首先是在lib->defaults->index.js
中
js
const defaults = {
// ...........省略
// 此处改为fetch
adapter: platform.isNode ? 'http' : 'fetch'
// ...........省略
}
另外在lib->adapters->adapters.js
中引入fetch
,这样axios
就可以获取到fetch
了。
js
// 新增
import fetchAdapter from './fetch.js';
const knownAdapters = {
http: httpAdapter,
xhr: xhrAdapter,
// 新增
fetch: fetchAdapter,
};
最后执行npm run build
,我们的包就打出来了。
看看我们的成果:
5 测试fetch是否可行
首先新建一个index.html
文件进行测试,当然自己要准备一些可调用的接口,这边就不介绍了。
5.1 测试基本的请求
js
<html>
<script type="module" >
import axios from '../dist/esm/axios.js'
// 测试get请求
axios({
url:'http://127.0.0.1:5556/auth/login?username=admin&password=admin',
method:'get',
}).then(res=>{
console.log(res);
}).catch(err=>{
console.log(err);
})
</script>
可以看到请求已经变成fetch了,并且打印出了我们的关键日志,结果也正常返回。
5.2 测试AbortController取消
js
<html>
<script type="module" >
import axios from '../dist/esm/axios.js'
// 测试signal请求
const controller = new AbortController();
const signal = controller.signal;
axios({
url:'http://127.0.0.1:5556/auth/login?username=admin&password=admin',
method:'get',
signal
}).then(res=>{
console.log(res);
}).catch(err=>{
console.log(err);
})
// 一秒后取消,我这边是在后端打断点,模拟
setTimeout(() => {
controller.abort()
}, 1000);
</script>
可以看到,抛出了请求取消的错误
5.3 测试cancelToken取消
返回结果同上,我就不贴了,测试代码如下:
js
<html>
<script type="module" >
import axios from '../dist/esm/axios.js'
// 测试cancelToken请求
const {token,cancel} = axios.CancelToken.source();
axios({
url:'http://127.0.0.1:5556/auth/login?username=admin&password=admin',
method:'get',
cancelToken:token,
}).then(res=>{
console.log(res);
}).catch(err=>{
console.log(err);
})
// 一秒后取消,我这边是在后端打断点,模拟
setTimeout(() => {
cancel()
}, 1000);
</script>
5.4 测试超时
js
<html>
<script type="module" >
import axios from '../dist/esm/axios.js'
axios({
url:'http://127.0.0.1:5556/auth/login?username=admin&password=admin',
method:'get',
//多了超时设置
timeout:1000
}).then(res=>{
console.log(res);
}).catch(err=>{
console.log(err);
})
</script>
返回的错误:
5.5 测试下载进度
js
<html>
<script type="module" >
import axios from '../dist/esm/axios.js'
axios({
url: 'http://127.0.0.1:5556/auth/test-file',
method: 'get',
// 监听下载进度
onDownloadProgress: ({ loaded, total }) => {
// 这里只做打印
console.log({loaded,total});
},
})
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log(err);
});
</script>
6 结束
至此,fetch适配器
就完成了,如果小伙伴们有疑问欢迎评论区沟通。
如果觉得本文对您有所帮助,可以给个小小的赞吗?
如果你想对axios
有更深的了解,可以阅读我的文章: