持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情
写在最开始的话
大概在很早之前,当时还是2015年,Fetch
刚面世不久,我就借着一次重构产品的机会把当时公司一个电商isv项目全部由 $.ajax
迁移到了 Fetch
如果我没有记错的话,当时项目首页的 uv
在百万左右,pv
超过千万,后端并发高峰能达到 3.6w/s
,迁移完以后项目运行非常稳定,直到我圆满毕业,这套基于Fetch
实现的 requset 一样让整个技术团队都非常满意。
我之前是使用React
的,新公司技术体系基于Vue
因为不太熟悉新的环境和生态关系,在架构动作上不敢操之过急🐶 ,所以在发现现公司目前还在使用基于XMLHttpRequest
封装的axios
时候,也并没有为团队封装自己的requset,我相信这也是大多数各位的现状
——
那么,如果你也想和我一样,抛弃掉老旧的 XMLHttpRequest
的话,就跟着我一起来试试看如果使用 Fetch
为我们自己,也为团队实现一套自己的requset吧。
这是我自己实现requset封装的第一篇思考,其实主要是介绍 XHR
和 Fetch
本质上面是讲异步处理和 Promise
,以及我为什么选择 Fetch
而不用 axios
Why & Fetch
XMLHttpRequest
如果你还在使用 XMLHttpRequest
的话,你的请求可能是这样的:
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'json';
xhr.onload = function() {
console.log(xhr.response);
};
xhr.onerror = function() {
console.log("Oops, error");
};
xhr.send();
XMLHttpRequest
是一个设计粗糙的 API,不符合关注分离(Separation of Concerns)的原则,配置和调用方式非常混乱,而且基于事件的异步模型写起来也没有现代的Promise
,generator/yield
,async/await
友好。
相信还在使用 XHR
的同学已经非常少了,但是因为 ajax
和 axios
其实都是基于 XHR
来实现,所以这里还是要简单介绍一下 XHR
一个相对完整的 XHR
主要监听了两个时间,onload
和 onerror
,其通过这两个方法来绑定成功和失败的回调事件,并调用 open
和 send
两个方法来完成一次 requset 的请求
这样子使用起来,异步访问、读取资源都会显得很繁琐,所以我们需要对它进行一些小小的加工
const $ = {};
$.ajax = (obj) => {
let xhr;
if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else if (window.ActiveXObject) {
// IE
try {
xhr = new ActiveXObject('Msxml2.XMLHTTP');
} catch (e) {
try {
xhr = new ActiveXObject('Microsoft.XMLHTTP');
} catch (e) {}
}
}
if (xhr) {
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
obj.success(xhr.responseText);
// 返回值传callback
} else {
// failcallback
obj.error('There was a problem with the request.');
}
} else {
console.log('still not ready...');
}
};
xhr.open(obj.method, obj.url, true);
// 设置 Content-Type 为 application/x-www-form-urlencoded
// 以表单的形式传递数据
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send(util(obj.data));
//处理body数据
}
//处理数据
const util = (obj) => {
let str = '';
for (key in obj) {
str += key + '=' + obj[key] + '&';
}
return str.substring(0, str.length - 1);
};
};
这就是 ajax
的简单封装 🌊
Ajax
XHR
加工好以后,你可能会这样使用
$.ajax({
url: 'https://xxx',
type: 'POST',
data: {
username: 'root',
password: '123123'
},
success:function(){...}
});
嗯,这样看起来好像没什么问题,但是 body
和 header
的处理其实不太友好并且有些乱的
同时,如果你的请求依赖上一次请求的返回结果,并且可能重复多次,那就会显得非常恶心了,也就是我们常说的回调地狱
$.ajax({
url: 'https://xxx',
type: 'POST',
data: {
username: 'root',
password: '123123'
},
success:function(){
$.ajax({
url: 'https://xxx',
type: 'POST',
data: {
username: 'root',
password: '123123'
},
success:function(){
$.ajax({
url: 'https://xxx',
type: 'POST',
data: {
username: 'root',
password: '123123'
},
success:function(){
$.ajax({
url: 'https://xxx',
type: 'POST',
data: {
password: '123123'
},
success:function(){
...
}
});
}
});
}
});
}
});
同志,大清早就亡了!
面对这种情况,我们可以用 Promise
来对他二次封装,完美解决回调地狱的问题
const axios = (url, type, data)=>{
return new Promise((resolve, reject) => {
$.ajax({
url,
type,
data,
success:function(){
resolve();
}
});
}
}
这样 requset 就会好看起来了
axios('https://xxx', 'post', {})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
然后你还可以使用 async/await
来解决回调地狱的问题
async ()=> {
const rsp = await axios('https://xxx', 'post', {});
const rsp2 = await axios('https://xxx', 'post', rsp.data);
const rsp3 = await axios('https://xxx', 'post', rsp2.data);
const rsp4 = await axios('https://xxx', 'post', rsp3.data);
const rsp5 = await axios('https://xxx', 'post', rsp4.data);
const rsp6 = await axios('https://xxx', 'post', rsp5.data);
...
}
Axios
axios
的功能真的非常强大,包括 取消请求
,超时处理
,进度处理
,拦截器
……
但是经过上面的简单例子,你不难发现,它的本质其实还是 ajax
,基于 Promise
进行封装,解决了回调地狱问题🙅🏻♀️
// 请求拦截
axios.interceptors.request.use((config) => {
console.log('Request sent');
})
// 响应拦截
axios.interceptors.response.use((response) => {
return response
})
axios
.post('/user', { firstName: 'Fred', lastName: 'Flintstone' })
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
Fetch
那Fetch
呢?
fetch(url)
.then(function(response) {
return response.json();
})
.then(function(data) {
console.log(data);
})
.catch(function(e) {
console.log("Oops, error");
});
使用箭头函数优化
fetch(url).then(response => response.json())
.then(data => console.log(data))
.catch(e => console.log("Oops, error", e))
Fetch
属于原生的 js 代码,脱离 XRH
,基于 Promise
,这是和 ajax
、axios
有本质区别的
因为是基于 Promise
对象设计,所以Fetch
天生支持async/await
优化
try{
const rsp = await fetch(url)
const data = await rsp.json()
}catch( e => console.log("Oops, error", e))
Ajax & Axios & Fetch
通过上面的介绍
我们再分别总结一下它们的优缺点
方便有一个直观的对比,这也是方便接下来我们实现自己的requset第一个步骤
—— 技术选型
- Ajax
- 属 js 原生,基于XHR进行开发,XHR 结构不清晰。
- 针对 mvc 编程,由于近来vue和React的兴起,不符合mvvm前端开发流程。
- 单纯使用 ajax 封装,核心是使用 XMLHttpRequest 对象,使用较多并有先后顺序的话,容易产生回调地狱。
- Axios
- 在浏览器中创建XMLHttpRequest请求,在node.js中创建http请求。
- 解决回调地狱问题。
- 自动转化为json数据类型。
- 支持Promise技术,提供并发请求接口。
- 可以通过网络请求检测进度。
- 提供超时处理。
- 浏览器兼容性良好。
- 有拦截器,可以对请求和响应统一处理。
- Fetch
- 属于原生 js,脱离了xhr ,号称可以替代 ajax技术。
- 基于 Promise 对象设计的,可以解决回调地狱问题。
- 提供了丰富的 API,使用结构简单。
- 默认不带cookie,使用时需要设置。
- 没有办法检测请求的进度,无法取消或超时处理。
- 返回结果是 Promise 对象,获取结果有多种方法,数据类型有对应的获取方法,封装时需要分别处理,易出错。
- 浏览器支持性比较差。
至此
我相信我们对 ajax
、 axios
和 fetch
应该已经有了一个比较简单了解,那么我为什么会使用 Fetch
来实现我们自己的requset请求呢?
为什么是Fetch而不是Axios
首先,我可以直接告诉你经过大量对比以后,我得出了以下结论
Axios
比Fetch
好用Axios
使用体验优于Fetch
Fetch
相对Axios
来说,存在浏览器兼容性问题,就好像电车相比油车存在续航焦虑一样
对的,抛开浏览器原生支持不谈,Fetch
比起Axios
来讲几乎没有任何优势
Axios
各个方面都比Fetch
好用,Fetch
要想实现Axios
的一些功能还需要手动进行封装
但是
-
主流的网站都已经大量开始使用
Fetch
进行网络请求
也许,Fetch的优势仅仅在于浏览器原生支持。
—— 也正是因为这点,我选择了 Fetch
Axios是对XMLHttpRequest的封装,而Fetch是一种新的获取资源的接口方式,并不是对XMLHttpRequest的封装。
它们最大的不同点在于
Fetch
是浏览器原生支持,而Axios
需要引入Axios
库。
从用户量上面来看
此刻时间停留在 2022.05.27 11:36:14 🙄
因为Node环境下默认是不支持Fetch的,所以必须要使用node-fetch
这个包,那么我们可以在 npmjs
查看它的下载量
Fetch
的下载量: 2.7kw
Axios
的下载量: 2.4kw
由上面的对比数据我们可以看出,node-fetch
的下载量远远高于 axios
的下载
而且,这还仅仅是nodejs环境,浏览器则是原生支持,不需要第三方包
兼容性
axios是基于 XHR
,通过封装封装实现,这个库本身就考虑过兼容性问题,基本不存在浏览器兼容
fetch的兼容性具体查看 can i use
基本上,Fetch
在IE和一些老版的浏览器上面是完全不兼容的,如果使用 Fetch
需要考虑兼容性的问题,可以去网上找一些第三方库做的 polyfill
基本原理都是探寻浏览器本身是否存在 window.fetch ,如果不存在则通过 XHR
实现
所以本质上我们如果选用 Fetch
封装 requset 的话,可以基于这个思想自己实现一套 polyfill 逻辑
常见的 polyfill 库
如果你使用了jsonp请求的话
如果你需要兼容 ie 和老版浏览器,在 ie 上最多能支持 ie8+
API & 使用
在功能性和所提供的API方面,比如我们常用的 请求重试
、响应超时处理
、拦截器
、状态hook以及进度处理
、统一返回结构格式【数据转化】
、并发请求
……
这些相对axios
而言,Fetch
本身都不提供,需要我们自己再封装 requset 的时候去实现
以拦截器为例
我们先来看一段简单的使用Fetch
封装的拦截器
因为我封装的时候使用的是ts,所以这里是ts代码
export class FetchAPI implements FetchAPIType {
// fetch 实例
instance: (options: Options) => Promise<Response>;
baseUrl: string;
controller: any;
interceptors: any; // 拦截器
interceptorsRes: Function[]; // 成功的拦截器队列
interceptorsResError: Function[]; // 失败的拦截器队列
constructor(parm: FetchParam) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
this.baseUrl = parm?.baseUrl || '';
this.controller = new AbortController();
this.instance = fetchRequest;
this.interceptorsRes = [];
this.interceptorsResError = [];
this.interceptors = {
request: {
// 请求拦截
use(callback: any, errorCallback: any) {
self.interceptorsRes.push(callback);
errorCallback && self.interceptorsResError.push(errorCallback);
},
},
response: {
// 响应拦截
use(callback: any, errorCallback: any) {
self.interceptorsRes.push(callback);
errorCallback && self.interceptorsResError.push(errorCallback);
},
},
};
}
………
总结
对于我来说,浏览器对Fetch
的原生支持,是压死骆驼的最后一根稻草
是Fetch
唯一碾压Axios
的一点
我们所需求的各种多样性和个性化的需求,其实Axios
大部分都考虑到了并且实现了
反而,Fetch
其实只提供了core部分,如果想要实现Axios
所具备的功能,都需要我们自己封装
这里有一个建议
如果不喜欢折腾直接在项目中使用Axios
是一个非常明智的选择,这完全取决于你是否愿意使用浏览器内置API。
历史的潮流永远是向前发展的,今天我们的需求是想要实现一个自己的requset库
如果还继续使用XHR
的话,你确定不是49年入国军吗?
有时候,新技术逐步取代老旧技术这是一个必然趋势,所以Fetch
有一天终将会完全取代XHR
到这时候,或许Axios
库也会改为Fetch
请求
早晚都会有这一天来临
还不如从现在开始拥抱变化
马上我就分享我具体是如何用 Fetch
来封装我们自己的 requset
因为在项目中,我用到了ts
作为开发语言,所以你可以通过我之前的这篇文章了解什么是ts
这里有一份邀请,邀请你来和我一起学习Ts —— TypeScript科普(一):都2022年了,你还对TypeScript云里雾里?