js学习笔记 fetch

什么是fetch

fetch、ajax、axios有什么区别

ajax

  • 本身是针对MVC的编程,不符合现在前端MVVM的浪潮
  • 基于原生的XHR开发,XHR本身的架构不清晰,已经有了fetch的替代方案
  • JQuery整个项目太大,单纯使用ajax却要引入整个JQuery非常的不合理(采取个性化打包的方案又不能享受CDN服务)

axios

  • 从浏览器中创建XMLHttpRequest
  • 从node.js 中发出http请求
  • 支持 Promise API
  • 客户端支持防止CSRF(伪脚本攻击)
  • 提供了并发请求API接口
  • 支持请求/响应拦截
  • 取消请求
  • 自动转换json数据

fetch

  • 基于promise,关注分离,没有将输入、输出和用事件来跟踪的状态混合在一个对象中
  • 更加底层化,提供了丰富的API(request, response)
  • 脱离了XHR,是ES规范里新的实现方式
    • fetchtch只对网络请求报错,对400,500都当做成功的请求,需要封装去处理
    • fetch默认不会带cookie,需要添加配置项
    • fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了量的浪费
    • fetch没有办法原生监测请求的进度,而XHR可以

fetch用法

response

1
2
3
4
5
6
7
8
9
10
11
12
{
body: ReadableStream
bodyUsed: false
headers: Headers
ok : true
redirected : false
status : 200
statusText : "OK"
type : "cors"
url : "http://some-website.com/some-url"
__proto__ : Response
}

从上面例子看出请求是成功的(ok是true,status是200),但是我们想获取的仓库名却不在这里。
请求的资源都存储在body中,作为一种可读的流。所以需要调用一个恰当方法将可读流转换为我们可以使用的数据。

  • JSON格式的,使用response.json方法来转换数据。
  • 一个XML格式文件,则调用response.text。
  • 图片,使用response.blob方法。
    所有这些方法(response.json等等)返回另一个Promise,所以可以调用.then方法处理我们转换后的数据。

处理异常

虽然希望Ajax响应成功,但是仍会有问题出现:

  • 可能尝试获取不存在的资源
  • 没有权限获取资源
  • 输入参数有误
  • 服务器抛出异常
  • 服务器超时
  • 服务器崩溃
  • API更改

fetch请求进入then方法,仅代表请求发送成功和接收请求,但是不能是否相应失败,所以需要手动判断并处理异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fetch('some-url')
.then(response => {
if (response.ok) {
return response.json()
} else {
return Promise.reject({
status: response.status,
statusText: response.statusText
})
}
})
.catch(error => {
if (error.status === 404) {
// do something about 404
}
})

但对于下面这些特定的错误不适用:
400:Bad request
例如,如果请求错误缺少必要的参数,就会返回400.
光在catch中告诉状态及原因短语并不足够。我们需要知道缺少什么参数。
所以服务器需要返回一个对象,告诉造成错误请求原因。如果使用Node和Express,会返回像下面这样的响应:

1
2
3
res.status(400).send({
err: 'no first name'
})

无法在最初的.then方法中reject,因为错误对象需要response.json来解析。
解决的方法是需要两个then方法。这样可以首先通过response.json读取,然后决定怎么处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
fetch('some-error')
.then(handleResponse)
function handleResponse(response) {
return response.json()
.then(json => {
if (response.ok) {
return json
} else {
return Promise.reject(json)
}
})
}

首先我们调用response.json读取服务器发来的JSON数据,response.json返回Promise,所以可以链式调用.then方法。

在第一个.then中调用第二个.then,因为我们仍希望通过repsonse.ok判断响应是否成功。
如果想发送状态和原因短语,可以使用Object.assign()将二者结合为一个对象。

1
2
3
4
5
let error = Object.assign({}, json, {
status: response.status,
statusText: response.statusText
})
return Promise.reject(error)

可以使用这样新的handleResponse函数,让数据能自动的进入.then和.catch中。

1
2
3
4
fetch('some-url')
.then(handleResponse)
.then(data => console.log(data))
.catch(error => console.log(error))

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

fetch默认不携带cookie

fetch请求对某些错误http状态不会reject(400,500)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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方式来实现,起到真正超时丢弃当前的请求。

方法一:单纯setTimeout方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)
}
)
})
}

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

1
2
3
4
5
6
7
8
9
10
11
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丢弃掉,它在后台仍然会发送到服务器端,只是本次请求的响应内容被丢弃而已

fetch不支持JSONP

fetch不支持progress事件

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

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透明的原因。

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

参考链接

使用 Fetch