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

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中的错误进行抛出,然后对不同的状态返回不同的报错信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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来进行异常的捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
  。。。
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方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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功能:

1
2
3
4
5
6
7
8
9
10
11
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实例状态就跟着改变,具体可以参考这里。

1
2
3
4
5
6
7
8
9
10
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的常见问题及其解决办法