fetch的用法、缺陷、常见缺陷处理

作者: 淡退 | 来源:发表于2019-11-21 14:55 被阅读0次

    fetch号称是ajax的替代品,它的API是基于Promise设计的,旧版本的浏览器不支持 Promise。

    关于fetch的用法 ,本文就不做介绍了,可以参看官方文档,可以得到很详细的介绍 使用Fetch - Web API 接口参考 | MDN

    Fetch的代码结构比起ajax简单多了,参数有点像jQuery ajax。但是,一定记住fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象。
    fetch的优点:

    1. 符合关注分离,没有将输入、输出和用事件来跟踪的状态混杂在一个对象里
    2. 更好更方便的写法,坦白说,上面的理由对我来说完全没有什么说服力,因为不管是Jquery还是Axios都已经帮我们把xhr封装的足够好,使用起来也足够方便,为什么我们还要花费大力气去学习fetch?

    我认为fetch的优势就是

    1. 语法简洁,更加语义化
    2. 基于标准 Promise 实现,支持 async/await
    3. 同构方便,使用 isomorphic-fetch
    4. 更加底层,提供的API丰富(request, response)
    5. 脱离了XHR,是ES规范里新的实现方式,是js原生方法不需要引入额外的库。

    近在使用fetch的时候,也遇到了不少的问题:fetch是一个低层次的API,你可以把它考虑成原生的XHR,所以使用起来并不是那么舒服,需要进行封装。

    例如:

    1. fetch只对网络请求报错,对400,500都当做成功的请求,服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。
    2. fetch默认不会带cookie,需要添加配置项: fetch(url, {credentials: 'include'})
    3. fetch不支持abort,不支持超时控制。
    4. fetch没有办法原生监测请求的进度,而XHR可以

    正对上面几个问题,我们下面分别来分析,并对其问题进行处理:

    1. fetch请求对某些错误http状态不会reject

    这主要是由fetch返回promise导致的,因为fetch返回的promise在某些错误的http状态下如400、500等不会reject,相反它会被resolve;只有网络错误会导致请求不能完成时,fetch 才会被 reject;所以一般会对fetch请求做一层封装。 简单的理解就是 fetch 只会认为断网这种情况才会是错误的,其他情况比如:404,403等请求错误都是认为请求成功了,应为它发起请求并收到了响应。

    所以我们对返回状态进行校验,然后抛出错误,以便返回正常的报错信息。

    对fetch中的错误进行抛出,然后对不同的状态返回不同的报错信息

    function checkStatus(response) {
      if (response.status >= 200 && response.status < 300) {
        return response;
      }
    
      const errortext = response.statusText;
      const error = new Error(errortext);
      error.name = response.status;
      error.response = response;
      throw error;
    }
    
    let fetchChain = fetch(newUrl, newOptions)
        .then(checkStatus)
        .then(response => {
          if (response.status === 204) {
            return {};
          }
          if (newOptions.responseType === 'blob') {
            return response.blob();
          }
          return newOptions.responseType === 'text' ? response.text() : response.json();
        });
    

    上面的代码中的checkStatus 主要是检查成功状态下的问题处理。其他情况不做处理。

    如果要对其他的状态进行检查 需要通过catch来进行异常的捕获

      。。。
    fetchChain = fetchChain.catch(e => {
        const status = e.name;
        // 监听到 401 错误 重新登陆
        if (status === 401) {
            // code 
        }
    
        // 监听到 网络请求错误
        // https://github.com/github/fetch/issues/201
        if (status === 'TypeError') {
           // code 
        }
    
        if (status === 501) {
          // 后端正常的报错/服务器报错
        }
    
        if (status === 403) {
          notification.error({
            message: `${status}`,
            description: '抱歉,您无权限访问此功能',
          });
          return;
        }
       
        // 其他错误
        notification.error({
          message: `${status}`,
          description: e.message,
        });
      });
    

    上面代码就实现了 fetch对某些错误http状态不会reject,同时面对不同状态的不同处理。
    关于catch理解可以看对Promise中的resolve,reject,catch理解

    2. fetch不支持abort,不支持超时控制。

    fetch不像大多数ajax库那样对请求设置超时timeout,它没有有关请求超时的feature,这一点比较蛋疼。所以在fetch标准添加超时feature之前,都需要polyfill该特性。

    实际上,我们真正需要的是abort(), timeout可以通过timeout+abort方式来实现,起到真正超时丢弃当前的请求。

    而在目前的fetch指导规范中,fetch并不是一个具体实例,而只是一个方法;其返回的promise实例根据Promise指导规范标准是不能abort的,也不能手动改变promise实例的状态,只能由内部来根据请求结果来改变promise的状态。

    既然不能手动控制fetch方法执行后返回的promise实例状态,那么是不是可以创建一个可以手动控制状态的新Promise实例呢。所以:
    实现fetch的timeout功能,其思想就是新创建一个可以手动控制promise状态的实例,根据不同情况来对新promise实例进行resolve或者reject,从而达到实现timeout的功能;

    方法一:单纯setTimeout方式
    var oldFetchfn = fetch; //拦截原始的fetch方法
    window.fetch = function(input, opts) {
        //定义新的fetch方法,封装原有的fetch方法
        return new Promise(function(resolve, reject) {
            var timeoutId = setTimeout(function() {
                reject(new Error("fetch timeout"));
            }, opts.timeout);
            oldFetchfn(input, opts).then(
                res => {
                    clearTimeout(timeoutId);
                    resolve(res);
                },
                err => {
                    clearTimeout(timeoutId);
                    reject(err);
                }
            );
        });
    };
    

    当然在上面基础上可以模拟类似XHR的abort功能:

    var oldFetchfn = fetch;
    window.fetch = function(input, opts) {
        return new Promise(function(resolve, reject) {
            var abort_promise = function() {
                reject(new Error("fetch abort"));
            };
            var p = oldFetchfn(input, opts).then(resolve, reject);
            p.abort = abort_promise;
            return p;
        });
    };
    
    方法二:利用Promise.race方法

    Promise.race方法接受一个promise实例数组参数,表示多个promise实例中任何一个最先改变状态,那么race方法返回的promise实例状态就跟着改变,具体可以参考这里

    var oldFetchfn = fetch; //拦截原始的fetch方法
    window.fetch = function(input, opts){//定义新的fetch方法,封装原有的fetch方法
        var fetchPromise = oldFetchfn(input, opts);
        var timeoutPromise = new Promise(function(resolve, reject){
            setTimeout(()=>{
                 reject(new Error("fetch timeout"))
            }, opts.timeout)
        });
        retrun Promise.race([fetchPromise, timeoutPromise])
    }
    

    通过上面两种方式发现可以发现:
    timeout不是请求连接超时的含义,它表示请求的response时间,包括请求的连接、服务器处理及服务器响应回来的时间;

    fetch的timeout即使超时发生了,本次请求也不会被abort丢弃掉,它在后台仍然会发送到服务器端,只是本次请求的响应内容被丢弃而已; 这样就会造成了流量的浪费。 关于怎么正在取消请求可以参考:https://github.com/hjylewis/trashable

    参考文章:
    Fetch 手动终止
    Fetch的数据获取和发送以及异常处理
    fetch的常见问题及其解决办法

    相关文章

      网友评论

        本文标题:fetch的用法、缺陷、常见缺陷处理

        本文链接:https://www.haomeiwen.com/subject/mzgzictx.html