fetch使用的常见问题及其解决办法

首先声明一下,本文不是要讲解fetch的具体用法,不清楚的能够参考MDN fetch教程javascript

引言

说道fetch就不得不提XMLHttpRequest了,XHR在发送web请求时须要开发者配置相关请求信息和成功后的回调,尽管开发者只关心请求成功后的业务处理,可是也要配置其余繁琐内容,致使配置和调用比较混乱,也不符合关注分离的原则;fetch的出现正是为了解决XHR存在的这些问题。例以下面代码:css

fetch(url).then(function(response) {
  return response.json();
}).then(function(data) {
  console.log(data);
}).catch(function(e) {
  console.log("Oops, error");
});

上面这段代码让开发者只关注请求成功后的业务逻辑处理,其余的不用关心,至关简单;也比较符合现代Promise形式,比较友好。html

fetch是基于Promise设计的,从上面代码也能看得出来,这就要求fetch要配合Promise一块儿使用。正是这种设计,fetch所带来的优势正如传统 Ajax 已死,Fetch 永生总结的同样:前端

  • 语法简单,更加语义化java

  • 基于标准的Promise实现,支持async/awaitgit

  • 使用isomorphic-fetch能够方便同构es6

不过话说回来,fetch虽然有不少优势,可是使用fetch来进行项目开发时,也是有一些常见问题的,下面就来讲说fetch使用的常见问题。github

fetch兼容性

fetch是相对较新的技术,固然就会存在浏览器兼容性的问题,借用上面应用文章的一幅图加以说明fetch在各类浏览器的原生支持状况:
web

从上图能够看出,在各个浏览器低版本的状况下都是不被支持的。面试

那么问题来了,如何在全部浏览器中通用fetch呢,固然就要考虑fetch的polyfill了。

上面说过,fetch是基于Promise来实现的,因此在低版本浏览器中Promise可能也未被原生支持,因此还须要Promise的polyfill;大多数状况下,实现fetch的polyfill须要涉及到的:

  • promise的polyfill,例如es6-promise、babel-polyfill提供的promise实现。

  • fetch的polyfill实现,例如isomorphic-fetch和whatwg-fetch

这样是否就能够安全的使用fetch来进行先后端通讯了?上面说了在大多数状况下是这样,可是IE8/9则比较特殊:IE8它使用的是ES3,而IE9则对ES5部分支持。这种状况下还须要ES5的polyfill es5-shim支持了。

上述有关promise的polyfill实现,须要说明的是:

babel-runtime是不能做为Promise的polyfill的实现的,不然在IE8/9下使用fetch会报Promise未定义。为何?我想你们猜到了,由于babel-runtime实现的polyfill是局部实现而不是全局实现,fetch底层实现用到Promise就是从全局中去取的,拿不到这报上述错误。

另外,顺便补充一下fetch的polyfill实现思路是:

首先判断浏览器是否原生支持fetch,不然结合Promise使用XMLHttpRequest的方式来实现;这正是whatwg-fetch的实现思路,而同构应用中使用的isomorphic-fetch,其客户端fetch的实现是直接require whatwg-fetch来实现的。

fetch默认不携带cookie

fetch发送请求默认是不发送cookie的,不论是同域仍是跨域;那么问题就来了,对于那些须要权限验证的请求就可能没法正常获取数据,这时能够配置其credentials项,其有3个值:

  • omit: 默认值,忽略cookie的发送

  • same-origin: 表示cookie只能同域发送,不能跨域发送

  • include: cookie既能够同域发送,也能够跨域发送

credentials所表达的含义,其实与XHR2中的withCredentials属性相似,表示请求是否携带cookie;具体能够参考阮一峰老师的跨域资源共享 CORS 详解中withCredentials一节的介绍;

这样,若要fetch请求携带cookie信息,只需设置一下credentials选项便可,例如fetch(url, {credentials: 'include'});

另外补充一点:

fetch默认对服务端经过Set-Cookie头设置的cookie也会忽略,若想选择接受来自服务端的cookie信息,也必需要配置credentials选项;

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

这主要是由fetch返回promise致使的,由于fetch返回的promise在某些错误的http状态下如400、500等不会reject,相反它会被resolve;只有网络错误会致使请求不能完成时,fetch 才会被 reject;因此通常会对fetch请求作一层封装,例以下面代码所示:

function checkStatus(response) {
  if (response.status >= 200 && response.status < 300) {
    return response;
  }
  const error = new Error(response.statusText);
  error.response = response;
  throw error;
}
function parseJSON(response) {
  return response.json();
}
export default function request(url, options) {
  let opt = options||{};
  return fetch(url, {credentials: 'include', ...opt})
    .then(checkStatus)
    .then(parseJSON)
    .then((data) => ( data ))
    .catch((err) => ( err ));
}

fetch不支持超时timeout处理

用过fetch的都知道,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的功能;

根据github上timeout handling上的讨论,目前能够有两种不一样的解决方法:

方法一:单纯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])
}

最后,对fetch的timeout的上述实现方式补充几点:

timeout不是请求链接超时的含义,它表示请求的response时间,包括请求的链接、服务器处理及服务器响应回来的时间;

fetch的timeout即便超时发生了,本次请求也不会被abort丢弃掉,它在后台仍然会发送到服务器端,只是本次请求的响应内容被丢弃而已;

fetch不支持JSONP

fetch是与服务器端进行异步交互的,而JSONP是外链一个javascript资源,并非真正ajax,因此fetch与JSONP没有什么直接关联,固然至少目前是不支持JSONP的。

