fetch号称是ajax的替代品,它的API是基于Promise设计的,旧版本的浏览器不支持 Promise。
关于fetch的用法 ,本文就不做介绍了,可以参看官方文档,可以得到很详细的介绍 使用Fetch - Web API 接口参考 | MDN
Fetch的代码结构比起ajax简单多了,参数有点像jQuery ajax。但是,一定记住fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象。
fetch的优点:
- 符合关注分离,没有将输入、输出和用事件来跟踪的状态混杂在一个对象里
- 更好更方便的写法,坦白说,上面的理由对我来说完全没有什么说服力,因为不管是Jquery还是Axios都已经帮我们把xhr封装的足够好,使用起来也足够方便,为什么我们还要花费大力气去学习fetch?
我认为fetch的优势就是
- 语法简洁,更加语义化
- 基于标准 Promise 实现,支持 async/await
- 同构方便,使用 isomorphic-fetch
- 更加底层,提供的API丰富(request, response)
- 脱离了XHR,是ES规范里新的实现方式,是js原生方法不需要引入额外的库。
近在使用fetch的时候,也遇到了不少的问题:fetch是一个低层次的API,你可以把它考虑成原生的XHR,所以使用起来并不是那么舒服,需要进行封装。
例如:
- fetch只对网络请求报错,对400,500都当做成功的请求,服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。
- fetch默认不会带cookie,需要添加配置项: fetch(url, {credentials: 'include'})
- fetch不支持abort,不支持超时控制。
- 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的常见问题及其解决办法