持续创作,加速成长!这是我参与「掘金日新计划 · 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使用体验优于FetchFetch相对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云里雾里?