这里咱们把JSONP与fetch关联在一块儿有点差强人意,fetch只是一个ajax库,咱们不可能使fetch支持JSONP;只是咱们要实现一个JSONP,只不过这个JSONP的实现要与fetch的实现相似,即基于Promise来实现一个JSONP;而其外在表现给人感受是fetch支持JSONP同样;

目前比较成熟的开源JSONP实现fetch-jsonp给咱们提供了解决方案,想了解能够自行前往。不过再次想唠叨一下其JSONP的实现步骤,由于在本人面试的前端候选人中大部分人对JSONP的实现语焉不详;

使用它很是简单,首先须要用npm安装fetch-jsonp

npm install fetch-jsonp --save-dev

而后在像下面同样使用:

fetchJsonp('/users.jsonp', {
    timeout: 3000,
    jsonpCallback: 'custom_callback'
  })
  .then(function(response) {
    return response.json()
  }).catch(function(ex) {
    console.log('parsing failed', ex)
  })

fetch不支持progress事件

XHR是原生支持progress事件的,例以下面代码这样:

var xhr = new XMLHttpRequest()
xhr.open('POST', '/uploads')
xhr.onload = function() {}
xhr.onerror = function() {}
function updateProgress (event) {
  if (event.lengthComputable) {
    var percent = Math.round((event.loaded / event.total) * 100)
    console.log(percent)
  }
xhr.upload.onprogress =updateProgress; //上传的progress事件
xhr.onprogress = updateProgress; //下载的progress事件
}
xhr.send();

可是fetch是不支持有关progress事件的;不过可喜的是,根据fetch的指导规范标准,其内部设计实现了RequestResponse类;其中Response封装一些方法和属性,经过Response实例能够访问这些方法和属性,例如response.json()response.body等等;

值得关注的地方是,response.body是一个可读字节流对象,其实现了一个getRender()方法,其具体做用是:

getRender()方法用于读取响应的原始字节流,该字节流是能够循环读取的,直至body内容传输完成;

所以,利用到这点能够模拟出fetch的progress,具体能够参考这篇文章2016 - the year of web streams

代码实现以下,在线demo请参考fetch progress demo

// fetch() returns a promise that resolves once headers have been received
fetch(url).then(response => {
  // response.body is a readable stream.
  // Calling getReader() gives us exclusive access to the stream's content
  var reader = response.body.getReader();
  var bytesReceived = 0;

  // read() returns a promise that resolves when a value has been received
  reader.read().then(function processResult(result) {
    // Result objects contain two properties:
    // done  - true if the stream has already given you all its data.
    // value - some data. Always undefined when done is true.
    if (result.done) {
      console.log("Fetch complete");
      return;
    }

    // result.value for fetch streams is a Uint8Array
    bytesReceived += result.value.length;
    console.log('Received', bytesReceived, 'bytes of data so far');

    // Read some more, and call this function again
    return reader.read().then(processResult);
  });
});

另外,github上也有使用Promise+XHR结合的方式实现类fetch的progress效果(固然这跟fetch彻底不搭边)能够参考这里,具体代码以下:

function fetchProgress(url, opts={}, onProgress){
    return new Promise(funciton(resolve, reject){
        var xhr = new XMLHttpRequest();
        xhr.open(opts.method || 'get', url);
        for(var key in opts.headers || {}){
            xhr.setRequestHeader(key, opts.headers[key]);
        }

        xhr.onload = e => resolve(e.target.responseText)
        xhr.onerror = reject;
        if (xhr.upload && onProgress){
            xhr.upload.onprogress = onProgress; //上传
        }
        if ('onprogerss' in xhr && onProgress){
            xhr.onprogress = onProgress; //下载
        }
        xhr.send(opts.body)
    })
}
fetchProgress('/upload').then(console.log)

fetch跨域问题

既然是ajax库,就不可避免与跨域扯上关系;XHR2是支持跨域请求的,只不过要知足浏览器端支持CORS,服务器经过Access-Control-Allow-Origin来容许指定的源进行跨域,仅此一种方式。

与XHR2同样,fetch也是支持跨域请求的,只不过其跨域请求作法与XHR2同样,须要客户端与服务端支持;另外,fetch还支持一种跨域,不须要服务器支持的形式,具体能够经过其mode的配置项来讲明。

fetch的mode配置项有3个值,以下:

  • same-origin:该模式是不容许跨域的,它须要遵照同源策略,不然浏览器会返回一个error告知不能跨域;其对应的response type为basic

  • cors: 该模式支持跨域请求,顾名思义它是以CORS的形式跨域;固然该模式也能够同域请求不须要后端额外的CORS支持;其对应的response type为cors

  • no-cors: 该模式用于跨域请求可是服务器不带CORS响应头,也就是服务端不支持CORS;这也是fetch的特殊跨域请求方式;其对应的response type为opaque

针对跨域请求,cors模式是常见跨域请求实现,可是fetch自带的no-cors跨域请求模式则较为陌生,该模式有一个比较明显的特色:

该模式容许浏览器发送本次跨域请求,可是不能访问响应返回的内容,这也是其response type为opaque透明的缘由。

这与<img/>发送的请求相似,只是该模式不能访问响应的内容信息;可是它能够被其余APIs进行处理,例如ServiceWorker。另外,该模式返回的repsonse能够在Cache API中被存储起来以便后续的对它的使用,这点对script、css和图片的CDN资源是很是合适的,由于这些资源响应头中都没有CORS头。

总的来讲,fetch的跨域请求是使用CORS方式,须要浏览器和服务端的支持。

参考文献

相关文章
相关标签/搜索