首先声明一下,本文不是要讲解 fetch 的具体用法,不清楚的可以参考。
说道 fetch 就不得不提 XMLHttpRequest 了,XHR 在发送 web 请求时需要开发者配置相关请求信息和成功后的回调,尽管开发者只关心请求成功后的业务处理,但是也要配置其他繁琐内容,导致配置和调用比较混乱,也不符合关注分离的原则;fetch 的出现正是为了解决 XHR 存在的这些问题。例如下面代码:
- fetch(url).then(function(response) {
- return response.json();
- }).then(function(data) {
- console.log(data);
- }).
- catch(function(e) {
- console.log("Oops, error");
- });
上面这段代码让开发者只关注请求成功后的业务逻辑处理,其他的不用关心,相当简单;也比较符合现代 Promise 形式,比较友好。
fetch 是基于 Promise 设计的,从上面代码也能看得出来,这就要求 fetch 要配合 Promise 一起使用。正是这种设计,fetch 所带来的优点正如总结的一样:
不过话说回来,fetch 虽然有很多优点,但是使用 fetch 来进行项目开发时,也是有一些常见问题的,下面就来说说 fetch 使用的常见问题。
fetch 是相对较新的技术,当然就会存在浏览器兼容性的问题,借用上面应用文章的一幅图加以说明 fetch 在各种浏览器的原生支持情况:
从上图可以看出,在各个浏览器低版本的情况下都是不被支持的。
那么问题来了,如何在所有浏览器中通用 fetch 呢,当然就要考虑 fetch 的 polyfill 了。
上面说过,fetch 是基于 Promise 来实现的,所以在低版本浏览器中 Promise 可能也未被原生支持,所以还需要 Promise 的 polyfill;大多数情况下,实现 fetch 的 polyfill 需要涉及到的:
这样是否就可以安全的使用 fetch 来进行前后端通信了?上面说了在大多数情况下是这样,但是 IE8/9 则比较特殊:IE8 它使用的是 ES3,而 IE9 则对 ES5 部分支持。这种情况下还需要 ES5 的 polyfill 支持了。
另外,顺便补充一下:
fetch 发送请求默认是不发送 cookie 的,不管是同域还是跨域;那么问题就来了,对于那些需要权限验证的请求就可能无法正常获取数据,这时可以配置其
项,其有 3 个值:
- credentials
: 默认值,忽略 cookie 的发送
- omit
: 表示 cookie 只能同域发送,不能跨域发送
- same-origin
: cookie 既可以同域发送,也可以跨域发送
- include
所表达的含义,其实与 XHR2 中的
- credentials
属性类似,表示请求是否携带 cookie;具体可以参考阮一峰老师的中 withCredentials 一节的介绍;
- withCredentials
这样,若要 fetch 请求携带 cookie 信息,只需设置一下 credentials 选项即可,例如
;
- fetch(url, {credentials: 'include'})
另外补充一点:
这主要是由 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 的都知道,fetch 不像大多数 ajax 库那样对请求设置超时 timeout,它没有有关请求超时的 feature,这一点比较蛋疼。所以在 fetch 标准添加超时 feature 之前,都需要 polyfill 该特性。
实际上,我们真正需要的是
, timeout 可以通过
- abort()
方式来实现,起到真正超时丢弃当前的请求。
- timeout+abort
而在目前的 fetch 指导规范中,fetch 并不是一个具体实例,而只是一个方法;其返回的 promise 实例根据 Promise 指导规范标准是不能 abort 的,也不能手动改变 promise 实例的状态,只能由内部来根据请求结果来改变 promise 的状态。
既然不能手动控制 fetch 方法执行后返回的 promise 实例状态,那么是不是可以创建一个可以手动控制状态的新 Promise 实例呢。所以:
- 实现fetch的timeout功能,其思想就是新创建一个可以手动控制promise状态的实例,根据不同情况来对新promise实例进行resolve或者reject,从而达到实现timeout的功能;
根据 github 上上的讨论,目前可以有两种不同的解决方法:
- 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 实例数组参数,表示多个 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 的上述实现方式补充几点:
fetch 是与服务器端进行异步交互的,而 JSONP 是外链一个 javascript 资源,并不是真正 ajax,所以 fetch 与 JSONP 没有什么直接关联,当然至少目前是不支持 JSONP 的。
这里我们把 JSONP 与 fetch 关联在一起有点差强人意,fetch 只是一个 ajax 库,我们不可能使 fetch 支持 JSONP;只是我们要实现一个 JSONP,只不过这个 JSONP 的实现要与 fetch 的实现类似,
;而其外在表现给人感觉是 fetch 支持 JSONP 一样;
- 即基于Promise来实现一个JSONP
目前比较成熟的开源 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)
- })
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 是不支持有关
事件的;不过可喜的是,根据 fetch 的指导规范标准,其内部设计实现了
- progress
和
- Request
类;其中 Response 封装一些方法和属性,通过 Response 实例可以访问这些方法和属性,例如
- Response
、
- response.json()
等等;
- response.body
值得关注的地方是,
是一个可读字节流对象,其实现了一个
- response.body
方法,其具体作用是:
- getRender()
因此,利用到这点可以模拟出 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 上也有使用
结合的方式实现类 fetch 的 progress 效果 (当然这跟 fetch 完全不搭边)可以参考,具体代码如下:
- Promise+XHR
- 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)
既然是 ajax 库,就不可避免与跨域扯上关系;XHR2 是支持跨域请求的,只不过要满足浏览器端支持
,服务器通过
- CORS
来允许指定的源进行跨域,仅此一种方式。
- Access-Control-Allow-Origin
与 XHR2 一样,fetch 也是支持跨域请求的,只不过其跨域请求做法与 XHR2 一样,需要客户端与服务端支持;另外,fetch 还支持一种跨域,不需要服务器支持的形式,具体可以通过其
的配置项来说明。
- mode
fetch 的
配置项有 3 个值,如下:
- mode
:该模式是不允许跨域的,它需要遵守同源策略,否则浏览器会返回一个 error 告知不能跨域;其对应的 response type 为
- same-origin
。
- basic
: 该模式支持跨域请求,顾名思义它是以 CORS 的形式跨域;当然该模式也可以同域请求不需要后端额外的 CORS 支持;其对应的 response type 为
- cors
。
- cors
: 该模式用于跨域请求但是服务器不带 CORS 响应头,也就是服务端不支持 CORS;这也是 fetch 的特殊跨域请求方式;其对应的 response type 为
- no-cors
。
- opaque
针对跨域请求,cors 模式是常见跨域请求实现,但是 fetch 自带的
跨域请求模式则较为陌生,该模式有一个比较明显的特点:
- no-cors
这与
发送的请求类似,只是该模式不能访问响应的内容信息;但是它可以被其他 APIs 进行处理,例如 ServiceWorker。另外,该模式返回的 repsonse 可以在 Cache API 中被存储起来以便后续的对它的使用,这点对 script、CSS 和图片的 CDN 资源是非常合适的,因为这些资源响应头中都没有 CORS 头。
- <img/>
总的来说,fetch 的跨域请求是使用 CORS 方式,需要浏览器和服务端的支持。
来源: http://www.cnblogs.com/wonyun/p/fetch_polyfill_timeout_jsonp_cookie_progress.html