手把手教你实现自己的requset🌊,先来了解一下ajax、axios 和 fetch

1,033 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

写在最开始的话

大概在很早之前,当时还是2015年,Fetch 刚面世不久,我就借着一次重构产品的机会把当时公司一个电商isv项目全部由 $.ajax 迁移到了 Fetch

如果我没有记错的话,当时项目首页的 uv 在百万左右,pv 超过千万,后端并发高峰能达到 3.6w/s ,迁移完以后项目运行非常稳定,直到我圆满毕业,这套基于Fetch实现的 requset 一样让整个技术团队都非常满意。

我之前是使用React的,新公司技术体系基于Vue

因为不太熟悉新的环境和生态关系,在架构动作上不敢操之过急🐶 ,所以在发现现公司目前还在使用基于XMLHttpRequest封装的axios时候,也并没有为团队封装自己的requset,我相信这也是大多数各位的现状

——

那么,如果你也想和我一样,抛弃掉老旧的 XMLHttpRequest 的话,就跟着我一起来试试看如果使用 Fetch 为我们自己,也为团队实现一套自己的requset吧。

这是我自己实现requset封装的第一篇思考,其实主要是介绍 XHRFetch

本质上面是讲异步处理和 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)的原则,配置和调用方式非常混乱,而且基于事件的异步模型写起来也没有现代的 Promisegenerator/yieldasync/await 友好。

相信还在使用 XHR 的同学已经非常少了,但是因为 ajaxaxios 其实都是基于 XHR 来实现,所以这里还是要简单介绍一下 XHR

一个相对完整的 XHR 主要监听了两个时间,onloadonerror ,其通过这两个方法来绑定成功和失败的回调事件,并调用 opensend 两个方法来完成一次 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(){...}
});

嗯,这样看起来好像没什么问题,但是 bodyheader 的处理其实不太友好并且有些乱的

同时,如果你的请求依赖上一次请求的返回结果,并且可能重复多次,那就会显得非常恶心了,也就是我们常说的回调地狱

$.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,这是和 ajaxaxios有本质区别的

因为是基于 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 对象,获取结果有多种方法,数据类型有对应的获取方法,封装时需要分别处理,易出错。
    • 浏览器支持性比较差。

至此

我相信我们对 ajaxaxiosfetch 应该已经有了一个比较简单了解,那么我为什么会使用 Fetch 来实现我们自己的requset请求呢?

为什么是Fetch而不是Axios

首先,我可以直接告诉你经过大量对比以后,我得出了以下结论

  • AxiosFetch好用
  • Axios使用体验优于Fetch
  • Fetch相对Axios来说,存在浏览器兼容性问题,就好像电车相比油车存在续航焦虑一样

对的,抛开浏览器原生支持不谈,Fetch比起Axios来讲几乎没有任何优势

Axios各个方面都比Fetch好用,Fetch要想实现Axios的一些功能还需要手动进行封装

但是

  • 主流的网站都已经大量开始使用Fetch进行网络请求

    image.png

也许,Fetch的优势仅仅在于浏览器原生支持。

—— 也正是因为这点,我选择了 Fetch

Axios是对XMLHttpRequest的封装,而Fetch是一种新的获取资源的接口方式,并不是对XMLHttpRequest的封装。

它们最大的不同点在于Fetch是浏览器原生支持,而Axios需要引入Axios库。

从用户量上面来看

此刻时间停留在 2022.05.27 11:36:14 🙄

因为Node环境下默认是不支持Fetch的,所以必须要使用node-fetch这个包,那么我们可以在 npmjs 查看它的下载量

node-fetch

image.png

Fetch的下载量: 2.7kw

Axios的下载量: 2.4kw

www.npmjs.com/package/axi…

image.png

由上面的对比数据我们可以看出,node-fetch 的下载量远远高于 axios 的下载

而且,这还仅仅是nodejs环境,浏览器则是原生支持,不需要第三方包

兼容性

axios是基于 XHR,通过封装封装实现,这个库本身就考虑过兼容性问题,基本不存在浏览器兼容

fetch的兼容性具体查看 can i use

image.png

基本上,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云里雾里?